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| Method | Endpoint | What it does |
|---|---|---|
GET | /leads | Finds leads by username, email or phone. |
GET | /leads/{id} | Returns one lead by its identifier. |
GET | /leads/resolve | Returns 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.
The lead
Section titled “The lead”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", "phone": null, "state": "IN_PROGRESS", "tags": ["hot"], "createdAt": "2026-08-11T09:12:30.000Z"}| Field | What it is |
|---|---|
id | The lead’s identifier in ZeroChats. Stable — use it as your key. |
name | The lead’s name. |
username | Their username on the source platform. |
platform | Source channel: INSTAGRAM, WHATSAPP, WASENDERAPI or MESSENGER. |
externalId | The lead’s identifier on their platform: the Instagram-scoped id, or the phone digits on the WhatsApp channels. null when unknown. |
email | The lead’s email, or null if they haven’t given it yet. |
phone | The lead’s phone number, or null if they haven’t given it yet. |
state | Stage in the lead funnel, by name. The mapping table is further down. |
tags | The lead’s full list of tags at that moment, by name. |
createdAt | When the lead was created, in ISO 8601 format (UTC). |
On these endpoints the response carries no meta: only success and data.
Finding a lead
Section titled “Finding a lead”GET /leads searches by exactly one criterion, sent as a URL parameter:
| Parameter | What it matches |
|---|---|
username | The Instagram or WhatsApp username, exact match, case-insensitive. |
email | The email collected in the conversation, exact match, case-insensitive. |
phone | The 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. |
curl -H "Authorization: Bearer zcst_your_key" \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", "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.
Reading a lead
Section titled “Reading a lead”GET /leads/{id} returns one lead by its ZeroChats identifier: the id the webhook or the lookup gives you.
curl -H "Authorization: Bearer zcst_your_key" \ https://app.zerochats.com/api/v1/leads/3d51f0c2-9a18-4b7e-8c56-1e2d3f4a5b6cIf 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.
Editing a lead
Section titled “Editing a lead”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.
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-1e2d3f4a5b6cThe body accepts these fields, and only these:
{ "state": "BOOKED", "addTags": ["hot", "9f1c8a4e-2b77-4d0a-9d1c-3f2b6a5c7e10"], "removeTags": ["cold"], "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 app | state |
|---|---|
| Open | OPEN_CONVERSATION |
| Discovering | IN_PROGRESS |
| Qualifying | QUALIFYING |
| Proposal | LEAD |
| Booked | BOOKED |
| Paid | CLIENT |
| Postponed | POSTPONED |
| Unqualified | NO_QUALIFY |
| Lost | LOST |
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.
addTags and removeTags
Section titled “addTags and removeTags”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.
email and phone
Section titled “email and phone”A string to write the value, or null to clear it. An email that isn’t shaped like an email is a 400.
Errors when editing
Section titled “Errors when editing”| Status | error.code | When |
|---|---|---|
| 404 | not_found | The lead doesn’t exist or doesn’t belong to your business. |
| 400 | invalid_request | Empty body, malformed JSON, a field that doesn’t exist, a stage that isn’t in the table, an unknown tag or an invalid email. |
Resolve a link’s utm_user
Section titled “Resolve a link’s utm_user”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.
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", "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.
Errors when resolving
Section titled “Errors when resolving”| Status | error.code | When |
|---|---|---|
| 400 | invalid_request | utm_user is missing, or the value is not one ZeroChats produces. The message is utm_user: not a value produced by ZeroChats. |
| 404 | not_found | Conversion 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. |
Custom integration
Section titled “Custom integration”The full flow, without the script:
-
Capture the
utm_useron 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. -
Send it to your server. With the form submission, or inside the notification your tool fires when someone books or pays.
-
Resolve the lead. From the server,
GET /leads/resolve?utm_user=...with your key. If you get a404, the visitor didn’t arrive from a ZeroChats link; log the case and end the flow. -
Edit the lead.
PATCH /leads/{id}with theidyou 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.
-
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.
-
Find the lead by email. An HTTP Request node with method
GETand URL:https://app.zerochats.com/api/v1/leads?email={{ $json.email }}With the same header credential you use for the rest of the API.
-
Check there is a result. If
datais 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. -
Edit the lead. Another HTTP Request, this time
PATCH, againsthttps://app.zerochats.com/api/v1/leads/{{ $json.data[0].id }}, with aContent-Type: application/jsonheader and this body:{ "state": "BOOKED", "addTags": ["booked"] } -
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.
Best practices
Section titled “Best practices”- Store the
id, not the username. An Instagram username can change; theidcan’t. If your flow receives the lead through the webhook, you already have theidand 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
PATCHwith the stage the lead already has breaks nothing, but adds nothing either; aPATCHwith 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.
Common problems
Section titled “Common problems”I get a 404 with an id I have stored
Section titled “I get a 404 with an id I have stored”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.600111222doesn’t find a lead stored as34600111222.
I get a 400 when adding a tag
Section titled “I get a 400 when adding a tag”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.