POST /calls/search
Search call history
A page of calls, newest first, for a time window you specify.
| Scope | reports:read |
| Role permission | Call reports |
| Cost | Free |
A POST that reads. The filter is a nested object — include and
exclude sets, four ranges, the AI fields — which does not survive a query
string, and query strings are logged by every proxy in the path. This
request creates nothing, changes nothing and is safe to retry.
start and end are required, unix seconds, at most 366 days apart.
There is deliberately no default window: a client that forgot the field
would otherwise receive one day of data and report it as the month.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
start | integer | yes | Start of the window, unix seconds. Required. |
end | integer | yes | End of the window, unix seconds. Required, must be after start, and at most 366 days later. |
timeZone | string | no | IANA zone, e.g. America/New_York. Decides where a day boundary falls for the day-of-week and day-of-month filters. Defaults to UTC, which shifts every daily figure — send it. |
limit | integer | no | Page size. Defaults to 50, capped at 500. |
offset | integer | no | Rows to skip. |
direction | inbound | outbound | no | Omit for both. |
filters | object | no | Every field is optional. Excludes are applied after includes. |
filters.numbers | string[] | no | Match these account numbers. |
filters.numbersExclude | string[] | no | |
filters.extensions | string[] | no | |
filters.extensionsExclude | string[] | no | |
filters.queues | string[] | no | |
filters.queuesExclude | string[] | no | |
filters.huntgroups | string[] | no | |
filters.huntgroupsExclude | string[] | no | |
filters.users | string[] | no | |
filters.directions | inbound | outbound[] | no | |
filters.statuses | answered | missed | voicemail[] | no | The words, not the numeric codes. |
filters.search | string | no | Matches caller and called names and numbers. |
filters.duration | object | no | Either end may be omitted for an open bound. Durations are seconds; timeOfDay takes HH:MM. |
filters.duration.start | string | no | |
filters.duration.end | string | no | |
filters.talkTime | object | no | Either end may be omitted for an open bound. Durations are seconds; timeOfDay takes HH:MM. |
filters.talkTime.start | string | no | |
filters.talkTime.end | string | no | |
filters.waitTime | object | no | Either end may be omitted for an open bound. Durations are seconds; timeOfDay takes HH:MM. |
filters.waitTime.start | string | no | |
filters.waitTime.end | string | no | |
filters.timeOfDay | object | no | Either end may be omitted for an open bound. Durations are seconds; timeOfDay takes HH:MM. |
filters.timeOfDay.start | string | no | |
filters.timeOfDay.end | string | no | |
filters.callPurpose | string[] | no | Per-account vocabulary, not a fixed enum. A value your account does not use matches nothing — that is not an error, and not a typo we can catch for you. |
filters.callSentiment | string[] | no | Per-account vocabulary, as callPurpose. |
sort | object | no | |
sort.field | string | no | A response field name, e.g. startTime. |
sort.direction | asc | desc | no |
Everything in a time window:
{
"start": 1754006400,
"end": 1754092800,
"timeZone": "America/New_York",
"limit": 50
}
Inbound calls nobody answered:
{
"start": 1754006400,
"end": 1754092800,
"timeZone": "America/New_York",
"direction": "inbound",
"filters": {
"statuses": [
"missed"
]
}
}
One queue, filtered on the AI purpose field:
{
"start": 1754006400,
"end": 1754092800,
"filters": {
"queues": [
"501"
],
"callPurpose": [
"billing"
]
},
"sort": {
"field": "startTime",
"direction": "desc"
}
}
Responses
200
A page of matching calls.
| Field | Type | Description |
|---|---|---|
calls | object[] | |
calls[].id | string | The call id — the same value in call webhooks and the recordings endpoints. Keep it a string: it looks like a decimal number and is not one, and parsing it as a float destroys the low digits. |
calls[].direction | inbound | outbound | |
calls[].status | answered | missed | voicemail | unknown | The word rather than the switch's integer. |
calls[].statusCode | integer | The raw code, because status flattens a distinction some clients need: -1 missed, 0 voicemail, 1 answered, and anything above 1 means answered by that many people — a call that rang a group and was picked up after a transfer. All of those report answered. |
calls[].callerType | string | external, user, queue, huntgroup. Read this before assuming a number field holds a phone number — on an internal leg it is an extension. |
calls[].callerName | string | |
calls[].callerNumber | string | |
calls[].calledType | string | |
calls[].calledName | string | |
calls[].calledNumber | string | |
calls[].dialledNumber | string | What was actually dialled — for an inbound call, your DID. |
calls[].callerIdInternal | string | |
calls[].callerIdExternal | string | |
calls[].startTime | string (RFC 3339) | RFC 3339 UTC. |
calls[].endTime | string (RFC 3339) | Omitted for a call with no recorded end. |
calls[].durationSeconds | integer | Includes ringing. |
calls[].talkTimeSeconds | integer | Connected time only. On a missed call this is 0 and durationSeconds is how long it rang. |
calls[].transferred | integer | A count, not a boolean — a call can be transferred more than once. |
calls[].abandoned | boolean | The caller hung up before anyone answered. This is what separates "we missed it" from "they gave up" — reporting on missed calls without it blames agents for calls nobody could have taken. |
calls[].recorded | boolean | Audio was captured. Fetch it with GET /calls/{callid}/recordings — this stays true for a call whose recording was since deleted, so it means "was recorded", not "can be played". |
calls[].calledBack | string | The id of the call that returned this one, when an abandoned call was called back. |
calls[].repeated | integer | Earlier calls from the same caller within the window. |
total | integer | How many calls match the filter across all pages — what a "1–50 of 812" control needs. Not the size of this page. |
limit | integer | The page size actually applied, after defaulting and capping. Send limit: 10000 and this comes back 500, so a short page is explained rather than mysterious. |
offset | integer |
{
"data": {
"total": 812,
"limit": 50,
"offset": 0,
"calls": [
{
"id": "1754489201.884213",
"direction": "inbound",
"status": "answered",
"statusCode": 1,
"callerType": "external",
"callerName": "ACME ROOFING",
"callerNumber": "13475550123",
"calledType": "user",
"calledName": "Dana Spencer",
"calledNumber": "204",
"dialledNumber": "12125550188",
"startTime": "2026-08-11T13:20:01Z",
"endTime": "2026-08-11T13:24:00Z",
"durationSeconds": 239,
"talkTimeSeconds": 220,
"transferred": 0,
"abandoned": false,
"recorded": true
}
]
}
}
Errors
| Status | Meaning |
|---|---|
400 | invalid_request — a missing or malformed field, an unknown enum value, or an unknown field in the body. Nothing was charged. |
401 | unauthenticated — no credential, or one that is invalid, revoked or expired. The WWW-Authenticate header names the scope the endpoint wanted. |
403 | Two different failures share this status, and the type tells them apart: |
500 | internal_error — something failed on our side. For a send, nothing was charged, guaranteed, which is what makes a retry safe. |
See Errors for the full catalog and what to do about each.
Example
curl -X POST 'https://api.account.telebroad.com/api/public/v1/calls/search' \
-H "Authorization: Bearer $TB_KEY" \
-H 'Content-Type: application/json' \
-d '{"start":1754006400,"end":1754092800,"timeZone":"America/New_York","limit":50}'