Appearance
Stats & usage
Generated from
DialerDigital/corecommit0fc18008b3c8— specopenapi/dialer-v1.yamlv0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
Operational counters, blocked-dial rollups, usage metering.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/stats/summary | Today's operational counters (UTC day) |
GET | /v1/stats/contact-windows | Contact rate by the consumer's local hour, best window, phones per person |
GET | /v1/stats/promises | PTP pipeline summary, tenant totals and per campaign |
GET | /v1/stats/penetration | Person-level penetration analytics |
GET | /v1/stats/abandonment | Durable abandonment report of every campaign, in one response |
GET | /v1/stats/blocked | Durable blocked-dial rollup |
GET | /v1/stats/capacity | Tenant capacity ceilings + live usage (ADR #12) |
GET | /v1/stats/ai-usage | Voice-AI minute metering (invoicing read) |
GET | /v1/stats/sms-usage | SMS segment metering (invoicing read) |
Today's operational counters (UTC day)
http
GET /v1/stats/summaryComputed from the append-only CDR + live seat registry — reconstructible truth, not cached state.
Responses
200
Today's counters.
| Field | Type | Required | Description |
|---|---|---|---|
date | string (date) | no | |
window | "utc_day" | no | |
calls_today | integer | no | |
connected_today | integer | no | |
connect_rate | number | no | |
agents_online | integer | no | |
agents_available | integer | no | |
campaigns_running | integer | no |
json
{
"date": "2026-07-05",
"window": "utc_day",
"calls_today": 412,
"connected_today": 67,
"connect_rate": 0.1626,
"agents_online": 5,
"agents_available": 3,
"campaigns_running": 2
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}503
The counters could not be read within the statement timeout (report_timeout).
Contact rate by the consumer's local hour, best window, phones per person
http
GET /v1/stats/contact-windowsFor every hour of the day (0-23) in the CONSUMER's local time: how many attempts were placed, how many reached a person (answered_human) and the rate. The hour is the consumer's, not the tenant's or UTC's: it uses the zone the quiet-hours gate froze into the attempt when it was placed (the number's area-code zones plus the contact's declared zone), summer time included. Derived on read from the append-only CDR; no counter is stored.
An attempt is one original platform-placed outbound call. Inbound calls, external-transfer legs and rows the platform never placed are not attempts. A contact is an attempt with at least one answered_human outcome.
An attempt whose local hour cannot be known is counted in unknown, never spread over the hours: no_zone (nothing frozen, or a zone the time-zone database does not know) and ambiguous_zone (an area code that spans several zones, or a declared zone that disagrees with it). unknown.attempts is the sum of both reasons.
best_window is the two consecutive local hours with the highest contact rate, and it is named only when EACH of the two hours holds at least min_attempts attempts (tenant setting contact_window_min_attempts, default 50). Otherwise it is null and best_window_reason says why (insufficient_attempts, or no_resolved_attempts when no attempt had a known local hour); an hour is never invented from a handful of calls. Ties go to the earlier hour; the window does not wrap past midnight. Both hours must also lie inside the legal calling window [08:00, 21:00), so a window is never recommended in a time the platform would refuse to dial; hours outside it still appear in hours but cannot anchor best_window.
weekdays is the same contact rate on the consumer's local ISO weekday axis (1 = Monday ... 7 = Sunday). applied says whether the tenant's best_time_ranking_enabled knob is on (the dialer feed orders leads by this data), source the level that speaks for it (campaign, portfolio or none), and weights the weight table the dialer applies to each (weekday, local hour) cell inside the legal window while applied is true, with the level that spoke for that cell; when applied is false the dialer keeps the neutral order and source/weights show what it would apply with the knob on. weights always comes from the trailing 90 days and is not narrowed by from/to.
phones_per_person is the mean number of distinct phone numbers per person (consumer_ref) among the tenant's active accounts (any state but paid, settled or closed). A person with an active account and no phone counts as 0. It is tenant-wide: from, to and campaign_id do not narrow it. average is null when there is no active person.
from/to default to the trailing 30 days; a window wider than 90 days, or with from not before to, is 422. An unknown campaign (or another tenant's) is 404, never an all-zero report.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string (date-time) | no | RFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted). |
to | query | string (date-time) | no | RFC 3339 upper bound. |
campaign_id | query | string (uuid) | no |
Responses
200
Hourly contact rates, the best window and phones per person.
| Field | Type | Required | Description |
|---|---|---|---|
from | string (date-time) | yes | |
to | string (date-time) | yes | |
campaign_id | string (uuid) | null | yes | |
basis | "consumer_local_hour" | yes | |
min_attempts | integer | yes | The floor each hour of best_window must meet (contact_window_min_attempts). |
hours | array of ContactWindowHour | yes | Always 24 rows, hour 0 to 23, in the consumer's local time. |
hours[].hour | integer | yes | |
hours[].attempts | integer | yes | |
hours[].contacts | integer | yes | |
hours[].contact_rate | number | null | yes | contacts / attempts, four decimals; null when there were no attempts. |
weekdays | array of ContactWindowWeekday | no | Always 7 rows, ISO weekday 1 (Monday) to 7 (Sunday), in the consumer's local time. Additive (core#1980); optional for clients written before it. |
weekdays[].weekday | integer | yes | ISO weekday, 1 = Monday. |
weekdays[].attempts | integer | yes | |
weekdays[].contacts | integer | yes | |
weekdays[].contact_rate | number | null | yes | contacts / attempts, four decimals; null when there were no attempts. |
applied | boolean | no | Whether the tenant's best_time_ranking_enabled knob is on, i.e. whether the dialing feed orders leads by this data. Additive (core#1980). |
source | campaign · portfolio · none | no | The level that speaks for weights — the campaign's own history, the tenant's portfolio, or none (no evidence, neutral order). Additive (core#1980). |
weights | array of ContactWindowWeight | no | The weight the dialer applies to each (weekday, local hour) cell inside the legal window [08:00, 21:00) while applied is true (with applied false it keeps the neutral order and this is what it would apply) — 7 x 13 rows, or empty if it could not be computed. Trailing 90 days; not narrowed by from/to. Additive (core#1980). |
weights[].weekday | integer | yes | |
weights[].hour | integer | yes | |
weights[].weight | number | yes | 1.0 is neutral. |
weights[].source | campaign · portfolio · none | yes | |
weights[].attempts | integer | yes | Attempts in this cell at the level that spoke; 0 when source is none. |
weights[].contacts | integer | yes | |
unknown | object | yes | Attempts whose local hour is not knowable. Never redistributed over hours. |
unknown.attempts | integer | yes | |
unknown.contacts | integer | yes | |
unknown.no_zone | integer | yes | Attempts with no usable frozen zone. |
unknown.ambiguous_zone | integer | yes | Attempts whose area code spans several zones or disagrees with the declared one. |
best_window | ContactWindow | null | yes | |
best_window_reason | insufficient_attempts · no_resolved_attempts | null | yes | Why best_window is null; null when a window is named. |
phones_per_person | object | yes | |
phones_per_person.persons | integer | yes | Distinct consumer_ref with an active account. |
phones_per_person.phones | integer | yes | Sum of distinct phones per person. |
phones_per_person.average | number | null | yes | phones / persons, two decimals; null with no active person. |
json
{
"from": "2026-09-04T00:00:00Z",
"to": "2026-10-04T00:00:00Z",
"campaign_id": null,
"basis": "consumer_local_hour",
"min_attempts": 50,
"hours": [
{
"hour": 9,
"attempts": 120,
"contacts": 18,
"contact_rate": 0.15
},
{
"hour": 10,
"attempts": 140,
"contacts": 28,
"contact_rate": 0.2
},
{
"hour": 11,
"attempts": 0,
"contacts": 0,
"contact_rate": null
}
],
"weekdays": [
{
"weekday": 1,
"attempts": 90,
"contacts": 15,
"contact_rate": 0.1667
}
],
"unknown": {
"attempts": 14,
"contacts": 1,
"no_zone": 9,
"ambiguous_zone": 5
},
"applied": true,
"source": "portfolio",
"weights": [
{
"weekday": 1,
"hour": 9,
"weight": 1.2,
"source": "portfolio",
"attempts": 120,
"contacts": 18
},
{
"weekday": 1,
"hour": 10,
"weight": 1,
"source": "none",
"attempts": 0,
"contacts": 0
}
],
"best_window": {
"start_hour": 9,
"end_hour": 11,
"attempts": 260,
"contacts": 46,
"contact_rate": 0.1769
},
"best_window_reason": null,
"phones_per_person": {
"persons": 4,
"phones": 7,
"average": 1.75
}
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}422
The window is wider than 90 days, or from is not before to.
json
{
"error": {
"code": "unprocessable",
"message": "the window must not exceed 90 days"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}PTP pipeline summary, tenant totals and per campaign
http
GET /v1/stats/promisesCurrencies never sum together; kept_rate_30d is null when nothing resolved in the window.
by_campaign has one row per campaign with at least one promise, derived at read time through the attempt the promise was captured on (promises.call_attempt_id -> call_attempts.campaign_id); no counter is stored. A promise with no attempt, or whose attempt has no campaign, is grouped in the row with campaign_id: null — never dropped — so the rows always add up to the top-level totals.
With campaign_id every figure narrows to that campaign. An unknown campaign (or another tenant's) is 404, never an all-zero summary.
The kept rate is client-reported. The platform does not learn on its own whether a consumer paid: a promise only becomes kept or broken when the client says so through PATCH /v1/promises/{id}. A client that never reports leaves its promises pending and the rate null.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaign_id | query | string (uuid) | no |
Responses
200
Pipeline summary.
json
{
"campaign_id": null,
"open": {
"count": 4,
"amount_cents": {
"USD": 50000,
"MXN": 120000
}
},
"resolved_30d": {
"kept": 3,
"broken": 1
},
"kept_rate_30d": 0.75,
"window_days": 30,
"by_campaign": [
{
"campaign_id": "3f2c1b0a-9d8e-4c7b-a6f5-4e3d2c1b0a99",
"open": {
"count": 3,
"amount_cents": {
"USD": 50000,
"MXN": 0
}
},
"resolved_30d": {
"kept": 3,
"broken": 1
},
"kept_rate_30d": 0.75
},
{
"campaign_id": null,
"open": {
"count": 1,
"amount_cents": {
"USD": 0,
"MXN": 120000
}
},
"resolved_30d": {
"kept": 0,
"broken": 0
},
"kept_rate_30d": null
}
]
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Person-level penetration analytics
http
GET /v1/stats/penetrationPerson-level dedup (each debt counts once): attempted = at least one original attempt, reached = any non-failed disposition or conversation, effective_contact = answered_human or conversation. campaign_ids is optional (max 100; campaign_id= also accepted): without it the report covers EVERY campaign of the calling tenant, with one person-level total (an account in two campaigns counts once), so a tenant with more than 100 campaigns reads its whole penetration in one call. With the list the response is the same as ever.
granularity=day adds by_day: one row per LOCAL day of the window and per campaign, the day cut in the IANA zone tz names, summer time included (CET is not a fixed +01:00). The grid is dense (a quiet day is a row of zeros) and is derived on read from the append-only CDR. With granularity=day, tz is REQUIRED — there is no tenant zone to default to — and a missing or non-IANA tz is 422, never a silent UTC cut; from and to are required too and the window is capped at 31 days (400 otherwise, the /v1/reports/attempts criterion). totals and by_campaign stay the range numbers: a person-level count does not add up across days. Without granularity (or with total) the response is unchanged and tz is not read.
window=7d|30d|90d|all answers the same totals / by_campaign over a window RELATIVE to now, served from a short-lived in-memory snapshot derived from the append-only CDR (nothing is written when dialing). The response adds window and as_of, the instant the numbers were computed; a snapshot older than the server's max_age (default 60 s) is never served, the report is computed live instead. window is exclusive with from / to and with granularity=day (422); without it the response is unchanged.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaign_ids | query | string | no | Comma-separated campaign UUIDs (max 100). Omit it (or send it empty) for every campaign of the tenant. |
from | query | string (date-time) | no | RFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted). |
to | query | string (date-time) | no | RFC 3339 upper bound. |
window | query | 7d · 30d · 90d · all | no | Relative window served from the derived snapshot. Exclusive with from/to and with granularity=day. |
granularity | query | total · day | no | total (default) or day (adds by_day; requires tz, from and to). |
tz | query | string | no | IANA time zone the day is cut in. Required with granularity=day; ignored otherwise. |
Responses
200
Penetration totals + per-campaign rows (+ by_day with granularity=day).
| Field | Type | Required | Description |
|---|---|---|---|
totals | object (PenetrationRow) | no | |
totals.campaign_id | string (uuid) | null | no | |
totals.unique_accounts | integer | no | |
totals.unique_consumers | integer | no | |
totals.attempted | integer | no | |
totals.reached | integer | no | |
totals.effective_contact | integer | no | |
totals.attempted_pct | number | no | |
totals.reached_pct | number | no | |
totals.effective_contact_pct | number | no | |
by_campaign | array of PenetrationRow | no | |
by_campaign[].campaign_id | string (uuid) | null | no | |
by_campaign[].unique_accounts | integer | no | |
by_campaign[].unique_consumers | integer | no | |
by_campaign[].attempted | integer | no | |
by_campaign[].reached | integer | no | |
by_campaign[].effective_contact | integer | no | |
by_campaign[].attempted_pct | number | no | |
by_campaign[].reached_pct | number | no | |
by_campaign[].effective_contact_pct | number | no | |
window | 7d · 30d · 90d · all | no | Present only with window=. |
as_of | string (date-time) | no | Present only with window=: when the numbers were computed. Never older than the server's snapshot max_age. |
granularity | day | no | Present only with granularity=day. |
tz | string | no | The zone the days were cut in. Present only with granularity=day. |
by_day | array of PenetrationDayRow | no | Present only with granularity=day. Every local day the window touches × every campaign with a universe, ordered by day then campaign; zeros included. The first and last day are partial when from/to are not local midnights. Counts add up across campaigns (a debt belongs to one campaign), not across days. |
by_day[].day | string (date) | no | |
by_day[].campaign_id | string (uuid) | no | |
by_day[].unique_accounts | integer | no | The campaign's universe — the same number its by_campaign row reports. |
by_day[].attempted | integer | no | |
by_day[].reached | integer | no | |
by_day[].effective_contact | integer | no | |
by_day[].attempted_pct | number | no | Over unique_accounts. |
by_day[].reached_pct | number | no | Over unique_accounts. |
by_day[].effective_contact_pct | number | no | Over unique_accounts. |
json
{
"totals": {
"campaign_id": null,
"unique_accounts": 4,
"unique_consumers": 4,
"attempted": 2,
"reached": 1,
"effective_contact": 1,
"attempted_pct": 50,
"reached_pct": 25,
"effective_contact_pct": 25
},
"by_campaign": [
{
"campaign_id": "3f2c1b0a-9d8e-4c7b-a6f5-4e3d2c1b0a99",
"unique_accounts": 4,
"unique_consumers": 4,
"attempted": 2,
"reached": 1,
"effective_contact": 1,
"attempted_pct": 50,
"reached_pct": 25,
"effective_contact_pct": 25
}
]
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}422
granularity=day without tz, or with a tz that is not an IANA time zone; or window that is unknown or combined with from/to or granularity=day.
json
{
"error": {
"code": "unprocessable",
"message": "tz is required with granularity=day"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Durable abandonment report of every campaign, in one response
http
GET /v1/stats/abandonmentThe same report GET /v1/campaigns/{id}/abandonment gives, for EVERY campaign of the calling tenant at once (the console used to ask once per campaign). Served from a short-lived in-memory snapshot derived from the append-only CDR (nothing is written when dialing): as_of is the instant the numbers were computed, and a snapshot older than the server's max_age (default 60 s) is never served, the report is computed live instead. The per-campaign route stays live and is the record; this one is the overview.
Responses
200
One AbandonmentReport per campaign, ordered by campaign name.
| Field | Type | Required | Description |
|---|---|---|---|
as_of | string (date-time) | yes | |
campaigns | array of AbandonmentReport | yes | |
campaigns[].campaign_id | string (uuid) | yes | |
campaigns[].measured_at | string (date-time) | yes | |
campaigns[].period_days | 30 | yes | |
campaigns[].ceiling_percent | 3 | yes | |
campaigns[].anchored_at | string (date-time) | null | yes | The campaign's first placed dial; null = no dial yet (and no period). |
campaigns[].predicate | object | yes | |
campaigns[].predicate.counts | array of no_agent · ai_stream_lost · agent_bridge_failed · handoff_failed · late_bridge | yes | The reasons the rate counts as abandoned. |
campaigns[].predicate.reported_apart | array of no_agent · ai_stream_lost · agent_bridge_failed · handoff_failed · late_bridge | yes | Reasons reported per period and NOT counted in the rate. |
campaigns[].periods | array of AbandonmentPeriod | yes | One entry per 30-day period with at least one placed dial, oldest first. |
campaigns[].periods[].index | integer | yes | [anchored_at + 30·index days, + 30 days). |
campaigns[].periods[].starts_at | string (date-time) | yes | |
campaigns[].periods[].ends_at | string (date-time) | yes | Exclusive. |
campaigns[].periods[].complete | boolean | yes | ends_at is not after measured_at. |
campaigns[].periods[].placed | integer | yes | |
campaigns[].periods[].answered | integer | yes | |
campaigns[].periods[].answered_machine | integer | yes | Answered with an AMD machine verdict. |
campaigns[].periods[].answered_live | integer | yes | The denominator: answered - answered_machine. |
campaigns[].periods[].abandoned | integer | yes | The numerator; the sum of abandoned_by_reason. |
campaigns[].periods[].abandoned_by_reason | object | yes | One key per reason in predicate.counts. Readers must not assume a closed set of keys: agent_bridge_failed was added in core#1383. |
campaigns[].periods[].handoff_failed | integer | yes | Voice-AI handoffs with no seat; not in the rate. |
campaigns[].periods[].late_bridge | integer | yes | Live answers not counted as abandoned whose consumer leg was bridged more than 2 s after answered_at (core#1426); not in the rate. |
campaigns[].periods[].rate | number | null | yes | abandoned / answered_live, 4 decimals; null with no live answer. |
campaigns[].periods[].exceeds_ceiling | boolean | yes | abandoned * 100 > 3 * answered_live. |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Durable blocked-dial rollup
http
GET /v1/stats/blockedReads the durable, APPEND-ONLY gate_blocks evidence: one row per (debt, gate, campaign, UTC day) the compliance engine REFUSED to dial. Every number is a count of distinct refusals, never an estimate. Defaults: trailing 30 UTC days.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string (date-time) | no | RFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted). |
to | query | string (date-time) | no | RFC 3339 upper bound. |
campaign_id | query | string (uuid) | no |
Responses
200
Blocked-dial rollup.
| Field | Type | Required | Description |
|---|---|---|---|
from | string (date-time) | no | |
to | string (date-time) | no | |
campaign_id | string (uuid) | null | no | |
total | integer | no | |
by_gate | object | no | Keys are the ComplianceGate vocabulary. |
daily | array of object | no | |
daily[].date | string (date) | no | |
daily[].total | integer | no | |
daily[].by_gate | object | no |
json
{
"from": "2026-06-05T00:00:00Z",
"to": "2026-07-05T00:00:00Z",
"campaign_id": null,
"total": 5,
"by_gate": {
"quiet_hours": 4,
"dnc_listed": 1
},
"daily": [
{
"date": "2026-06-01",
"total": 3,
"by_gate": {
"quiet_hours": 2,
"dnc_listed": 1
}
}
]
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Tenant capacity ceilings + live usage (ADR #12)
http
GET /v1/stats/capacityThe tenant's admin-set capacity ceilings (max_cps, max_concurrent_channels; null = no ceiling) plus live usage read from the capacity gate. limits: null = the ceilings could not be read AND were never cached — the exact condition the gate fails CLOSED on; surfaced honestly, never fabricated as "unlimited".
supervision is a SECOND, independent ceiling (ADR #37): live-call supervision legs originate outside the dial choke point, so they are counted in their own quota. Neither lane can exhaust the other — the dialing floor can be at its ceiling while supervision still admits, and vice versa.
Responses
200
Capacity ceilings + live usage.
| Field | Type | Required | Description |
|---|---|---|---|
limits | object | null | no | Admin-set ceilings, or null when the gate fails closed (ceilings unreadable and never cached). Each ceiling is null when unset. |
channels_in_use | integer | no | |
dials_last_second | integer | no | |
supervision | object | no | The SECOND ceiling (ADR #37): concurrent live-call supervision sessions, counted and bounded separately from the dialing lane so neither can starve the other. limit is the deploy-time max_supervision_channels; in_use counts (call, supervisor) sessions, so a listen → barge switch on one call counts once. |
supervision.limit | integer | no | |
supervision.in_use | integer | no |
json
{
"limits": {
"max_cps": 10,
"max_concurrent_channels": 50
},
"channels_in_use": 3,
"dials_last_second": 2,
"supervision": {
"limit": 20,
"in_use": 1
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Voice-AI minute metering (invoicing read)
http
GET /v1/stats/ai-usageOff the durable exactly-once ai_usage feed (one row per answered AI call, billed EXACTLY ONCE on call_uuid). Each row snapshots the per-minute rate it was metered under — changing the configured rate never reprices history. The window is half-open — from inclusive, to EXCLUSIVE — so a call answered exactly at a billing cut belongs to one period only (the one starting at that instant), never to both sides of the cut. ?format=csv (or Accept: text/csv) returns the billing artifact with its own X-Artifact-SHA256.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string (date-time) | no | RFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted). |
to | query | string (date-time) | no | RFC 3339 upper bound. |
campaign_id | query | string (uuid) | no | |
format | query | csv | no | csv for the downloadable artifact (equivalent: Accept: text/csv). |
Responses
200
Metering stats (JSON) or the CSV billing artifact.
| Field | Type | Required | Description |
|---|---|---|---|
from | string (date-time) | no | |
to | string (date-time) | no | |
campaign_id | string (uuid) | null | no | |
minute_rate_usd | number | no | |
calls | integer | no | |
billable_seconds | integer | no | |
billable_minutes | number | no | |
estimated_cost_usd | number | no | |
daily | array of object | no | |
daily[].date | string (date) | no | |
daily[].calls | integer | no | |
daily[].billable_seconds | integer | no | |
daily[].billable_minutes | number | no | |
daily[].minute_rate_usd | number | no | |
daily[].estimated_cost_usd | number | no |
json
{
"from": "2026-06-05T00:00:00Z",
"to": "2026-07-05T00:00:00Z",
"campaign_id": null,
"minute_rate_usd": 0.25,
"calls": 3,
"billable_seconds": 270,
"billable_minutes": 4.5,
"estimated_cost_usd": 1.13,
"daily": [
{
"date": "2026-06-01",
"calls": 2,
"billable_seconds": 180,
"billable_minutes": 3,
"minute_rate_usd": 0.25,
"estimated_cost_usd": 0.75
}
]
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}SMS segment metering (invoicing read)
http
GET /v1/stats/sms-usageStraight off the unified ledger: sms_segment billable events, exactly-once at provider ACCEPT, each priced at its region-rate snapshot. JSON, or ?format=csv / Accept: text/csv for the billing artifact.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string (date-time) | no | RFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted). |
to | query | string (date-time) | no | RFC 3339 upper bound. |
campaign_id | query | string (uuid) | no | |
format | query | csv | no | csv for the downloadable artifact (equivalent: Accept: text/csv). |
Responses
200
Metering stats (JSON) or the CSV billing artifact.
| Field | Type | Required | Description |
|---|---|---|---|
from | string (date-time) | no | |
to | string (date-time) | no | |
campaign_id | string (uuid) | null | no | |
segment_rates_usd | object | no | Per-region rate currently in force (what new sends will snapshot). |
messages | integer | no | |
segments | string | no | Decimal as string. |
amount_usd | string | no | Decimal as string. |
daily | array of object | no | |
daily[].date | string (date) | no | |
daily[].messages | integer | no | |
daily[].segments | string | no | |
daily[].segment_rate_usd | string | no | |
daily[].amount_usd | string | no |
json
{
"from": "2026-06-05T00:00:00Z",
"to": "2026-07-05T00:00:00Z",
"campaign_id": null,
"segment_rates_usd": {
"us": "0.0100",
"mx": "0.0300",
"eu": "0.0500"
},
"messages": 2,
"segments": "3",
"amount_usd": "0.03",
"daily": [
{
"date": "2026-07-01",
"messages": 2,
"segments": "3",
"segment_rate_usd": "0.0100",
"amount_usd": "0.03"
}
]
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}