LeadArc Docsv1

Markdown for agents

This page as plain markdown, ready to paste into an LLM.

.md

Pagination and filtering

Cursor paging with exact page edges, filters that all run in SQL, and one sort parameter.

Cursor pagination

Cursors only. There is no offset paging. New replies arrive while you page, and an offset would skip or repeat rows.

ParameterMeaning
limit1 to 200. Defaults to 50. Higher values are clamped.
cursorThe next_cursor from the last page. Opaque. Do not build one.
orderdesc (default) or asc. Flips the endpoint's one sort key. There is no sort_by.

Every list returns the same envelope:

json
{
  "object": "list",
  "data": [
    "…"
  ],
  "has_more": true,
  "next_cursor": "MjAyNi0wNy0yN1QwOToxNDoyMi4wMDBafDk5MTIwNDE"
}
bash
cursor=""
while :; do
  page=$(curl -sS "https://api.leadarc.io/v1/workspaces/airpay/leads?limit=100${cursor:+&cursor=$cursor}" \
    -H "Authorization: Bearer $LEADARC_API_KEY")
  echo "$page" | jq -c '.data[]'
  [ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break
  cursor=$(echo "$page" | jq -r '.next_cursor')
done
Cursors use two columns, not one.

The lead feed pages on last_message_at plus the reply id. Two leads can share a timestamp to the millisecond, and a one-column cursor would drop one of them at the page edge.

No list returns a total count. Counting is too expensive. Page until has_more is false.

Filters

Every filter runs in the database. Nothing is filtered after loading, so a filtered page is never a partial answer.

An unknown query parameter is always 400 unsupported_filter, and the error names it. A typo like ?statuses=interested is reported instead of ignored.

GET /v1/workspaces/{ws}/leads filters:

ParamValuesSQL
statusCSV of interested, not_interested, automated, unsubscribed, untagged, booked, disqualifiedTranslates to a last_action predicate for the first five and a booked_at/disqualified_at predicate for the last two
campaign_idone cmp_ idresponses_index.campaign_id
assignee_idCSV of usr_ ids, or the literal noneEXISTS / NOT EXISTS on lead_assignments
emailexact address, case-insensitivelower(lead_email) = ?
has_open_tasktrue \falseEXISTS on tasks scoped to the key owner. See the note.
task_due_beforeYYYY-MM-DDtasks.due_date < ?, same scoping
meeting_outcomeCSV of showed, no_show, no_show_host, cancelled, rescheduled, noneresponses_index.meeting_outcome
directionsent \receivedlast_message_direction
updated_sinceRFC3339last_message_at > ?, forward order
updated_beforeRFC3339last_message_at < ?

Inside the app, INTERESTED and friends live on last_action, while booked and disqualified live on the feed axis backed by booked_at and disqualified_at. the app deliberately excludes DISQUALIFIED from its tag allow-list for that reason. An agent reading a docs table that lists seven lifecycle states will send all seven to one param, so the API accepts all seven on one param and routes them internally.

Note that DNC is surfaced as unsubscribed, because STATUS_META.DNC.label in the app is already "Unsubscribed". dnc is accepted as an alias on input and never returned.

updated_since is not mutually exclusive with cursor. Inside the app, fetchInboxRows treats cursor and since as exclusive with since winning silently, which makes "every lead that replied in June" impossible to express. The API allows a bounded window plus a cursor inside it, which is what a reconciliation job needs.

There is no q= full-text search. responses_index has responses_index_lead_email_idx and no trigram or tsvector index. A substring search over body_preview is a sequential scan on the largest table in the database. Exact email= is offered instead. Real search needs an index first, and that is a separate decision.

Sorting

One param, order, with two values: desc (default) and asc. It flips the endpoint's single fixed sort key. There is no sort_by.

Fixed sort keys: leads by (last_message_at, eb_reply_id). Meetings by (booked_at, id). Tasks by (due_date, id). Notes, templates, assets, sequences by (order_index, id) or (created_at, id) as noted per endpoint.

Expansions

?expand=a,b inlines related objects. Maximum three per request. More is 400 invalid_request.

expand=messages and expand=thread are not available on any list endpoint, at any limit. Building a thread calls buildLeadThreadMessages in the app, which on a cache miss makes up to 14 EmailBison round trips per lead (the anchor reply's conversation thread, up to MAX_SIBLINGS = 12 sibling threads, and the lead's /sent-emails). One hundred leads is up to 1,400 upstream calls in a single HTTP request. That would time out, exhaust the pool while writes queue behind it, and get the whole agency rate-limited by EmailBison, which breaks the Master Inbox for every human SDR. Threads are single-lead only, on GET /v1/workspaces/{ws}/leads/{lead}/messages.

Dates and times

KindLooks likeUsed for
Instant2026-07-27T09:14:22.000ZEvery timestamp. Always UTC with a Z.
Day2026-07-29tasks.due_date, next_due_at, daily insights. A day, not an instant.
Wall clock10:30tasks.due_time. 24 hour local time, no timezone.
Unix seconds1785312060The x-ratelimit-reset header only.

updated_since, booked_before, from and to take a full date-time. task_due_before and due_after take YYYY-MM-DD and compare whole days, so no timezone shifts them.

Base URL https://api.leadarc.io/v1. Generated from the API source.