Create or update a lead
POST /
Creates a lead in a campaign, or merges into the campaign's existing lead with the same phone number.
Scope: leads:write
Access: The key's member must be allowed to manage leads in the campaign.
Phone-first upsert. Within a campaign the phone number, normalized to E.164, identifies the lead: posting a number the campaign already has never creates a duplicate. The new values merge into that lead and the answer is 200 with created: false; a new lead is 201 with created: true. Merging needs a lead the key's member can see in DialEcho, otherwise 404 and nothing is written.
The lead is linked to a CRM contact (created from the phone number when the organization has none yet). If it is not on the campaign's board yet, its card is placed in the first stage of the campaign's default pipeline (created with default stages if the campaign has none) and routed to a rep by the campaign's assignment rules. A lead that already has a card is never moved by POST or PATCH; your team's placement wins. Once the lead has a card, the campaign's automated outreach for it stops.
custom_fields are saved on that pipeline card, in its Custom Fields panel. If the lead has no card and none can be placed, a request with non-blank custom_fields fails with 409 no_pipeline_card; the lead and its other details are still saved, so fix the campaign's pipeline and send the same request again. For a member who may open only a follow-through card of the lead, pipeline_lead_id is that card and the custom_fields are saved on the deal it shares with the main card.
The lead's time zone is set when it is created: from its state, else a state found in the address, else the ZIP in postal_code, else the phone's area code. A later POST or PATCH can fill a missing state or time zone but never changes one the lead already has.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | No | A retry with the same key and body returns the first answer (201 stays 201) and writes nothing. It is 404 not_found once the lead is gone or the key's member can no longer see it, as for a new request; if they can no longer open the card the first answer named, pipeline_lead_id is the card a new request would name. Reusing the key with a different body is 409 idempotency_conflict. Even without a key, a retry merges into the same lead by phone. 16 to 128 printable ASCII characters, no spaces (a UUID works). |
Body
Content type: application/json
| Name | Type | Required | Description |
|---|---|---|---|
campaign_id | UUID | Yes | A campaign the key may write to. |
phone | string | Yes | The lead's phone number, normalized to E.164: (415) 555-0132, 415.555.0132 and +1 415 555 0132 all become +14155550132. Digits and a leading + are kept and everything else is dropped. 10 digits, or 11 starting with 1, are read as US/Canada; any other number must carry its country code. Do not send extensions: their digits become part of the number. A number of impossible length is rejected with field code phone_invalid. Up to 40 characters. |
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. | string | No | UTM source of the visit. Up to 500 characters. |
attribution. | string | No | UTM medium. Up to 500 characters. |
attribution. | string | No | UTM campaign. Up to 500 characters. |
attribution. | string | No | UTM term. Up to 500 characters. |
attribution. | string | No | UTM content. Up to 500 characters. |
attribution. | string | No | UTM id, for example the Meta campaign id. Up to 500 characters. |
attribution. | string | No | Ad set id of the ad the visitor came from. Up to 500 characters. |
attribution. | string | No | Ad id of the ad the visitor came from. Up to 500 characters. |
attribution. | string | No | Meta's click id from the landing page URL. Up to 500 characters. |
attribution. | string | No | The visitor's Meta _fbp cookie. Up to 500 characters. |
attribution. | string | No | The visitor's Meta _fbc cookie. Up to 500 characters. |
attribution. | string | No | Google's click id. Up to 500 characters. |
attribution. | 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. | string | No | The page the form was submitted on. Up to 2,048 characters. |
attribution. | string | No | Your name for the form or form step. Up to 200 characters. |
attribution. | string | No | The referring page. Up to 2,048 characters. |
Response
Status: 201 Created · 200 OK
201 for a new lead; 200 when the campaign already had this phone number and the values were merged. The identifiers are the same on every resubmission.
| 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. |
created | boolean | true when this request created the lead. |
Example request
curl
bash
curl -X POST "https://development.agencydevworks.ai/v1/leads" \
-H "Authorization: Bearer dei_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0f0f0f0f-0000-4000-8000-00000000abcd" \
-d '{
"campaign_id": "cccccccc-0000-4000-8000-000000000001",
"phone": "(415) 555-0132",
"first_name": "Pat",
"email": "pat@example.com",
"state": "CA",
"custom_fields": {
"roof_age": 12
},
"attribution": {
"utm_source": "facebook",
"utm_medium": "paid_social",
"utm_campaign": "fall-promo",
"fbclid": "IwAR0exampleClickId",
"fbp": "fb.1.1791900000000.1234567890",
"page_url": "https://www.example.com/get-a-quote",
"form_name": "quote-step-1"
}
}'JavaScript
js
const response = await fetch("https://development.agencydevworks.ai/v1/leads", {
method: "POST",
headers: {
"Authorization": "Bearer dei_your_key",
"Content-Type": "application/json",
"Idempotency-Key": "0f0f0f0f-0000-4000-8000-00000000abcd",
},
body: JSON.stringify({
"campaign_id": "cccccccc-0000-4000-8000-000000000001",
"phone": "(415) 555-0132",
"first_name": "Pat",
"email": "pat@example.com",
"state": "CA",
"custom_fields": {
"roof_age": 12
},
"attribution": {
"utm_source": "facebook",
"utm_medium": "paid_social",
"utm_campaign": "fall-promo",
"fbclid": "IwAR0exampleClickId",
"fbp": "fb.1.1791900000000.1234567890",
"page_url": "https://www.example.com/get-a-quote",
"form_name": "quote-step-1"
}
}),
});
const data = await response.json();Example response
Status: 201 Created
json
{
"lead_id": "aaaaaaaa-0000-4000-8000-000000000001",
"contact_id": "bbbbbbbb-0000-4000-8000-000000000001",
"pipeline_lead_id": "dddddddd-0000-4000-8000-000000000001",
"created": true
}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 this campaign. |
| 404 | not_found | The campaign does not exist or the key cannot reach it, or the phone matches a lead the key's member cannot see. |
| 409 | idempotency_conflict | The Idempotency-Key was already used with a different body. Nothing was written. |
| 409 | no_pipeline_card | Non-blank custom_fields were sent but the lead has no card and none could be placed. The lead and its other details were saved; the custom fields were not. |
| 413 | payload_too_large | The body is larger than 1 MB. It was not read and nothing was written. |
| 422 | invalid_request | The body or a header is invalid: unknown or missing fields, values too long, an invalid phone or email, unknown or file/image custom fields, a deal_value, or a malformed Idempotency-Key. error.fields lists every problem. |
Every endpoint can also answer 401 invalid_api_key, 429 rate_limited and 503 dependency_unavailable. See Errors in the overview.
Related endpoints
- PATCH
/· Update a leadv1/ leads/ {lead_id} - GET
/· Retrieve a leadv1/ leads/ {lead_id} - PUT
/· Move a lead's cardv1/ leads/ {lead_id}/ stage - POST
/· Upload a filev1/ leads/ {lead_id}/ files - POST
/· Place an AI callv1/ calls