Quickstart
Run a complete campaign against a simulated LinkedIn, replies included, within minutes. Everything below is plain HTTP you can paste into a terminal, in order; every step is also an MCP tool, so you can hand this page to your agent instead.
Base URL: https://api.getoutvoy.com/v1. Every request carries Authorization: Bearer <token>. Responses are { "data": … }, lists add "meta", errors are { "errorCode", "message" }. The full reference is at api.getoutvoy.com/docs.
1. Sign up and create an API key
Sign up at app.getoutvoy.com/sign-up and create a key under Agents & API, or do both with curl. Signing up creates your workspace with no plan yet: you can read it and create keys, but senders, imports and launches need a plan. Start the 7-day free trial in the console (Settings → Billing, card required, cancel anytime); it gives you the chosen plan's limits, sandbox senders included.
curl https://api.getoutvoy.com/auth/sign-up \
-H "Content-Type: application/json" \
-d '{ "email": "ada@acme.dev", "name": "Ada Lovelace", "password": "correct-horse-battery", "workspaceName": "Acme" }'
{
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresAt": "2026-10-28T22:46:02.754Z",
"user": { "id": "user_7btyAwcK", "email": "ada@acme.dev", "name": "Ada Lovelace", ... }
}
}
API keys can only be created with a console session (this token), never with another key, so a leaked key can't mint its own replacement. The secret is shown once: store it in your secret manager.
export OUTVOY_SESSION="eyJhbGciOiJIUzI1NiIs..." # from data.token
curl https://api.getoutvoy.com/v1/api-keys \
-H "Authorization: Bearer $OUTVOY_SESSION" \
-H "Content-Type: application/json" \
-d '{ "name": "Quickstart" }'
{
"data": {
"id": "key_JpAPY5ZI",
"name": "Quickstart",
"prefix": "ov_Sxf2",
"createdAt": "2026-10-28T14:46:02.769Z",
"secret": "ov_Sxf2pFaeM..."
}
}
export OUTVOY_API_KEY="ov_Sxf2pFaeM..." # from data.secret
curl https://api.getoutvoy.com/v1/me \
-H "Authorization: Bearer $OUTVOY_API_KEY"
{
"data": {
"apiKey": { "id": "key_JpAPY5ZI", "name": "Quickstart" },
"workspace": {
"id": "org_ICBUruZL",
"name": "Acme",
"plan": "growth",
"planName": "Growth",
"limits": { "linkedinSenders": 3, "includedLinkedinSenders": 3, "extraLinkedinSenders": 0, "sandboxSenders": 5, "enrollmentsPerMonth": 3000, "liveCampaigns": 20 },
"billing": { "source": "subscription", "status": "trialing", "trialEndsAt": "2026-11-04T14:46:02.690Z", "renewsAt": "2026-11-04T14:46:02.690Z" },
"usage": { "linkedinSenders": 0, "sandboxSenders": 0, "enrollmentsThisMonth": 0, "liveCampaigns": 0, "lists": 0, "launchedCampaigns": 0, "unreadConversations": 0 },
"createdAt": "2026-10-28T14:46:02.690Z"
}
}
}
2. Create a sandbox sender
A sandbox sender is a simulated LinkedIn account: active at once, searching a simulated population of 5,000 people, on a compressed clock where a day passes in a minute. About 45% of people accept an invitation and some reply. When you're ready for a real account, POST /v1/senders/connect-link returns a hosted page where its owner signs in to LinkedIn.
curl https://api.getoutvoy.com/v1/senders/sandbox \
-H "Authorization: Bearer $OUTVOY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Lena Hoffmann" }'
{
"data": {
"id": "snd_QoHXvZaM",
"kind": "sandbox",
"status": "active",
"name": "Lena Hoffmann",
"profileUrl": "https://www.linkedin.com/in/sbx-sender-lena-hoffmann-6hf4",
"limits": { "dailyInvites": 40, "weeklyInvites": 200, "dailyMessages": 150, "dailyProfileViews": 150, "dailyPostLikes": 100 },
"effectiveLimits": { "dailyInvites": 40, "weeklyInvites": 200, "dailyMessages": 150, "dailyProfileViews": 150, "dailyPostLikes": 100 },
"schedule": { "timezone": "UTC", "days": [1, 2, 3, 4, 5, 6, 7], "start": "00:00", "end": "23:59" },
"warmup": { "enabled": false, "startPercent": 30, "durationDays": 14, "startedAt": "2026-10-28T14:46:02.821Z" },
"usage": { "invitesToday": 0, "invitesThisWeek": 0, "messagesToday": 0, "profileViewsToday": 0, "postLikesToday": 0 },
"connectedAt": "2026-10-28T14:46:02.821Z",
...
}
}
Sandbox senders get the hard ceilings as limits and no warm-up or working hours, so a rehearsal runs fast. A real LinkedIn sender starts at the safe defaults.
export SENDER_ID="snd_QoHXvZaM" # from data.id
3. Find people
Search filters take LinkedIn ids for locations, industries and companies. Look them up through a sender, then preview one page of results. Nothing is saved yet.
curl "https://api.getoutvoy.com/v1/search/parameters?senderId=$SENDER_ID&type=LOCATION&q=Germany" \
-H "Authorization: Bearer $OUTVOY_API_KEY"
{
"data": [
{ "id": "101282230", "title": "Germany" },
{ "id": "90000008", "title": "Berlin, Germany" },
{ "id": "90000009", "title": "Munich, Bavaria, Germany" },
{ "id": "90000010", "title": "Hamburg, Germany" }
]
}
curl https://api.getoutvoy.com/v1/search/people \
-H "Authorization: Bearer $OUTVOY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "senderId": "'$SENDER_ID'", "filters": { "title": "Head of RevOps", "locationIds": ["101282230"] } }'
{
"data": [
{
"profileUrl": "https://www.linkedin.com/in/sbx-pablo-wilson-fvod",
"publicIdentifier": "sbx-pablo-wilson-fvod",
"providerId": "ACoSBX0002kfvod",
"firstName": "Pablo",
"lastName": "Wilson",
"fullName": "Pablo Wilson",
"headline": "Head of RevOps at Soylent Data",
"title": "Head of RevOps",
"company": "Soylent Data",
"location": "Munich, Bavaria, Germany"
},
...
],
"meta": { "nextCursor": "25", "total": 35 }
}
Instead of filters you can pass a url: a LinkedIn or Sales Navigator people search copied from the browser.
4. Create a list and import
curl https://api.getoutvoy.com/v1/lists \
-H "Authorization: Bearer $OUTVOY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: list-revops-de" \
-d '{ "name": "RevOps DE", "description": "Heads of RevOps in Germany" }'
{
"data": {
"id": "list_f6UScEuH",
"name": "RevOps DE",
"description": "Heads of RevOps in Germany",
"leadCount": 0,
"createdAt": "2026-10-28T14:46:02.854Z",
"updatedAt": "2026-10-28T14:46:02.854Z"
}
}
export LIST_ID="list_f6UScEuH" # from data.id
Imports run in the background through one of your senders, from the same search (filters or url) or from the people who reacted to or commented on a post ("type": "post_engagers" with a postUrl). maxResults caps how many people are read (default 250, at most 1,000). Twenty is plenty for a rehearsal.
curl https://api.getoutvoy.com/v1/lists/$LIST_ID/imports \
-H "Authorization: Bearer $OUTVOY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"senderId": "'$SENDER_ID'",
"source": {
"type": "search",
"filters": { "title": "Head of RevOps", "locationIds": ["101282230"] },
"maxResults": 20
}
}'
{
"data": {
"id": "imp_WVRtHwMy",
"listId": "list_f6UScEuH",
"senderId": "snd_QoHXvZaM",
"source": {
"type": "search",
"filters": { "title": "Head of RevOps", "locationIds": ["101282230"], "industryIds": [], "companyIds": [], "networkDistance": [] },
"maxResults": 20
},
"status": "queued",
"found": 0,
"imported": 0,
"skipped": 0,
...
}
}
export IMPORT_ID="imp_WVRtHwMy" # from data.id
Poll the import until status is completed (or failed, with error), or wait for the import.completed event. Blocklisted people, people already in the list and profiles without a usable identifier are counted in skipped.
curl https://api.getoutvoy.com/v1/imports/$IMPORT_ID \
-H "Authorization: Bearer $OUTVOY_API_KEY"
{
"data": {
"id": "imp_WVRtHwMy",
...
"status": "completed",
"found": 20,
"imported": 20,
"skipped": 0,
"startedAt": "2026-10-28T14:46:03.319Z",
"finishedAt": "2026-10-28T14:46:03.359Z",
...
}
}
5. Create a campaign
A campaign is a pool of senders, one or more lists and a linear sequence of steps: visit_profile, like_post, invite (at most one, optional note) and message (text). Each step has an optional delay of { "amount", "unit" } with unit minutes, hours or days, counted from the previous step; the step right after an invite counts from the acceptance. Some rules are always on: a reply stops the sequence, nobody is messaged before they are connected, and an invitation nobody accepts is withdrawn after settings.acceptanceWindowDays (default 14). With several senders, settings.secondChance ({ "extraSenders": 1, "after": { "amount": 3, "unit": "days" } }) lets another sender invite whoever hasn't accepted, and settings.senderAssignment: "one_per_company" keeps a company with one sender.
curl https://api.getoutvoy.com/v1/campaigns \
-H "Authorization: Bearer $OUTVOY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: campaign-revops-de" \
-d '{
"name": "RevOps DE",
"senderIds": ["'$SENDER_ID'"],
"listIds": ["'$LIST_ID'"],
"steps": [
{ "type": "visit_profile" },
{ "type": "invite", "delay": { "amount": 1, "unit": "days" },
"note": "Hi {{firstName | \"there\"}}, I run pipeline audits for SaaS teams like {{company}}. Open to connecting?" },
{ "type": "message", "delay": { "amount": 1, "unit": "days" },
"text": "Thanks for connecting, {{firstName}}. {{fields.icebreaker | \"Happy to compare notes on RevOps.\"}}" }
],
"settings": { "acceptanceWindowDays": 14 }
}'
{
"data": {
"id": "cmp_Xd4zDgV7",
"name": "RevOps DE",
"status": "draft",
"senderIds": ["snd_QoHXvZaM"],
"listIds": ["list_f6UScEuH"],
"steps": [
{ "type": "visit_profile", "delay": { "amount": 0, "unit": "minutes" } },
{ "type": "invite", "delay": { "amount": 1, "unit": "days" }, "note": "Hi {{firstName | \"there\"}}, ..." },
{ "type": "message", "delay": { "amount": 1, "unit": "days" }, "text": "Thanks for connecting, {{firstName}}. ..." }
],
"settings": { "acceptanceWindowDays": 14, "audience": "anyone", "ifAlreadyConnected": "continue", "senderAssignment": "round_robin" },
"directLeadCount": 0,
"stats": { "enrolled": 0, "active": 0, "invited": 0, "accepted": 0, "acceptanceRate": 0, "messaged": 0, "replied": 0, "replyRate": 0, "completed": 0, "failed": 0, "stopped": 0 },
...
}
}
export CAMPAIGN_ID="cmp_Xd4zDgV7" # from data.id
The sequence is checked now: a template that doesn't parse, a second invite, or a message with no invite before it is refused with 422 INVALID_CAMPAIGN_SEQUENCE_ERROR, naming the step.
6. Preview every message
Preview renders every step for every lead and lists every problem. Nothing is saved and nothing is sent. A lead with an issue is skipped at launch and never sent the broken text. See Templates for the variables and the issue codes.
curl -X POST "https://api.getoutvoy.com/v1/campaigns/$CAMPAIGN_ID/preview?limit=2" \
-H "Authorization: Bearer $OUTVOY_API_KEY"
{
"data": {
"campaignId": "cmp_Xd4zDgV7",
"canLaunch": true,
"campaignIssues": [],
"totals": {
"leads": 20,
"eligible": 20,
"blocked": 0,
"enrollableThisMonth": 20,
"issues": { "missing_variable": 0, "note_too_long": 0, "message_too_long": 0, "blocklisted": 0, "already_in_campaign": 0, "already_enrolled": 0 }
},
"leads": [
{
"lead": { "id": "lead_0g7YpEdO", "fullName": "Paul Murphy", "headline": "Head of RevOps at Quanta Billing", "company": "Quanta Billing", ... },
"eligible": true,
"sender": { "id": "snd_QoHXvZaM", "name": "Lena Hoffmann", "kind": "sandbox", "status": "active" },
"steps": [
{ "index": 0, "type": "visit_profile" },
{ "index": 1, "type": "invite", "text": "Hi Paul, I run pipeline audits for SaaS teams like Quanta Billing. Open to connecting?", "length": 86 },
{ "index": 2, "type": "message", "text": "Thanks for connecting, Paul. Happy to compare notes on RevOps.", "length": 62 }
],
"issues": []
},
...
]
},
"meta": { "nextCursor": "Mg" }
}
7. Launch
Launch checks the campaign again (refused with 422 and details.issues while a blocking issue remains), enrolls every eligible lead and assigns senders round-robin. Leads with an issue are recorded as skipped. From here the engine plans each action inside the sender's limits and working hours.
curl -X POST https://api.getoutvoy.com/v1/campaigns/$CAMPAIGN_ID/launch \
-H "Authorization: Bearer $OUTVOY_API_KEY"
{
"data": {
"campaign": { "id": "cmp_Xd4zDgV7", "name": "RevOps DE", "status": "running", "launchedAt": "2026-10-28T14:46:22.956Z", ... },
"enrolled": 20,
"skipped": 0,
"waitingForPlan": 0
}
}
Ask any enrollment what happens next. While it is live, waitingReason answers in plain English, as of the moment you read it (“Next: send invite at …”, “Waiting: sender Lena Hoffmann paused by you”); a finished one says how it ended in stopDetail, and once there is a conversation with the lead it carries its conversationId:
curl "https://api.getoutvoy.com/v1/campaigns/$CAMPAIGN_ID/enrollments?limit=2" \
-H "Authorization: Bearer $OUTVOY_API_KEY"
{
"data": [
{
"id": "enr_1b8nUZYE",
"campaignId": "cmp_Xd4zDgV7",
"status": "active",
"stepIndex": 0,
"nextActionAt": "2026-10-28T14:46:22.956Z",
"waitingReason": "Next: visit their profile — due now, waiting for Lena Hoffmann's turn",
"fields": {},
"sender": { "id": "snd_QoHXvZaM", "name": "Lena Hoffmann", "kind": "sandbox", "status": "active" },
"lead": { "id": "lead_0g7YpEdO", "fullName": "Paul Murphy", "company": "Quanta Billing", ... },
"enrolledAt": "2026-10-28T14:46:22.956Z",
...
},
...
],
"meta": { "nextCursor": "eyJhdCI6IjIwMjYtMTAtMjhUMTQ6NDY6MjIuOTU2WiIs..." }
}
A sandbox sender takes one action at a time, so within a few minutes you'll see visits, invitations, acceptances after a simulated delay, messages and replies. GET /v1/campaigns/$CAMPAIGN_ID/enrollments/{enrollmentId} shows one lead's every action with its rendered text.
8. Read events
Everything that happens is an event in one stable shape. Read them as a cursor feed, passing meta.nextCursor back as after to get only what's new, or receive them on webhooks.
curl "https://api.getoutvoy.com/v1/events?types=lead.invite_accepted,lead.replied" \
-H "Authorization: Bearer $OUTVOY_API_KEY"
{
"data": [
{
"id": "evt_01M61FT70HFFGQ64TSZTFBK84R",
"type": "lead.invite_accepted",
"occurredAt": "2026-10-28T14:49:35.852Z",
"data": { "leadId": "lead_GaxMMGA7", "invitedAt": "2026-10-28T14:49:13.349Z", "acceptedAt": "2026-10-28T14:49:35.852Z", ... }
},
...
{
"id": "evt_01M61FWGAXMJG6N9SSJRJW6355",
"type": "lead.replied",
"occurredAt": "2026-10-28T14:50:53.377Z",
"data": {
"lead": { "id": "lead_GaxMMGA7", "name": "Laura Walker", "company": "Wayne Payments", "headline": "Head of RevOps at Wayne Payments", "profileUrl": "https://www.linkedin.com/in/sbx-laura-walker-wjqi" },
"text": "Appreciate the note. We're locked in until Q3, ping me then?",
"leadId": "lead_GaxMMGA7",
"sender": { "id": "snd_QoHXvZaM", "name": "Lena Hoffmann" },
"campaign": { "id": "cmp_Xd4zDgV7", "name": "RevOps DE" },
"senderId": "snd_QoHXvZaM",
"messageId": "msg_E6zLzXPg",
"campaignId": "cmp_Xd4zDgV7",
"enrollmentId": "enr_p1igsVd8",
"conversationId": "cnv_ycUrC1uw",
"sequenceFinished": true
}
}
],
"meta": { "nextCursor": "evt_01M61FWGAXMJG6N9SSJRJW6355", "prevCursor": "evt_01M61FT70HFFGQ64TSZTFBK84R" }
}
9. Answer a reply
A reply stops that lead's sequence. Find unread conversations, read one (that marks it read) and answer from the sender who owns it.
curl "https://api.getoutvoy.com/v1/conversations?unread=true" \
-H "Authorization: Bearer $OUTVOY_API_KEY"
{
"data": [
{
"id": "cnv_ycUrC1uw",
"sender": { "id": "snd_QoHXvZaM", "name": "Lena Hoffmann", "kind": "sandbox", "status": "active" },
"lead": { "id": "lead_GaxMMGA7", "fullName": "Laura Walker", "company": "Wayne Payments", ... },
"campaign": { "id": "cmp_Xd4zDgV7", "name": "RevOps DE" },
"lastMessage": { "text": "Appreciate the note. We're locked in until Q3, ping me then?", "direction": "inbound", "sentAt": "2026-10-28T14:50:53.377Z" },
"lastMessageAt": "2026-10-28T14:50:53.377Z",
"unread": true,
"createdAt": "2026-10-28T14:50:38.379Z"
}
],
"meta": {}
}
export CONVERSATION_ID="cnv_ycUrC1uw" # from data[0].id
curl https://api.getoutvoy.com/v1/conversations/$CONVERSATION_ID \
-H "Authorization: Bearer $OUTVOY_API_KEY"
{
"data": {
"id": "cnv_ycUrC1uw",
...
"unread": false,
"messages": [
{
"id": "msg_0fxU72T3",
"conversationId": "cnv_ycUrC1uw",
"direction": "outbound",
"text": "Thanks for connecting, Laura. Happy to compare notes on RevOps.",
"sentAt": "2026-10-28T14:50:38.371Z",
"actionId": "act_YIbniBmo"
},
{
"id": "msg_E6zLzXPg",
"conversationId": "cnv_ycUrC1uw",
"direction": "inbound",
"text": "Appreciate the note. We're locked in until Q3, ping me then?",
"sentAt": "2026-10-28T14:50:53.377Z"
}
]
}
}
curl https://api.getoutvoy.com/v1/conversations/$CONVERSATION_ID/messages \
-H "Authorization: Bearer $OUTVOY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Will do, Laura. I will check back in early Q3." }'
{
"data": {
"id": "msg_Ogz9gBCj",
"conversationId": "cnv_ycUrC1uw",
"direction": "outbound",
"text": "Will do, Laura. I will check back in early Q3.",
"sentAt": "2026-10-28T14:51:03.535Z"
}
}
Then check the funnel with GET /v1/campaigns/$CAMPAIGN_ID/stats: enrolled, invited, accepted, messaged and replied, in total, per step and per sender. That's the whole loop.
Next
- Connect your agent over MCP and ask it to do all of the above in one sentence.
- Receive events on a webhook and verify the signature.
- Read the safety defaults before connecting a real LinkedIn account.