mySDRDocs

API reference

Update a lead

Changes where a person stands in your work, so the user sees it in the Leads table. Send only what changes. {id} is what GET /tables/leads gives (sav_…, per_…), a LinkedIn profile URL or their email address; someone who is not in the table yet, you save first with POST /tables/leads (the state can go in that same call). A status change is logged. mySDR keeps your whole process, so the user never needs a spreadsheet next to it: the state of each company and person (status with a reason, next action, email and contact points, your own fields), every email, reply and bounce you log, the exclusions, and a check before every send. The user sees all of it in the Leads and Companies tabs.

PATCHhttps://mysdr.app/api/v1/tables/leads/{id}
View Markdown

A lead from the signals on Signals (a number) takes the whole state too. Archiving, snoozing and the lead status land on Leads itself, so the user can undo them there (Leads has no reason: status_reason goes to the row’s log); the rest (fields, next_action, email, needs_decision …) goes on your own row behind that lead, which the first such call makes (or finds, if you saved them before). It stays one row in the table, the response gives that row’s id and signal_lead_id, and the lead lists it in saved_ids. Only a signal lead without a LinkedIn profile takes just status and lead_status.

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.

Path parameters

  • id string · required — The row, a LinkedIn profile URL or an email address (URL-encoded).

Body parameters

  • status string — new (in the work), needs_decision (waits for the user; say why in status_reason), snoozed (until snooze_until, then new again) or archived (out; say why). One of: new, needs_decision, snoozed, archived.
  • status_reason string | null — Why, in a few words: "Below 50 FTE", "Rescan 23/11". Up to 500 characters.
  • snooze_until string | null — The day the row comes back as new: a date (2026-11-23, 09:00 Brussels time) or an ISO time. Alone it snoozes the row.
  • lead_status string | null — What they said. not_interested also stops LinkedIn for them and blocks a new sequence. One of: interested, meeting, later, not_interested.
  • next_action object | null — Your next step and when: at (a date or ISO time), kind (follow_up, recontact, rescan, approval, other), note, and email when that step is an email you will send yourself: { "text", "subject", "step", "template", "to" } (text required, up to 20,000 characters; subject up to 200 and template up to 80, as in POST /outreach/activity; step 1 to 20; to only if it is not the row’s address; kind defaults to follow_up). The user sees the planned email on Templates, with the emails you already logged above it, and GET /agenda gives it back on its day, so you keep no copy. A list (GET /tables/leads, /tables/companies) gives it without text; one row (GET /tables/leads/{id}, /tables/companies/{id}), the response of a change and GET /agenda give the text. Logging email_sent with the same step clears it (logged without a step, it clears one planned without a step), and so do a reply, a meeting, a bounce, an unsubscribe, a referral and not_interested; plan the next one. A new next_action replaces the current one and the email planned for it (replaced in the response gives them back); null clears both. GET /agenda lists the due ones.
  • out_of_office_until string | null — Away until this day (from an out-of-office reply).
  • email string | null — Their email address.
  • email_status string | null — What verification said about that address. One of: verified, catch_all, unverifiable, not_found, bounced.
  • fields object — Your own columns, merged with what is there: a name in lowercase letters, digits and _ (fte, ats, open_roles) with a value (text, number, true/false) or { "value", "label", "source_url", "observed_at" }; null removes one. Up to 40 per row. The user sees them as columns.
  • campaign_id string — Put the row in this campaign (POST /outreach/campaigns, or one from a LinkedIn sequence).
  • campaign_ids string[] — Replaces the campaigns of the row.
  • stage string | null — One of the organization's own stages (GET /stages), by its key (mql) or its name ("MQL"); null clears it. A stage sits next to status: it stops nothing and changes no status. A change is logged.
  • owner string | null — Who in the team follows the row up: me (the user behind the key), or the email address or user id of a colleague (GET /team); null clears it. A change is logged.
  • note string | null — Your note on the row, up to 500 characters.

Response

  • id string · required — The row: what list, PATCH and DELETE take.
  • object string · required — person or company.
  • name string · required — Its name.
  • company string | null — Their employer.
  • linkedin_url string | null — Their LinkedIn profile.
  • status string · required — Its status now.
  • status_reason string | null — Why.
  • snooze_until string | null — While snoozed: until when.
  • lead_status string | null — What they said.
  • next_action object | null — at, kind, note.
  • out_of_office_until string | null — While out of office: until when.
  • email string | null — Their address.
  • email_status string | null — Its verification.
  • fields object — Your own fields.
  • campaign_ids string[] — Its campaigns.
  • stage string | null — Its stage.
  • owner_id string | null — The user id of its owner.
  • owner object | null — With owner or stage in the body: the owner (id, name, email).
  • signal_lead_id string — On a lead from the signals: its number on Leads.
  • note string | null — Your note on the row.
  • changed boolean · required — Something changed.
  • replaced object — When this call set another next action (or null): the one it replaced, { next_action: { at, kind, note, email } }, with the full text of its planned email, so you can put it back.
  • added_to_table boolean · required — The row was not in the table before this call (a company by its domain or page).
  • stopped_linkedin_sequences integer — With lead_status not_interested: the LinkedIn sequences that stopped.
  • dashboard_url string · required — The table in mySDR.

Credits

Free. This call never uses credits.

Errors

  • 400 invalid_request — An unknown field (param names it; contact_points belong to a company), nothing to change, or a value that is not valid.
  • 404 not_found — No person in your Leads table has this id, profile or address.
  • 409 invalid_state — A lead from a signal without a LinkedIn profile, with a field other than status and lead_status.
  • 409 invalid_state — The key has no user behind it, so mySDR cannot record who did it.
  • 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 PATCH "https://mysdr.app/api/v1/tables/leads/sav_7c1f2a40-5b0e-4c3e-9a71-2f7d3e0c9b11" \
  -H "Authorization: Bearer $MYSDR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "needs_decision",
    "status_reason": "Replied positively, wants a demo next week",
    "lead_status": "interested",
    "next_action": {
      "at": "2026-10-27",
      "kind": "approval",
      "note": "Propose two demo slots"
    }
  }'

JavaScript

const response = await fetch('https://mysdr.app/api/v1/tables/leads/sav_7c1f2a40-5b0e-4c3e-9a71-2f7d3e0c9b11', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.MYSDR_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "status": "needs_decision",
    "status_reason": "Replied positively, wants a demo next week",
    "lead_status": "interested",
    "next_action": {
      "at": "2026-10-27",
      "kind": "approval",
      "note": "Propose two demo slots"
    }
  })
});
const body = await response.json();

Python

import json
import os
import urllib.request

payload = {
  "status": "needs_decision",
  "status_reason": "Replied positively, wants a demo next week",
  "lead_status": "interested",
  "next_action": {
    "at": "2026-10-27",
    "kind": "approval",
    "note": "Propose two demo slots"
  }
}

request = urllib.request.Request(
    "https://mysdr.app/api/v1/tables/leads/sav_7c1f2a40-5b0e-4c3e-9a71-2f7d3e0c9b11",
    headers={
    "Authorization": f"Bearer {os.environ['MYSDR_API_KEY']}",
    "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode(),
    method="PATCH",
)
with urllib.request.urlopen(request) as response:
    body = json.load(response)

Example response

{
  "id": "sav_7c1f2a40-5b0e-4c3e-9a71-2f7d3e0c9b11",
  "object": "person",
  "name": "Jane Example",
  "company": "Acme Logistics",
  "linkedin_url": null,
  "status": "needs_decision",
  "status_reason": "Replied positively, wants a demo next week",
  "snooze_until": null,
  "lead_status": "interested",
  "next_action": {
    "at": "2026-10-27T08:00:00.000Z",
    "kind": "approval",
    "note": "Propose two demo slots"
  },
  "out_of_office_until": null,
  "email": "jane@acme-logistics.example",
  "email_status": "verified",
  "fields": {},
  "campaign_ids": [],
  "note": null,
  "changed": true,
  "added_to_table": false,
  "dashboard_url": "https://mysdr.app/en/gtm/leads"
}