curl --request GET \
--url https://{host}/api/v1/campaigns/{id}/contacts \
--header 'Authorization: Bearer <token>'const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://{host}/api/v1/campaigns/{id}/contacts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://{host}/api/v1/campaigns/{id}/contacts"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text){
"items": [
{
"attempt_count": 123,
"contact_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"disposition": "<string>",
"dnc": true,
"e164": "<string>",
"fields": {},
"last_attempt_at": "2023-11-07T05:31:56Z",
"name": "<string>",
"next_attempt_at": "2023-11-07T05:31:56Z",
"state": "pending"
}
],
"total": 123,
"limit": 123,
"offset": 123
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}The campaign's call list
Paged, sortable and searchable, because this is what an operator reads BEFORE launching a campaign rather than a diagnostic dump afterwards. It was capped at 500 with no paging, no sort and no filter: a list of fifty thousand showed the first five hundred by state and gave no way to find anybody, which is a sample rather than a review.
Each contact carries fields — the columns their uploaded row had, the same values the agent sees mid-call — and the campaign carries list_columns, the file’s header in its own order, so the list renders as the table that was uploaded rather than as a phone book.
A contact whose record is marked do-not-call shows as suppressed, so the list says why somebody will never be called rather than leaving an operator to wonder. Sort keys: attempt_count, e164, last_attempt_at, name, state.
curl --request GET \
--url https://{host}/api/v1/campaigns/{id}/contacts \
--header 'Authorization: Bearer <token>'const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://{host}/api/v1/campaigns/{id}/contacts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://{host}/api/v1/campaigns/{id}/contacts"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text){
"items": [
{
"attempt_count": 123,
"contact_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"disposition": "<string>",
"dnc": true,
"e164": "<string>",
"fields": {},
"last_attempt_at": "2023-11-07T05:31:56Z",
"name": "<string>",
"next_attempt_at": "2023-11-07T05:31:56Z",
"state": "pending"
}
],
"total": 123,
"limit": 123,
"offset": 123
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}{
"error": {
"code": "invalid_request",
"message": "<string>"
}
}Authorizations
Every request sends Authorization: Bearer <token>. The token is either a panel session (a JWT from /auth/login, 12 hours) or an API key ft_<id>_<secret>. An API key is accepted only from an address on its IP allowlist (403 ip_not_allowed otherwise; 403 ip_allowlist_required for an old key that has none), is limited to its rate per minute (429 rate_limited with Retry-After; X-RateLimit-Limit/Remaining/Reset on every response), and at most 60 call placements a minute.
Path Parameters
Query Parameters
x <= 500Sort key. Each list accepts its own allow-list of keys, given in that endpoint's description; anything else is a 400. ORDER BY cannot be parameterised, so the key is looked up rather than interpolated. Every sort carries a tiebreak on the primary key, so paging stays disjoint when the sort column has duplicates.
Sort direction. Defaults to ascending, except the call log, which reads newest first.
asc, desc Free-text search across the list's searchable columns (case-insensitive substring). % and _ in the term match themselves.
pending, leased, done, failed, suppressed