# Start a LinkedIn sequence

Runs a LinkedIn sequence you wrote, for up to 50 people, from the user's own LinkedIn account: an invite (with `invite_note`, or without a note), `messages` after the invite is accepted, `messages_if_connected` for people who already are a connection (they never get the invite note), and the invite is withdrawn after `withdraw_after_days` (default 16) if it is still pending. Each person may bring their own texts (the same number of messages as the shared plan) and `variables`. mySDR does the LinkedIn part of your outreach: you pick the people and write every text, mySDR sends it from the user's own LinkedIn account within their working hours and the SafeOutreach limits, notices accepted invites and replies, and stops a person's sequence when they reply. These routes are the tools `start_linkedin_sequence`, `get_outreach_status`, `get_outreach_events` and `stop_linkedin_sequence` of the assistant server; your own emails you log with POST /outreach/activity. Free: no credits.

mySDR fills in `{{first_name}}`, `{{last_name}}`, `{{full_name}}`, `{{company}}`, `{{title}}` and each person's `variables`; a placeholder without a value skips that person, and a text never goes out with a placeholder in it.

**There is no approval step in mySDR**: without `dry_run` the sequence starts at once. So first call with `dry_run: true`. It lists everyone who would start (`people`, in the order you sent them, with the values of their placeholders), the whole sequence of everyone who brought their own texts or variables plus one example of the shared texts (`previews`), who is skipped and why (`skipped`, with their `index` in `leads`), and an estimate of when they start within the limits (`schedule`). Show the user the texts, who gets them and when, and start only after they said yes. People already in a running sequence or conversation, stopped, marked not interested, archived, snoozed or waiting for the user’s decision (`archived`, `snoozed`, `needs_decision`: the person or their company, as `check_before_sending` blocks them), or on the exclusions (`excluded`, with the reason) are skipped. Everyone you start a sequence for lands in the Leads table (`added_to_leads` counts the new ones), with their state in its Outreach column.

**Who gets an invite.** Everyone who is not a LinkedIn connection yet gets the invite first, with or without a note (a step with `without_note: true`): there is no way to reach only existing connections. An invite without a note and nothing after it is not supported: give a note or a message.

**The LinkedIn connection.** LinkedIn steps go out from the user’s own LinkedIn account: the one connected by the mySDR user who created your key or clicked Allow. Only that user can connect it, in mySDR under Settings → LinkedIn (https://mysdr.app/en/settings?tab=linkedin): they click Connect LinkedIn and sign in to LinkedIn once, in a secure sign-in window (LinkedIn may ask them for a verification code; mySDR never sees their password). You cannot do this for them: there is no API for it, and never ask for their LinkedIn password, code or cookies. linkedin_connected in the outreach status says whether it is done; a connection that stopped working is reconnected on the same page.

## Authentication

Send `Authorization: Bearer gtk_…`. Create the key under **Connect assistant → API keys**. Every `gtk_` key can call these routes, also while the Signals API is closed. A missing, invalid or revoked key is **401**.

### Body parameters

- **`campaign_name`** `string` — The name of a new campaign, as the user sees it, up to 80 characters. Or:
- **`campaign_id`** `string` — Add people to a LinkedIn campaign you started earlier.
- **`invite_note`** `string` — The shared invite note, at most 300 characters after placeholders. Empty = an invite without a note.
- **`messages`** `object[]` — Messages after the invite is accepted, in order (at most 4, 8000 characters each): `text` (empty when every person brings their own) and `wait_days`, required (the first: days after the invite is accepted, 0 = a few minutes; later ones: days after the previous message, at least 1; at most 60).
- **`messages_if_connected`** `object[]` — The same, for people who already are a connection: the first goes out `wait_days` after the start (0 = at the start). Without these, existing connections get nothing.
- **`withdraw_after_days`** `integer` — Withdraw a pending invite after this many days, from 7 to 60. Default 16.
- **`leads`** `object[]` · **required** — 1 to 50 people, each with exactly one of the keys below.
- **`leads[].id`** `string` — A row of GET /tables/leads: its `id` (a number, `per_…` or `sav_…`; a number may also be sent as a number). A `sav_…` uses that row’s LinkedIn profile and name.
- **`leads[].lead_id`** `integer` — A lead from the signals: the number GET /tables/leads gives (`201`, or as text `"201"`). A row id such as `sav_…` or `per_…` goes in `id`, and in `lead_id` is a 400 that says so.
- **`leads[].person_id`** `string` — A `per_…` from the Signals API (company_leads, search_signals), kept 30 days.
- **`leads[].linkedin_url`** `string` — A LinkedIn profile URL (`https://www.linkedin.com/in/…`) you found yourself.
- **`leads[].first_name / last_name / company / title`** `string` — Overrides what mySDR knows; used for the placeholders.
- **`leads[].variables`** `object` — Extra placeholder values for this person, as strings (up to 500 characters each), e.g. `{"hook": "your post on hiring"}`.
- **`leads[].invite_note`** `string` — This person’s own invite note; `""` = no note for them.
- **`leads[].messages / messages_if_connected`** `string[]` — This person’s own texts, in the same count and order as the shared plan.
- **`leads[].reason`** `string` — Why this person, in one sentence (up to 500 characters); shown with the lead in mySDR.
- **`dry_run`** `boolean` — `true`: check and show the texts only; nothing is created.

## Response

- **`dry_run`** `boolean` · **required** — Whether this was a dry run.
- **`would_start`** `integer` — With `dry_run`: how many sequences would start.
- **`campaign_id / campaign / new_campaign`** `string | null / string / boolean` — With `dry_run`: the campaign they would go in, and whether a start would create it (`campaign_name`). With both `campaign_id` and `campaign_name`, `campaign_id` decides and a note says so.
- **`people`** `object[]` — With `dry_run`: everyone who would start, in the order of `leads`: `index` (the place in `leads`), `lead` (how you referred to them), `name`, `values` (each placeholder in their texts with its value), `previewed` (their whole sequence is in `previews`) and `without_note` (if not a connection yet, they get an invite without a note).
- **`previews`** `object[]` — With `dry_run`: the whole sequence as it goes out of everyone with their own texts or variables (`texts`: `own_texts`), plus the first person with only the shared texts as their example (`shared_texts`): `index`, `lead`, `name`, `texts`, `steps` (`step`, `for`, `when`, `text`, `without_note` on the invite, `status`, `at`). Everyone else gets the shared texts with their own `values`.
- **`schedule`** `object | null` — When they start, estimated within today’s limits, working days and any pause or warm-up: `estimate` (a sentence to show the user), `today` (how many can start today), `first_day` and `last_day` (dates; `last_day` null = beyond 120 days) and `weekly_pause` (the weekly invite limit makes invites wait until the last 7 days have room again). People who already are a connection need no invite and may go sooner. Null when nobody starts.
- **`started`** `integer` — Without `dry_run`: how many sequences started.
- **`campaign_id / campaign / campaign_created`** `string / string / boolean` — Without `dry_run`: the campaign, and whether this call made it.
- **`sequences`** `object[]` — Without `dry_run`: per person `index`, `lead`, `name`, `state` (`scheduled`) and `scheduled_at`.
- **`skipped`** `object[]` · **required** — Who did not start, and why, in the order of `leads`: `index` (the place in `leads`), `lead`, `code`, `reason`.
- **`notes`** `string[]` · **required** — What the plan did by itself, or what is missing: the LinkedIn connection, who gets an invite without a note, people without a name (they cannot be shown in the Leads table).
- **`added_to_leads`** `integer` — Without `dry_run`: people new in the Leads table.
- **`linkedin_connected`** `boolean` · **required** — The user behind this key has a working LinkedIn connection.
- **`connect_linkedin`** `string | null` · **required** — Without a connection: what to ask the user, with the link. Null when connected.
- **`connect_url`** `string` · **required** — The page in mySDR where the user connects or reconnects LinkedIn (Settings → LinkedIn).
- **`paused`** `object | null` · **required** — A pause on the organization or the account: `reason` and `until` (null = until someone lifts it). Nothing goes out meanwhile, except when an invite limit is full: then only invites and new sequences wait, and messages, InMails and withdrawals in sequences that already started keep going (`reason` says which).
- **`warmup`** `string | null` · **required** — The warm-up of the LinkedIn account, in a sentence: before the user’s first LinkedIn campaign, that the first one starts a warm-up (the limits drop and grow back day by day); while it runs, how far it is and until when. Null when there is none.
- **`limits`** `object` · **required** — `invites_today`, `invites_per_day`, `invites_last_7_days`, `invites_per_7_days` (invites with a note), `invites_per_7_days_without_note` (a stricter weekly limit), `new_sequences_today`, `new_sequences_per_day`.
- **`next`** `string` · **required** — What to do now.
- **`dashboard_url`** `string` · **required** — The Leads table in mySDR on this campaign.

## Credits

Free. This call never uses credits. Each sequence uses one of today’s new sequences, and an invite one of today’s invites and of the last 7 days (`limits`).

## Errors

- **400** `invalid_request` — An unknown field, no `campaign_name` or `campaign_id`, more than 50 people, a raw text longer than its field allows, or a plan without any text. A person with none or several keys, or a text that is too long after its placeholders are filled in, does not fail the call: that person is in `skipped`.
- **404** `not_found` — No LinkedIn campaign of this user has this `campaign_id`.
- **409** `invalid_state` — Without `dry_run`: LinkedIn is not connected for this user (the message says where they connect it), or the plan allows no more running campaigns.
- **409** `invalid_state` — The key has no user behind it, so there is no LinkedIn account to send from.
- **401** `authentication_error` — The Authorization header is missing, or the key is unknown or revoked.
- **403** `permission_error` — API access is not part of your plan, or your account is suspended.
- **429** `rate_limit_error` — More than 60 requests per minute, or more than 3 at the same time, on one key. Wait for the Retry-After header.
- **503** `service_unavailable` — The API is temporarily unavailable. Retry later.
- **500** `internal_error` — Something went wrong on our side.

## Example

### cURL

```bash
curl -X POST "https://mysdr.app/api/v1/outreach/linkedin/sequences" \
  -H "Authorization: Bearer $MYSDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_name": "CFOs who just raised",
    "invite_note": "Hi {{first_name}}, congrats on the round at {{company}}. Happy to connect.",
    "messages": [
      {
        "text": "Thanks for connecting, {{first_name}}! Quick question about {{company}}.",
        "wait_days": 0
      },
      {
        "text": "Following up, {{first_name}}. Worth a chat?",
        "wait_days": 3
      }
    ],
    "messages_if_connected": [
      {
        "text": "Hi {{first_name}}, saw the news at {{company}}. Worth a chat?",
        "wait_days": 0
      }
    ],
    "leads": [
      {
        "id": "201",
        "reason": "Raised a Series A last week."
      },
      {
        "linkedin_url": "https://www.linkedin.com/in/jane-example",
        "first_name": "Jane",
        "company": "Acme Logistics"
      }
    ],
    "dry_run": true
  }'
```

### JavaScript

```javascript
const response = await fetch('https://mysdr.app/api/v1/outreach/linkedin/sequences', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MYSDR_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "campaign_name": "CFOs who just raised",
    "invite_note": "Hi {{first_name}}, congrats on the round at {{company}}. Happy to connect.",
    "messages": [
      {
        "text": "Thanks for connecting, {{first_name}}! Quick question about {{company}}.",
        "wait_days": 0
      },
      {
        "text": "Following up, {{first_name}}. Worth a chat?",
        "wait_days": 3
      }
    ],
    "messages_if_connected": [
      {
        "text": "Hi {{first_name}}, saw the news at {{company}}. Worth a chat?",
        "wait_days": 0
      }
    ],
    "leads": [
      {
        "id": "201",
        "reason": "Raised a Series A last week."
      },
      {
        "linkedin_url": "https://www.linkedin.com/in/jane-example",
        "first_name": "Jane",
        "company": "Acme Logistics"
      }
    ],
    "dry_run": true
  })
});
const body = await response.json();
```

### Python

```python
import json
import os
import urllib.request

payload = {
  "campaign_name": "CFOs who just raised",
  "invite_note": "Hi {{first_name}}, congrats on the round at {{company}}. Happy to connect.",
  "messages": [
    {
      "text": "Thanks for connecting, {{first_name}}! Quick question about {{company}}.",
      "wait_days": 0
    },
    {
      "text": "Following up, {{first_name}}. Worth a chat?",
      "wait_days": 3
    }
  ],
  "messages_if_connected": [
    {
      "text": "Hi {{first_name}}, saw the news at {{company}}. Worth a chat?",
      "wait_days": 0
    }
  ],
  "leads": [
    {
      "id": "201",
      "reason": "Raised a Series A last week."
    },
    {
      "linkedin_url": "https://www.linkedin.com/in/jane-example",
      "first_name": "Jane",
      "company": "Acme Logistics"
    }
  ],
  "dry_run": True
}

request = urllib.request.Request(
    "https://mysdr.app/api/v1/outreach/linkedin/sequences",
    headers={
    "Authorization": f"Bearer {os.environ['MYSDR_API_KEY']}",
    "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode(),
    method="POST",
)
with urllib.request.urlopen(request) as response:
    body = json.load(response)
```

### Example response

```json
{
  "dry_run": true,
  "would_start": 2,
  "campaign_id": null,
  "campaign": "CFOs who just raised",
  "new_campaign": true,
  "people": [
    {
      "index": 0,
      "lead": "201",
      "name": "Anna Peeters",
      "values": {
        "first_name": "Anna",
        "company": "AnnaCo"
      },
      "previewed": true,
      "without_note": false
    },
    {
      "index": 1,
      "lead": "https://www.linkedin.com/in/jane-example",
      "name": "Jane",
      "values": {
        "first_name": "Jane",
        "company": "Acme Logistics"
      },
      "previewed": false,
      "without_note": false
    }
  ],
  "previews": [
    {
      "index": 0,
      "lead": "201",
      "name": "Anna Peeters",
      "texts": "shared_texts",
      "steps": [
        {
          "step": "invite",
          "for": "everyone",
          "when": null,
          "text": "Hi Anna, congrats on the round at AnnaCo. Happy to connect.",
          "without_note": false,
          "status": "waiting",
          "at": null
        },
        {
          "step": "message",
          "for": "after_accept",
          "when": "Right after the invite is accepted",
          "text": "Thanks for connecting, Anna! Quick question about AnnaCo.",
          "status": "waiting",
          "at": null
        }
      ]
    }
  ],
  "schedule": {
    "estimate": "All 2 can start today, within working hours.",
    "today": 2,
    "first_day": "2026-10-24",
    "last_day": "2026-10-24",
    "weekly_pause": false
  },
  "skipped": [],
  "notes": [],
  "linkedin_connected": true,
  "connect_linkedin": null,
  "connect_url": "https://mysdr.app/en/settings?tab=linkedin",
  "paused": null,
  "warmup": null,
  "limits": {
    "invites_today": 3,
    "invites_per_day": 20,
    "invites_last_7_days": 41,
    "invites_per_7_days": 100,
    "invites_per_7_days_without_note": 75,
    "new_sequences_today": 3,
    "new_sequences_per_day": 25
  },
  "next": "Nothing was created. Show the user who gets the sequence (people), the texts (previews: the others get the shared texts with their own values) and when it would start (schedule); start (without dry_run) only after they said yes.",
  "dashboard_url": "https://mysdr.app/en/gtm/leads?outreach=any"
}
```
