Usage
What your organization was charged against its prepaid credit — the same ledger the Usage page in Call Center shows. Two routes, with one set of filters:
GET /api/public/v1/usage/events— the charges themselves, newest first, one per rowGET /api/public/v1/usage/summary— the same charges summed, grouped by up to three dimensions
Whatever filters you send, the summary sums exactly the charges the list pages through, so you can read a total from one and the detail behind it from the other.
| Property | Value |
|---|---|
| Method | GET |
| Scopes | usage:read |
| Org kind | Any organization |
| Cost | 1 token per /usage/events page, 5 per /usage/summary |
| Body | None |
usage:read is not in the default scope set: spend is billing data, so a key reads it only when it was granted on purpose. A key without it gets 403, naming the scope.
What is charged
Each charge belongs to one meter:
meter | What it charges | unit |
|---|---|---|
call_minutes | A call's line, from Twilio | second |
transcription_minutes | Transcribing a call's recording | second |
ai_tokens | An AI feature: QA scoring, the assistant, scripts… | token |
voice_ai_minutes | A voice AI agent's conversation | second |
ai_agent_tests | One simulated scenario of a voice AI agent's test | run |
number_rental | One phone number held for one monthly period | month |
A call is charged a few minutes after it ends, and occurred_at is when we became liable — the call's end, not when the row was written. So today's figures read low for a while, and a charge can appear behind your place in a list you are paging through. For a complete copy, read a range that has closed.
Filters
Both routes take these. Every filter narrows; omit one to take everything.
| Parameter | Default | Meaning |
|---|---|---|
from | 30 days | The first day, YYYY-MM-DD, inclusive. |
to | Today | The last day, YYYY-MM-DD, inclusive. |
meter | — | One or more meters from the table above. |
campaign_id | — | One or more campaign ids (at most 50): charges for those campaigns. |
contact_id | — | One contact's id: charges for calls with them. |
phone | — | One phone number: charges for calls with it. +27821234567, 0821234567 and 082 123 4567 are the same number. |
call_id | — | One call's id: everything it was charged — its line, its voice AI conversation, its transcription, the AI that scored it. |
Send a filter that takes several values repeated or comma-separated — ?meter=call_minutes&meter=voice_ai_minutes and ?meter=call_minutes,voice_ai_minutes are the same.
Days are South African calendar days (Africa/Johannesburg), as the dashboard counts them: from=2026-10-01 starts at midnight SAST, which is 22:00 UTC the day before. Without from, the range is the 30 days ending on to. A range is at most 92 days, both ends included; a longer one is a 400 rather than a shorter answer, and so is a from after to.
contact_id and phone reach a charge through its call. A charge with no call — number rental, most AI features — never matches them. phone is the customer's number on the call: the number dialled on an outbound call, the caller's on an inbound one. It matches whether or not that number belongs to a contact you hold. contact_id matches a contact your organization holds, as the calls endpoint's contact_id does.
campaign_id also matches charges with no call that ran for a campaign — an AI QA review of one of its calls, for instance.
Anything else is a 400, as on every list — see Pagination:
json
{
"error": {
"code": "INVALID_INPUT",
"message": "`meter` must be one of call_minutes, transcription_minutes, ai_tokens, voice_ai_minutes, ai_agent_tests, number_rental.",
"path": ["meter", 0]
}
}Money
Amounts are rand (currency is always ZAR), as numbers with up to six decimal places — what the Usage page's CSV export writes. A charge's quantity × unit_rate ÷ rate_basis is its cost, give or take the last place. The rate is the one in force on the day, charged at that day's exchange rate; it does not change afterwards.
GET /api/public/v1/usage/events
Query parameters
The filters, and:
| Parameter | Default | Meaning |
|---|---|---|
limit | 50 | Charges per page, 1–100. |
cursor | — | The next_cursor from the previous page. |
Newest first, by occurred_at. Paging is the same as every list's; see Pagination. Send the same filters with each cursor.
The charge object
| Field | Type | Nullable | Example |
|---|---|---|---|
id | string (uuid) | No | "01a1534d-7226-762e-ab67-45e1636b1f0d" |
occurred_at | string (ISO) | No | "2026-10-19T08:35:30.453Z" |
meter | string | No | "call_minutes" |
ai_task | string | Yes | "call_qa" |
quantity | number | No | 94 |
unit | string | No | "second" |
unit_rate | number | No | 0.0123 |
rate_basis | number | No | 1 |
cost | number | No | 1.1562 |
currency | string | No | "ZAR" |
call_id | string (uuid) | Yes | "5c9d3a70-1e84-4b2f-9c6a-8d0f1e3b7a25" |
transcript_id | string (uuid) | Yes | null |
campaign_id | string (uuid) | Yes | "7e8f9a0b-1c2d-4e3f-a4b5-c6d7e8f9a0b1" |
contact_id | string (uuid) | Yes | "8b7a6c5d-4e3f-4a2b-9c8d-7e6f5a4b3c2d" |
agent_id | string (uuid) | Yes | "3f2e1d0c-9b8a-4765-a4b3-c2d1e0f9a8b7" |
quantityis inunit: seconds, not minutes, for the three minute meters, and tokens forai_tokens(input and output together).unit_rateis rand perrate_basisunits: per second whererate_basisis1, per million tokens where it is1000000.ai_taskis which AI feature anai_tokenscharge ran for (call_qa,assistant_chat,script_generate,qa_api_review, …), andnullon every other meter. Read it as an open list: a new feature adds a value.call_id,campaign_id,contact_idandagent_idsay what the charge was for, where it was for anything. A campaign and an agent are read off the charge itself, else off its call.agent_idis the person or voice AI agent who handled the call; the summary names them.
The fields are defined by ApiUsageSchema.event in src/lib/schema/api/api_usage.schema.ts.
Request
bash
curl -sS "https://<your-app-domain>/api/public/v1/usage/events?from=2026-10-01&to=2026-10-31&phone=0821234567" \
-H "authorization: Bearer $CALL_CENTER_API_KEY"Response
200 OK
json
{
"data": [
{
"id": "01a1534d-7226-762e-ab67-45e1636b1f0d",
"occurred_at": "2026-10-19T08:35:30.453Z",
"meter": "call_minutes",
"ai_task": null,
"quantity": 94,
"unit": "second",
"unit_rate": 0.0123,
"rate_basis": 1,
"cost": 1.1562,
"currency": "ZAR",
"call_id": "5c9d3a70-1e84-4b2f-9c6a-8d0f1e3b7a25",
"transcript_id": null,
"campaign_id": "7e8f9a0b-1c2d-4e3f-a4b5-c6d7e8f9a0b1",
"contact_id": "8b7a6c5d-4e3f-4a2b-9c8d-7e6f5a4b3c2d",
"agent_id": "3f2e1d0c-9b8a-4765-a4b3-c2d1e0f9a8b7"
}
],
"pagination": {
"limit": 50,
"has_more": false,
"next_cursor": null
}
}GET /api/public/v1/usage/summary
Query parameters
The filters, and:
| Parameter | Default | Meaning |
|---|---|---|
group_by | — | Up to 3 dimensions from the table below, in order. Without it, only the whole filter is summed. |
sort | see below | -cost, cost, -events, events or key. |
limit | 100 | Groups to return, 1–1000. |
Dimensions
Each group carries a key for every dimension asked, under the dimension's name. A key is null where the charge has none — a contact for number rental, say — and those charges are one group of their own.
group_by | The group's key | Example |
|---|---|---|
meter | The meter | "call_minutes" |
ai_task | The AI feature, null on other meters | "call_qa" |
agent | Who handled the call: kind is human or ai | { "id": "3f2e…", "kind": "human", "name": "Thandi Mokoena" } |
campaign | The campaign | { "id": "7e8f…", "name": "Solar Q4" } |
contact | The contact the call was with; name is null with none on file | { "id": "8b7a…", "name": "Ada Lovelace" } |
call | The call, with when it was placed | { "id": "5c9d…", "created_at": "2026-10-19T08:30:02.000Z" } |
phone | The customer's number on the call, E.164 | "+27821234567" |
day | The day charged | "2026-10-19" |
week | The week charged, by the Monday it starts on | "2026-10-13" |
month | The month charged | "2026-10" |
hour | The hour of the day charged, 0–23 | 9 |
weekday | The day of the week charged, 1 Monday to 7 Sunday | 1 |
Every time is South African, as the filters' days are. A practice call — an agent's microphone check — has no phone. A person who has left the organization keeps their name on agent.
Measures
Every group, and overall, carries:
| Field | Meaning |
|---|---|
events | How many charges. |
cost | What they cost, in rand. |
quantity | How much was used, in unit — only where the group's charges are all one meter, else null. |
unit | quantity's unit, as on a charge; null with it. |
quantity is null across meters because seconds and tokens do not add up. Group by meter, or filter to one, to get it everywhere.
Order and the cut
sort decides which groups are kept when there are more than limit:
-cost(the default) — the costliest firstcost— the cheapest first-events/events— the most / fewest charges firstkey— by the groups' keys, ingroup_by's order; for a time, oldest first
When the first group_by is a time — day, week, month, hour or weekday — the default is key instead, so a series comes back in time order. A day with no charges has no group.
truncated is true when more groups matched than limit. overall always sums every matching charge, kept or not. The summary is not paged: to see more, raise limit or narrow the filters.
Request
bash
curl -sS "https://<your-app-domain>/api/public/v1/usage/summary?from=2026-10-01&to=2026-10-31&campaign_id=7e8f9a0b-1c2d-4e3f-a4b5-c6d7e8f9a0b1&group_by=meter,day" \
-H "authorization: Bearer $CALL_CENTER_API_KEY"Response
200 OK
json
{
"data": {
"from": "2026-10-01",
"to": "2026-10-31",
"currency": "ZAR",
"group_by": ["meter", "day"],
"groups": [
{
"meter": "call_minutes",
"day": "2026-10-01",
"events": 212,
"cost": 418.204812,
"quantity": 33960,
"unit": "second"
},
{
"meter": "voice_ai_minutes",
"day": "2026-10-01",
"events": 64,
"cost": 301.5,
"quantity": 13404,
"unit": "second"
}
],
"overall": {
"events": 5120,
"cost": 13840.993201,
"quantity": null,
"unit": null
},
"truncated": false
}
}from and to are the days summed, defaults filled in. The fields are defined by ApiUsageSchema.summary in src/lib/schema/api/api_usage.schema.ts.
Examples
What one contact cost, per call:
GET /api/public/v1/usage/summary?contact_id=8b7a6c5d-4e3f-4a2b-9c8d-7e6f5a4b3c2d&group_by=callEach campaign's call minutes this month, costliest first:
GET /api/public/v1/usage/summary?from=2026-10-01&to=2026-10-31&meter=call_minutes&group_by=campaignEvery charge for one number, newest first:
GET /api/public/v1/usage/events?phone=%2B27821234567Encode a + in a query string as %2B, or send the number as you would dial it locally (0821234567): a bare + reads as a space.