# 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.

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

```bash
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

```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

```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

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