Skip to content

API · Leads — find and edit your leads from another tool

The leads endpoints let you find a lead, read their record and modify it from outside ZeroChats: move them to a new stage when they book in your calendar, tag them when they pay through your gateway, or store the email they left on a form. They are the natural complement to the Custom Webhook: the webhook tells you what happens inside ZeroChats, and with these endpoints you reflect in ZeroChats what happens outside.

This page covers only those endpoints. How to get the key, how to authenticate, the rate limit and the errors shared by the whole API live in ZeroChats API.

The base URL is:

https://app.zerochats.com/api/v1
MethodEndpointWhat it does
GET/leadsFinds leads by username, email or phone.
GET/leads/{id}Returns one lead by its identifier.
GET/leads/resolveReturns the lead a link’s utm_user points to.
PATCH/leads/{id}Changes a lead’s stage, tags or contact details.

The API creates no leads and sends no messages. A lead is born when someone writes to your business; from here you only find and edit them.

All four endpoints return the lead in the same shape, and it is exactly the same as the lead object the Custom Webhook sends. Whatever you learn to read in one place works in the other, and the id you get from the webhook is the one you use here.

{
"id": "3d51f0c2-9a18-4b7e-8c56-1e2d3f4a5b6c",
"name": "Ada Lovelace",
"username": "ada",
"platform": "INSTAGRAM",
"externalId": "17841400000000000",
"email": "[email protected]",
"phone": null,
"state": "IN_PROGRESS",
"tags": ["hot"],
"createdAt": "2026-08-11T09:12:30.000Z"
}
FieldWhat it is
idThe lead’s identifier in ZeroChats. Stable — use it as your key.
nameThe lead’s name.
usernameTheir username on the source platform.
platformSource channel: INSTAGRAM, WHATSAPP, WASENDERAPI or MESSENGER.
externalIdThe lead’s identifier on their platform: the Instagram-scoped id, or the phone digits on the WhatsApp channels. null when unknown.
emailThe lead’s email, or null if they haven’t given it yet.
phoneThe lead’s phone number, or null if they haven’t given it yet.
stateStage in the lead funnel, by name. The mapping table is further down.
tagsThe lead’s full list of tags at that moment, by name.
createdAtWhen the lead was created, in ISO 8601 format (UTC).

On these endpoints the response carries no meta: only success and data.

GET /leads searches by exactly one criterion, sent as a URL parameter:

ParameterWhat it matches
usernameThe Instagram or WhatsApp username, exact match, case-insensitive.
emailThe email collected in the conversation, exact match, case-insensitive.
phoneThe lead’s WhatsApp number and the phone collected in the conversation. Any formatting works — +34 600 111 222, 0034600111222, 34600111222 — it is reduced to digits and compared. Send it with the country code.
Ventana de terminal
curl -H "Authorization: Bearer zcst_your_key" \
"https://app.zerochats.com/api/v1/[email protected]"

data is always a list, newest first, with at most 20 results:

{
"success": true,
"data": [
{
"id": "3d51f0c2-9a18-4b7e-8c56-1e2d3f4a5b6c",
"name": "Ada Lovelace",
"username": "ada",
"platform": "INSTAGRAM",
"externalId": "17841400000000000",
"email": "[email protected]",
"phone": null,
"state": "IN_PROGRESS",
"tags": ["hot"],
"createdAt": "2026-08-11T09:12:30.000Z"
}
]
}

It’s a list rather than a single lead because the same person can be two leads: one per channel they wrote to you on. If Ada messaged you on Instagram and later on WhatsApp and left the same email in both conversations, the email lookup returns both records. When nobody matches you get a 200 with data set to [], not a 404.

Sending two criteria at once, or none, is a 400 with error.code set to invalid_request and the message provide exactly one of username, email or phone. A phone with no digits is also a 400.

GET /leads/{id} returns one lead by its ZeroChats identifier: the id the webhook or the lookup gives you.

Ventana de terminal
curl -H "Authorization: Bearer zcst_your_key" \
https://app.zerochats.com/api/v1/leads/3d51f0c2-9a18-4b7e-8c56-1e2d3f4a5b6c

If the lead doesn’t exist or doesn’t belong to your business, the response is a 404 with error.code set to not_found. Both cases look identical on purpose: the key can’t even confirm that someone else’s lead exists.

PATCH /leads/{id} takes a JSON body with the fields you want to change. Only the ones you send are written; everything else stays as it is.

Ventana de terminal
curl -X PATCH \
-H "Authorization: Bearer zcst_your_key" \
-H "Content-Type: application/json" \
-d '{ "state": "BOOKED", "addTags": ["hot"] }' \
https://app.zerochats.com/api/v1/leads/3d51f0c2-9a18-4b7e-8c56-1e2d3f4a5b6c

The body accepts these fields, and only these:

{
"state": "BOOKED",
"addTags": ["hot", "9f1c8a4e-2b77-4d0a-9d1c-3f2b6a5c7e10"],
"removeTags": ["cold"],
"email": "[email protected]",
"phone": "+34 600 111 222"
}

The response is the lead as it is after the change, in the same shape as GET /leads/{id}.

The funnel stage, by name:

Stage in the appstate
OpenOPEN_CONVERSATION
DiscoveringIN_PROGRESS
QualifyingQUALIFYING
ProposalLEAD
BookedBOOKED
PaidCLIENT
PostponedPOSTPONED
UnqualifiedNO_QUALIFY
LostLOST

Changing the stage through the API behaves exactly like changing it by hand in the dashboard:

  • It is recorded in the lead’s state history, with the reason “The state was changed through the ZeroChats API”.
  • Follow-Ups react to the new stage as they would to any other change.
  • Connected CRMs receive their status tag, and the Custom Webhook receives lead.state_changed.

Sending the stage the lead already has does nothing: no change is recorded and nobody is notified.

Lists of tags to add or remove, up to 50 in each. Every entry can be the tag’s id or its name, case-insensitive.

The tags must already exist in your business: the API never creates tags. If any reference isn’t recognised, the whole request fails with a 400 that names the unknown ones, and nothing is changed — not even the ones that did exist. Create the tag from the app and repeat the call.

Tags you add behave like the ones the AI assigns: if the tag has its notification enabled, it is sent; connected CRMs and the Custom Webhook receive lead.tagged. Removing a tag is silent. If you send both fields, additions are applied first and removals afterwards.

A string to write the value, or null to clear it. An email that isn’t shaped like an email is a 400.

Statuserror.codeWhen
404not_foundThe lead doesn’t exist or doesn’t belong to your business.
400invalid_requestEmpty body, malformed JSON, a field that doesn’t exist, a stage that isn’t in the table, an unknown tag or an invalid email.

While the Conversion tracking script is active, every link ZeroChats sends to a lead carries a utm_user parameter with an encrypted identifier for the lead: an opaque 22-character value only ZeroChats can decrypt. GET /leads/resolve turns that value back into the lead, so you can recognise them from your own server without installing the script.

Ventana de terminal
curl -H "Authorization: Bearer zcst_your_key" \
"https://app.zerochats.com/api/v1/leads/resolve?utm_user=qdfomARk3BiETYsZgmtDbg"

data is the lead, in the same shape as GET /leads/{id}:

{
"success": true,
"data": {
"id": "3d51f0c2-9a18-4b7e-8c56-1e2d3f4a5b6c",
"name": "Ada Lovelace",
"username": "ada",
"platform": "INSTAGRAM",
"externalId": "17841400000000000",
"email": "[email protected]",
"phone": null,
"state": "IN_PROGRESS",
"tags": ["hot"],
"createdAt": "2026-08-11T09:12:30.000Z"
}
}

A value with a leading @ — utm_user=@qdfomARk3BiETYsZgmtDbg — is accepted as well. And deactivating conversion tracking invalidates nothing: values from links sent earlier keep resolving.

Statuserror.codeWhen
400invalid_requestutm_user is missing, or the value is not one ZeroChats produces. The message is utm_user: not a value produced by ZeroChats.
404not_foundConversion tracking has never been activated for your business — the message says so — or the value doesn’t match any lead of yours: it comes from another business’s links or has been tampered with. That last case looks like an unknown lead on purpose.

The full flow, without the script:

  1. Capture the utm_user on your page. Read it from the URL the visitor lands on and store it in a hidden form field, or pass it to your booking or payment tool as extra data.

  2. Send it to your server. With the form submission, or inside the notification your tool fires when someone books or pays.

  3. Resolve the lead. From the server, GET /leads/resolve?utm_user=... with your key. If you get a 404, the visitor didn’t arrive from a ZeroChats link; log the case and end the flow.

  4. Edit the lead. PATCH /leads/{id} with the id you just received: the stage, the tags, the email or the phone you want to store.

Worked example: move the lead when they book

Section titled “Worked example: move the lead when they book”

A complete case in n8n or Make: your booking tool — or your payment gateway — fires a webhook every time someone books, and you want that lead moved to Booked in ZeroChats with the booked tag added, without anyone opening the dashboard.

  1. Receive the notification from your tool. Almost all of them include the email the person booked with; that’s what you’ll use to find them.

  2. Find the lead by email. An HTTP Request node with method GET and URL:

    https://app.zerochats.com/api/v1/leads?email={{ $json.email }}

    With the same header credential you use for the rest of the API.

  3. Check there is a result. If data is empty, the person booked with an email they never left in the conversation; log the case and end the flow. If there are several, keep the first one: it’s the most recent.

  4. Edit the lead. Another HTTP Request, this time PATCH, against https://app.zerochats.com/api/v1/leads/{{ $json.data[0].id }}, with a Content-Type: application/json header and this body:

    { "state": "BOOKED", "addTags": ["booked"] }
  5. Done. The lead shows up in Booked, tagged, with the change in their history. If you have a CRM connected or the Custom Webhook enabled, they receive the change exactly as if you had made it by hand.

For a payment gateway the flow is the same, swapping BOOKED for CLIENT. And if your tool gives you a phone number instead of an email, search with phone instead of email.

  • Store the id, not the username. An Instagram username can change; the id can’t. If your flow receives the lead through the webhook, you already have the id and can skip the lookup.
  • Treat the lookup result as a list. It will almost always carry one result, but decide what to do with zero or several before it happens in production.
  • Send only what changes. A PATCH with the stage the lead already has breaks nothing, but adds nothing either; a PATCH with the current email rewrites the same value.
  • Leave the stage to the AI unless you know better. The API is for reflecting facts that happen outside — a booking, a payment — not for moving leads around routinely from a flow.

Either the id belongs to another business — a key only sees its own leads — or the lead was deleted from the app. In both cases the response is the same on purpose. Check that you’re using the key of the right business.

The lookup returns [] and I know the lead exists

Section titled “The lookup returns [] and I know the lead exists”

Check the criterion:

  • With username, the username must be exact: no @, no spaces and spelled as it is on the platform.
  • With email, it must be the email as the lead typed it in the conversation. If they gave it with a typo, the lookup needs the same typo.
  • With phone, include the country code. 600111222 doesn’t find a lead stored as 34600111222.

The message tells you which references weren’t recognised. The API doesn’t create tags: create it first from the app, with the same name you use in the flow, and run the call again. Until then nothing changes on the lead, not even the other fields of the same PATCH.