mySDRDocs

API reference

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.

POSThttps://mysdr.app/api/v1/outreach/linkedin/sequences
View Markdown

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

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

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

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

{
  "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"
}