Reading and Polling Leads
GET /v1/leads lists your company's leads with their custom fields. Use it to read back leads you sent in or that arrived through your web forms and channels. It also keeps another system in sync by polling, which is useful when webhooks can't reach that system (for example, an internal app behind a login).
List leads
curl "https://api.abcsalesbot.com/v1/leads?limit=100" \
-H "Authorization: Bearer $ABC_API_KEY"
{
"data": [
{
"id": 123456,
"external_id": "crm-000123",
"name": "Ali Hassan",
"phone": "+60123456789",
"email": "ali@example.com",
"source": "Challenge sign-up form",
"tags": ["week-1"],
"automation_ids": [10657],
"ai_enabled": true,
"scheduled_messages_enabled": true,
"from_ad": false,
"assigned_to": [{ "id": 42, "full_name": "Jane Tan" }],
"total_messages": 12,
"custom_fields": {
"team": "Blue",
"proof": {
"type": "file",
"attachment_id": 9876,
"file_name": "receipt.jpg",
"content_type": "image/jpeg",
"download_url": "https://api.abcsalesbot.com/v1/attachments/9876/download"
}
},
"created_at": "2026-10-18T09:15:00+08:00",
"updated_at": "2026-10-18T01:20:31.123456Z"
}
],
"meta": {
"total_results": 230,
"has_more": true,
"next_cursor": "eyJwZXJfcGFnZSI6MTAwfQ",
"timezone": "Asia/Kuala_Lumpur"
}
}
Leads are ordered by updated_at, oldest change first; leads changed at the same instant are ordered by id. Deleted leads are not returned.
created_atis in the timezone shown inmeta.timezone: thetimezoneyou asked for, or else your company's timezone, or else UTC.updated_atis always UTC, with microseconds.
Filters
All filters are optional query parameters and can be combined.
| Parameter | Description |
|---|---|
updated_since | Only leads changed strictly after this RFC 3339 timestamp. See Polling for changes. |
automation_id | Only leads subscribed to this automation. Pass its numeric id from GET /v1/automations, the same ID a lead lists in automation_ids. Not the slug_id. |
tags | Only leads with any of these tags. Repeat the parameter (tags=vip&tags=hot) or separate with commas. |
date_from, date_to | Only leads created on or after / on or before this day (YYYY-MM-DD), measured in timezone. |
timezone | IANA timezone such as Asia/Kuala_Lumpur, for the date filters and created_at. |
search | Partial match on name, phone or email. |
source | Only leads with this source. |
ai_enabled | true or false: only leads with AI replies on or off. |
from_ad | true or false: only leads that did or did not come from a click-to-WhatsApp ad. |
messaging_channel | Only leads on this channel type: meta_whatsapp, twilio_whatsapp, twilio_sms, telegram, naver_line, facebook_messenger, instagram. |
messaging_channel_connection_ids | Only leads on these connections, from GET /v1/messaging-channel-connections. Repeat or comma-separate. |
limit | Page size, 1 to 100. Default 50. |
cursor | meta.next_cursor from the previous page. |
A malformed value (an unknown channel, a date that isn't YYYY-MM-DD, an invalid timezone) returns 400 with a message naming the parameter.
Paging
Follow meta.next_cursor until meta.has_more is false, as described in Pagination. The cursor carries every filter from the first page, so a request with cursor needs no other parameters.
Polling for changes
To keep another system in sync, poll on a schedule, for example once a minute:
- Call
GET /v1/leads?updated_since=<last>&limit=100. On the very first run, leave outupdated_sinceto read every lead. - Process each lead, upserting by
id. - Follow
meta.next_cursoruntilmeta.has_moreisfalse. - Store the
updated_atof the last lead you processed, exactly as returned, and use it as<last>next time. If a poll returns no leads, keep the value you had.
curl -G "https://api.abcsalesbot.com/v1/leads" \
-H "Authorization: Bearer $ABC_API_KEY" \
--data-urlencode "updated_since=2026-10-18T01:20:31.123456Z" \
--data-urlencode "limit=100"
What this guarantees:
- No lead is missed or repeated.
updated_sinceis exclusive, so the lead you stored isn't returned again. A lead that changes while you are paging comes back on a later poll with its newupdated_at, and isn't returned twice by the walk in progress. - Updates are picked up. Anything that changes a lead's details or custom fields moves its
updated_at. That includes someone submitting your web form again, which updates their existing lead instead of creating a new one. - Tag and assignment updates are picked up too. Adding or removing a tag, or assigning or unassigning a user, advances the lead's
updated_at. These changes can appear in a subsequentupdated_sincepoll, including when no other lead field changed. The same 30-second holdback applies. - Recent writes are held back for 30 seconds. A lead written in the last 30 seconds is returned by a later call, not this one. This lets every write finish before you pass its timestamp. A once-a-minute poll sees each change within about 90 seconds.
Persist updated_at, not next_cursor. A cursor is only for finishing the walk you started (see Pagination). Pass updated_since exactly as returned, including the microseconds. If you build the URL yourself, URL-encode it: a + in an offset must be sent as %2B.
- Deletions. A lead deleted after you synced it just stops appearing; there is no deletion record.
- Conversation activity. A message sent to the lead never moves
updated_at. A message from the lead usually does, but not reliably, so do not useupdated_sinceto track new messages.
tags, assigned_to and total_messages are current as of the call, for every lead you do receive.
File custom fields
A file custom field is returned as an object, not the stored path:
| Field | Description |
|---|---|
type | Always file. |
attachment_id | The attachment to download, or null if the file is no longer available. |
file_name, content_type | The uploaded file's original name and type. |
download_url | GET /v1/attachments/{id}/download for this file, or null. |
Download it with the same API key. The URL redirects to a short-lived link, so request it when you need the file rather than storing the redirect target:
curl -L "https://api.abcsalesbot.com/v1/attachments/9876/download" \
-H "Authorization: Bearer $ABC_API_KEY" \
-o receipt.jpg
This works for files uploaded through your embedded web forms and through the custom-field upload flow alike. An empty file field is null.
Rate limit
GET /v1/leads is limited to 300 requests per 10 minutes per company. That covers a once-a-minute poll with several pages, and an initial sync of a few thousand leads at limit=100. Back off on 429 as described in Rate Limits.