Skip to main content

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_at is in the timezone shown in meta.timezone: the timezone you asked for, or else your company's timezone, or else UTC.
  • updated_at is always UTC, with microseconds.

Filters​

All filters are optional query parameters and can be combined.

ParameterDescription
updated_sinceOnly leads changed strictly after this RFC 3339 timestamp. See Polling for changes.
automation_idOnly 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.
tagsOnly leads with any of these tags. Repeat the parameter (tags=vip&tags=hot) or separate with commas.
date_from, date_toOnly leads created on or after / on or before this day (YYYY-MM-DD), measured in timezone.
timezoneIANA timezone such as Asia/Kuala_Lumpur, for the date filters and created_at.
searchPartial match on name, phone or email.
sourceOnly leads with this source.
ai_enabledtrue or false: only leads with AI replies on or off.
from_adtrue or false: only leads that did or did not come from a click-to-WhatsApp ad.
messaging_channelOnly leads on this channel type: meta_whatsapp, twilio_whatsapp, twilio_sms, telegram, naver_line, facebook_messenger, instagram.
messaging_channel_connection_idsOnly leads on these connections, from GET /v1/messaging-channel-connections. Repeat or comma-separate.
limitPage size, 1 to 100. Default 50.
cursormeta.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:

  1. Call GET /v1/leads?updated_since=<last>&limit=100. On the very first run, leave out updated_since to read every lead.
  2. Process each lead, upserting by id.
  3. Follow meta.next_cursor until meta.has_more is false.
  4. Store the updated_at of 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_since is 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 new updated_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 subsequent updated_since poll, 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.
Store the timestamp, not the cursor

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.

What polling does not report
  • 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 use updated_since to 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:

FieldDescription
typeAlways file.
attachment_idThe attachment to download, or null if the file is no longer available.
file_name, content_typeThe uploaded file's original name and type.
download_urlGET /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.