Skip to content

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.

Request
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" }'
200 OK
{
  "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.

Save
export OUTVOY_SESSION="eyJhbGciOiJIUzI1NiIs..."   # from data.token
Request
curl https://api.getoutvoy.com/v1/api-keys \
  -H "Authorization: Bearer $OUTVOY_SESSION" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Quickstart" }'
200 OK
{
  "data": {
    "id": "key_JpAPY5ZI",
    "name": "Quickstart",
    "prefix": "ov_Sxf2",
    "createdAt": "2026-10-28T14:46:02.769Z",
    "secret": "ov_Sxf2pFaeM..."
  }
}
Save
export OUTVOY_API_KEY="ov_Sxf2pFaeM..."   # from data.secret
Request
curl https://api.getoutvoy.com/v1/me \
  -H "Authorization: Bearer $OUTVOY_API_KEY"
200 OK
{
  "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.

Request
curl https://api.getoutvoy.com/v1/senders/sandbox \
  -H "Authorization: Bearer $OUTVOY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Lena Hoffmann" }'
200 OK
{
  "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.

Save
export SENDER_ID="snd_QoHXvZaM"   # from data.id

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.

Request
curl "https://api.getoutvoy.com/v1/search/parameters?senderId=$SENDER_ID&type=LOCATION&q=Germany" \
  -H "Authorization: Bearer $OUTVOY_API_KEY"
200 OK
{
  "data": [
    { "id": "101282230", "title": "Germany" },
    { "id": "90000008", "title": "Berlin, Germany" },
    { "id": "90000009", "title": "Munich, Bavaria, Germany" },
    { "id": "90000010", "title": "Hamburg, Germany" }
  ]
}
Request
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"] } }'
200 OK
{
  "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

Request
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" }'
200 OK
{
  "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"
  }
}
Save
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.

Request
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
    }
  }'
200 OK
{
  "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,
    ...
  }
}
Save
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.

Request
curl https://api.getoutvoy.com/v1/imports/$IMPORT_ID \
  -H "Authorization: Bearer $OUTVOY_API_KEY"
200 OK
{
  "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.

Request
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 }
  }'
200 OK
{
  "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 },
    ...
  }
}
Save
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.

Request
curl -X POST "https://api.getoutvoy.com/v1/campaigns/$CAMPAIGN_ID/preview?limit=2" \
  -H "Authorization: Bearer $OUTVOY_API_KEY"
200 OK
{
  "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.

Request
curl -X POST https://api.getoutvoy.com/v1/campaigns/$CAMPAIGN_ID/launch \
  -H "Authorization: Bearer $OUTVOY_API_KEY"
200 OK
{
  "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:

Request
curl "https://api.getoutvoy.com/v1/campaigns/$CAMPAIGN_ID/enrollments?limit=2" \
  -H "Authorization: Bearer $OUTVOY_API_KEY"
200 OK
{
  "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.

Request
curl "https://api.getoutvoy.com/v1/events?types=lead.invite_accepted,lead.replied" \
  -H "Authorization: Bearer $OUTVOY_API_KEY"
200 OK
{
  "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.

Request
curl "https://api.getoutvoy.com/v1/conversations?unread=true" \
  -H "Authorization: Bearer $OUTVOY_API_KEY"
200 OK
{
  "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": {}
}
Save
export CONVERSATION_ID="cnv_ycUrC1uw"   # from data[0].id
Request
curl https://api.getoutvoy.com/v1/conversations/$CONVERSATION_ID \
  -H "Authorization: Bearer $OUTVOY_API_KEY"
200 OK
{
  "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"
      }
    ]
  }
}
Request
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." }'
200 OK
{
  "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