Skip to content

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 row
  • GET /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.

PropertyValue
MethodGET
Scopesusage:read
Org kindAny organization
Cost1 token per /usage/events page, 5 per /usage/summary
BodyNone

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:

meterWhat it chargesunit
call_minutesA call's line, from Twiliosecond
transcription_minutesTranscribing a call's recordingsecond
ai_tokensAn AI feature: QA scoring, the assistant, scripts…token
voice_ai_minutesA voice AI agent's conversationsecond
ai_agent_testsOne simulated scenario of a voice AI agent's testrun
number_rentalOne phone number held for one monthly periodmonth

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.

ParameterDefaultMeaning
from30 daysThe first day, YYYY-MM-DD, inclusive.
toTodayThe 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:

ParameterDefaultMeaning
limit50Charges 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 ​

FieldTypeNullableExample
idstring (uuid)No"01a1534d-7226-762e-ab67-45e1636b1f0d"
occurred_atstring (ISO)No"2026-10-19T08:35:30.453Z"
meterstringNo"call_minutes"
ai_taskstringYes"call_qa"
quantitynumberNo94
unitstringNo"second"
unit_ratenumberNo0.0123
rate_basisnumberNo1
costnumberNo1.1562
currencystringNo"ZAR"
call_idstring (uuid)Yes"5c9d3a70-1e84-4b2f-9c6a-8d0f1e3b7a25"
transcript_idstring (uuid)Yesnull
campaign_idstring (uuid)Yes"7e8f9a0b-1c2d-4e3f-a4b5-c6d7e8f9a0b1"
contact_idstring (uuid)Yes"8b7a6c5d-4e3f-4a2b-9c8d-7e6f5a4b3c2d"
agent_idstring (uuid)Yes"3f2e1d0c-9b8a-4765-a4b3-c2d1e0f9a8b7"
  • quantity is in unit: seconds, not minutes, for the three minute meters, and tokens for ai_tokens (input and output together).
  • unit_rate is rand per rate_basis units: per second where rate_basis is 1, per million tokens where it is 1000000.
  • ai_task is which AI feature an ai_tokens charge ran for (call_qa, assistant_chat, script_generate, qa_api_review, …), and null on every other meter. Read it as an open list: a new feature adds a value.
  • call_id, campaign_id, contact_id and agent_id say 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_id is 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:

ParameterDefaultMeaning
group_by—Up to 3 dimensions from the table below, in order. Without it, only the whole filter is summed.
sortsee below-cost, cost, -events, events or key.
limit100Groups 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_byThe group's keyExample
meterThe meter"call_minutes"
ai_taskThe AI feature, null on other meters"call_qa"
agentWho handled the call: kind is human or ai{ "id": "3f2e…", "kind": "human", "name": "Thandi Mokoena" }
campaignThe campaign{ "id": "7e8f…", "name": "Solar Q4" }
contactThe contact the call was with; name is null with none on file{ "id": "8b7a…", "name": "Ada Lovelace" }
callThe call, with when it was placed{ "id": "5c9d…", "created_at": "2026-10-19T08:30:02.000Z" }
phoneThe customer's number on the call, E.164"+27821234567"
dayThe day charged"2026-10-19"
weekThe week charged, by the Monday it starts on"2026-10-13"
monthThe month charged"2026-10"
hourThe hour of the day charged, 0–239
weekdayThe day of the week charged, 1 Monday to 7 Sunday1

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:

FieldMeaning
eventsHow many charges.
costWhat they cost, in rand.
quantityHow much was used, in unit — only where the group's charges are all one meter, else null.
unitquantity'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 first
  • cost — the cheapest first
  • -events / events — the most / fewest charges first
  • key — by the groups' keys, in group_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=call

Each 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=campaign

Every charge for one number, newest first:

GET /api/public/v1/usage/events?phone=%2B27821234567

Encode a + in a query string as %2B, or send the number as you would dial it locally (0821234567): a bare + reads as a space.