# Update a lead

`PATCH /v1/leads/{lead_id}`

Merges more details into a lead, typically from later steps of a form.

**Scope:** `leads:write`

**Access:** The key's member must be allowed to manage leads in the lead's campaign.

Takes the same optional details as `POST /v1/leads`, with the same limits and merge rules, plus `deal_value`. Custom field slugs are checked against the lead's campaign.

`campaign_id` and `phone` cannot be sent: a lead's phone number is its identity, so to capture a different number `POST` a new lead.

`PATCH` never creates or moves the lead's pipeline card and never changes a time zone the lead already has. `custom_fields` and `deal_value` are saved on the card the lead already has; if it has none, a `PATCH` carrying either fails with `409 no_pipeline_card` and writes nothing. For a member who may open only a follow-through card of the lead, they are saved on the deal that card shares with the main card, and `pipeline_lead_id` is the follow-through card.

The lead must belong to a campaign the key may write to and be one the key's member can see in DialEcho, otherwise `404`, checked before the member's permission to manage leads.

## Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `lead_id` | `UUID` | Yes | The lead, as `POST /v1/leads` returned it. |

## Body

**Content type:** `application/json`

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `first_name` | `string` | No | First name. Up to 100 characters. |
| `last_name` | `string` | No | Last name. Up to 100 characters. |
| `email` | `string` | No | An email address that can receive mail. Anything else, including an address over 254 characters, is rejected with field code `email_invalid`. Up to 254 characters. |
| `address` | `string` | No | Street address. Up to 500 characters. |
| `city` | `string` | No | City. Up to 100 characters. |
| `state` | `string` | No | A US state name or 2-letter code; sets the lead's time zone when it has none. Up to 100 characters. |
| `postal_code` | `string` | No | A US ZIP sets the lead's state (and its time zone, if it has none) when neither `state` nor `address` names one and none is stored. Up to 20 characters. |
| `company` | `string` | No | Company name. Up to 200 characters. |
| `notes` | `string` | No | Free-text notes on the lead. Up to 5,000 characters. |
| `custom_fields` | `object` | No | Values for the campaign's own custom fields, keyed by field slug. Each value is a string, number, boolean or `null`. Saved on the lead's pipeline card as text, exactly as if your team typed it: `true`/`false` for checkbox fields, numbers as sent (`12` becomes `"12"`), strings trimmed. A slug that is not one of the campaign's fields, or is a file or image field, is reported as `custom_fields.<slug>` with `not_allowed` (upload files with `POST /v1/leads/{lead_id}/files`); a bad value (another type, or a string too long) is reported once, as `custom_fields`. Up to 100 entries. Keys 1 to 100 characters; string values up to 2,000 characters. |
| `attribution` | `object` | No | Marketing attribution, stored on the lead under the same key names and returned by the conversion report for ad-platform matching. Any key not listed here is rejected with `not_allowed`. |
| `attribution.utm_source` | `string` | No | UTM source of the visit. Up to 500 characters. |
| `attribution.utm_medium` | `string` | No | UTM medium. Up to 500 characters. |
| `attribution.utm_campaign` | `string` | No | UTM campaign. Up to 500 characters. |
| `attribution.utm_term` | `string` | No | UTM term. Up to 500 characters. |
| `attribution.utm_content` | `string` | No | UTM content. Up to 500 characters. |
| `attribution.utm_id` | `string` | No | UTM id, for example the Meta campaign id. Up to 500 characters. |
| `attribution.adset_id` | `string` | No | Ad set id of the ad the visitor came from. Up to 500 characters. |
| `attribution.ad_id` | `string` | No | Ad id of the ad the visitor came from. Up to 500 characters. |
| `attribution.fbclid` | `string` | No | Meta's click id from the landing page URL. Up to 500 characters. |
| `attribution.fbp` | `string` | No | The visitor's Meta `_fbp` cookie. Up to 500 characters. |
| `attribution.fbc` | `string` | No | The visitor's Meta `_fbc` cookie. Up to 500 characters. |
| `attribution.gclid` | `string` | No | Google's click id. Up to 500 characters. |
| `attribution.meta_event_id` | `string` | No | The event ID your Meta Pixel's browser `Lead` event used for this submission, so a `Lead` sent from the conversion report deduplicates against it. Up to 200 characters. |
| `attribution.page_url` | `string` | No | The page the form was submitted on. Up to 2,048 characters. |
| `attribution.form_name` | `string` | No | Your name for the form or form step. Up to 200 characters. |
| `attribution.referrer` | `string` | No | The referring page. Up to 2,048 characters. |
| `deal_value` | `number` | No | The deal value of the lead's pipeline card, as your team sees it on the card. `0` is a value and is saved; `null` or leaving it out keeps the stored value. The number is checked exactly as written in your JSON, never rounded first, so `100000000.000000001` is rejected and `12.500` is saved as `12.5`. Read it back as `card.deal_value` from `GET /v1/leads/{lead_id}`. 0 to 100,000,000 with at most 2 decimal places. Strings and booleans are rejected. |

## Response

**Status:** `200 OK`

Identifiers only.

| Name | Type | Description |
| --- | --- | --- |
| `lead_id` | `UUID` | The lead. |
| `contact_id` | `UUID \| null` | The CRM contact the lead is linked to. |
| `pipeline_lead_id` | `UUID \| null` | The lead's card on the campaign's board, the card `GET /v1/leads/{lead_id}` returns as `card` (for a member who may open only a follow-through card of the lead, that card). `null` only when the lead could not be placed (for example the first stage only accepts cards placed by a person); sending the same `POST` again retries the placement. |

## Example request

**curl**

```bash
curl -X PATCH "https://development.agencydevworks.ai/v1/leads/aaaaaaaa-0000-4000-8000-000000000001" \
  -H "Authorization: Bearer dei_your_key" \
  -H "Content-Type: application/json" \
  -d '{
  "last_name": "Lee",
  "address": "1 Market St",
  "city": "San Francisco",
  "postal_code": "94105",
  "custom_fields": {
    "roof_age": 12
  },
  "attribution": {
    "form_name": "quote-step-2"
  },
  "deal_value": 12500.5
}'
```

**JavaScript**

```js
const response = await fetch("https://development.agencydevworks.ai/v1/leads/aaaaaaaa-0000-4000-8000-000000000001", {
  method: "PATCH",
  headers: {
    "Authorization": "Bearer dei_your_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "last_name": "Lee",
    "address": "1 Market St",
    "city": "San Francisco",
    "postal_code": "94105",
    "custom_fields": {
      "roof_age": 12
    },
    "attribution": {
      "form_name": "quote-step-2"
    },
    "deal_value": 12500.5
  }),
});
const data = await response.json();
```

## Example response

**Status:** `200 OK`

```json
{
  "lead_id": "aaaaaaaa-0000-4000-8000-000000000001",
  "contact_id": "bbbbbbbb-0000-4000-8000-000000000001",
  "pipeline_lead_id": "dddddddd-0000-4000-8000-000000000001"
}
```

## Errors

| HTTP | Code | When |
| --- | --- | --- |
| 403 | `insufficient_scope` | The key does not have `leads:write`. Checked before the body is validated. |
| 403 | `permission_denied` | The key's member may not manage leads in the lead's campaign. Only for a lead they can see; any other lead is `404`. |
| 404 | `not_found` | The lead does not exist or the key cannot reach it (other organization, a campaign outside the key, or a lead its member cannot see). |
| 409 | `no_pipeline_card` | `custom_fields` or `deal_value` were sent but the lead has no pipeline card (or it was deleted while the request ran). Nothing was written. |
| 413 | `payload_too_large` | The body is larger than 1 MB. It was not read and nothing was written. |
| 422 | `invalid_request` | `lead_id` is not a UUID, or the body is invalid: `campaign_id` or `phone` sent, unknown fields, values too long, an invalid email, unknown or file/image custom fields, or a `deal_value` outside its rules. `error.fields` lists every problem. |

Every endpoint can also answer `401 invalid_api_key`, `429 rate_limited` and `503 dependency_unavailable`. See [Errors](/api/#errors) in the overview.

## Related endpoints

- `POST /v1/leads` · [Create or update a lead](/api/leads/create)
- `GET /v1/leads/{lead_id}` · [Retrieve a lead](/api/leads/get)
