Appearance
DIDs (caller IDs)
Generated from
DialerDigital/corecommitbbb8e373d900— specopenapi/dialer-v1.yamlv0.151.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
DID registry, CDR-computed health, launch checklist and carrier tickets.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/port-requests | Port-in requests of the tenant |
POST | /v1/port-requests | Port numbers in to the platform (port-in) |
GET | /v1/port-requests/{id} | Fetch one port-in request |
GET | /v1/dids | DID registry |
POST | /v1/dids | Register a tenant-owned number |
GET | /v1/dids/{id} | Fetch one DID |
PATCH | /v1/dids/{id} | Update metadata/status (e164 immutable) |
DELETE | /v1/dids/{id} | Retire a DID (rows never delete) |
GET | /v1/dids/{id}/health | CDR-computed DID health |
GET | /v1/dids/{id}/launch-checklist | Launch checklist of a number |
POST | /v1/dids/{id}/launch-checklist/{step}/confirmations | Confirm (or un-confirm) a manual launch step |
GET | /v1/dids/{id}/carrier-tickets | Carrier tickets of a number |
POST | /v1/dids/{id}/carrier-tickets | Open a carrier ticket for a number |
POST | /v1/dids/{id}/carrier-tickets/{ticket_id}/events | Add a note and/or move a carrier ticket's status |
Port-in requests of the tenant
http
GET /v1/port-requestsNewest first, at most 50. The documents are never returned, only their hash and type.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
status | query | pending · scheduled · rejected · completed | no |
Responses
200
The requests.
| Field | Type | Required | Description |
|---|---|---|---|
port_requests | array of PortRequest | yes | |
port_requests[].id | string (uuid) | yes | |
port_requests[].numbers | array of string | yes | |
port_requests[].account_holder | string | yes | |
port_requests[].status | pending · scheduled · rejected · completed | yes | |
port_requests[].reject_reason | string,null | no | |
port_requests[].foc_date | string,null (date) | no | Firm date set by the carrier. |
port_requests[].carrier | string | yes | |
port_requests[].loa | object (PortDocumentRef) | yes | |
port_requests[].loa.sha256 | string | yes | |
port_requests[].loa.content_type | string | yes | |
port_requests[].invoice | object (PortDocumentRef) | yes | |
port_requests[].invoice.sha256 | string | yes | |
port_requests[].invoice.content_type | string | yes | |
port_requests[].completed_at | string,null (date-time) | no | |
port_requests[].created_at | string (date-time) | yes | |
port_requests[].updated_at | string (date-time) | yes |
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 (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"
}
}Port numbers in to the platform (port-in)
http
POST /v1/port-requestsRegisters a port-in request and sends it to the house carrier. Documents travel as base64 in the JSON body (each at most 2 MiB; application/pdf, image/png or image/jpeg, checked against their magic bytes) and are stored ENCRYPTED, never returned. Fails closed before the carrier is called: inactive plan (402 plan_inactive), invalid input (400), a number already in the registry or in a live request (409 number_in_use). A test key is refused (403 test_environment). The carrier rejecting the submission is NOT an error: the request is stored with status: rejected and its reject_reason. 503 carrier_unavailable: the carrier did not answer; the request is kept as rejected (carrier_unavailable_at_submission) and its numbers are free to retry. 409 port_in_busy: another request of the tenant is being registered (retry shortly; nothing stored or sent). 429 port_request_limit: the tenant already has port_request_max_live (default 20) live requests; nothing stored or sent. 503 port_in_unavailable: this deployment has no port-in carrier configured; nothing was stored or sent.
Status follows the carrier (pending → scheduled with foc_date → completed, or rejected with reject_reason). On completed the numbers become origin: house DIDs of the tenant (they accrue the monthly rent) and the signed webhook port_request.ported is sent.
Role rules: a dd_ key or an account_admin login (403 role_forbidden otherwise).
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
numbers | array of string | yes | |
account_holder | string | yes | Name on the account at the current carrier. |
loa | object (PortDocument) | yes | |
loa.content_type | application/pdf · image/png · image/jpeg | yes | |
loa.content_base64 | string | yes | Base64 of the file, at most 2 MiB decoded. |
invoice | object (PortDocument) | yes | |
invoice.content_type | application/pdf · image/png · image/jpeg | yes | |
invoice.content_base64 | string | yes | Base64 of the file, at most 2 MiB decoded. |
Responses
201
The stored request.
| Field | Type | Required | Description |
|---|---|---|---|
port_request | object (PortRequest) | yes | A port-in request; also the data of the webhook port_request.ported. |
port_request.id | string (uuid) | yes | |
port_request.numbers | array of string | yes | |
port_request.account_holder | string | yes | |
port_request.status | pending · scheduled · rejected · completed | yes | |
port_request.reject_reason | string,null | no | |
port_request.foc_date | string,null (date) | no | Firm date set by the carrier. |
port_request.carrier | string | yes | |
port_request.loa | object (PortDocumentRef) | yes | |
port_request.loa.sha256 | string | yes | |
port_request.loa.content_type | string | yes | |
port_request.invoice | object (PortDocumentRef) | yes | |
port_request.invoice.sha256 | string | yes | |
port_request.invoice.content_type | string | yes | |
port_request.completed_at | string,null (date-time) | no | |
port_request.created_at | string (date-time) | yes | |
port_request.updated_at | string (date-time) | yes |
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"
}
}402 — PlanInactive
The tenant's plan was READ and is not effectively active, so the action is gated (code plan_inactive). On the SUPERVISION surface this status means the plan was actually read: a plan that could NOT be read passes instead of refusing. That is deliberately the opposite of the inbound gate's fail-CLOSED posture — a supervision leg goes to the tenant's own registrar and buys no carrier minutes, so the cost of a false refusal (a compliance officer blinded during a live call) exceeds the cost of a false admission.
Note which verbs can return it: the three EAVESDROP modes only. takeover is exempt and never returns plan_inactive, under the rule "gate what ADDS exposure, never what CORRECTS it" — listen/whisper/barge each open a new leg, while takeover takes the call away and kills the agent leg. A plan lapsing mid-shift must not strand a live call the supervisor is trying to end. The exemption is narrow, not a billing loophole: a dark tenant still cannot open NEW supervision, it only keeps the lever to end properly what is already running.
json
{
"error": {
"code": "plan_inactive",
"message": "tenant plan is not active; traffic is gated"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send. No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (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 carrier did not answer (carrier_unavailable) or this deployment has no port-in carrier configured (port_in_unavailable); nothing was stored or sent.
Fetch one port-in request
http
GET /v1/port-requests/{id}Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The request with the carrier status.
| Field | Type | Required | Description |
|---|---|---|---|
port_request | object (PortRequest) | yes | A port-in request; also the data of the webhook port_request.ported. |
port_request.id | string (uuid) | yes | |
port_request.numbers | array of string | yes | |
port_request.account_holder | string | yes | |
port_request.status | pending · scheduled · rejected · completed | yes | |
port_request.reject_reason | string,null | no | |
port_request.foc_date | string,null (date) | no | Firm date set by the carrier. |
port_request.carrier | string | yes | |
port_request.loa | object (PortDocumentRef) | yes | |
port_request.loa.sha256 | string | yes | |
port_request.loa.content_type | string | yes | |
port_request.invoice | object (PortDocumentRef) | yes | |
port_request.invoice.sha256 | string | yes | |
port_request.invoice.content_type | string | yes | |
port_request.completed_at | string,null (date-time) | no | |
port_request.created_at | string (date-time) | yes | |
port_request.updated_at | string (date-time) | yes |
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 (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"
}
}DID registry
http
GET /v1/didsParameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
status | query | active · quarantine · retired | no |
Responses
200
Registry rows ordered by e164.
| Field | Type | Required | Description |
|---|---|---|---|
dids | array of DID | yes | |
dids[].id | string (uuid) | no | |
dids[].tenant_id | string (uuid) | no | |
dids[].e164 | string | no | Immutable after creation. |
dids[].npa | string | null | no | |
dids[].us_state | string | null | no | |
dids[].attestation | A · B · C · unknown | no | STIR/SHAKEN level the carrier signs. Read-only for the tenant: only the platform admin sets it. |
dids[].status | active · quarantine · retired | no | |
dids[].verified_at | string (date-time) | null | no | Proof of ownership. null = self-declared, never proved; house DIDs are born verified. |
dids[].verification_method | house · otp · admin | null | no | How the ownership was proved; null while verified_at is null. |
dids[].origin | house · customer | no | house rows are admin-minted platform inventory; the tenant surface only creates customer. |
dids[].labels | array of string | no | |
dids[].notes | string | null | no | |
dids[].created_at | string (date-time) | no | |
dids[].updated_at | string (date-time) | no | |
truncated | boolean | yes | true when there are more rows than the 2 000 numbers the listing returns: the response is a prefix in the documented order, not the whole set. |
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 (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"
}
}Register a tenant-owned number
http
POST /v1/didse164 unique per tenant → 409 conflict on duplicates; npa is derived from +1 numbers when omitted. The tenant surface can only ever create origin: customer rows — house inventory is admin-minted (POST /v1/admin/tenants/{id}/dids).
Role rules: a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob telephony_config_admin_only, default on). The GET of this resource is not affected.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
e164 | string | yes | E.164 (immutable after creation). |
npa | string | no | |
us_state | string | no | |
attestation | A · B · C · unknown | no | Accepted and IGNORED: the STIR/SHAKEN attestation is a claim only the platform admin records (POST /v1/admin/tenants/{id}/dids/{did_id}/verify); it never changes through this route, which answers as if the key were absent. |
status | active · quarantine · retired | no | |
labels | array of string | no | |
notes | string | no | |
origin | house · customer | no | Accepted and IGNORED FOR SAFETY: a tenant can never mint origin: house, so any value here is overridden with customer. The enum mirrors the response schema because a GET of a house DID returns house — sending that value back does not fail, it has no effect. Declared so that THIS key does not become a 400 now that unknown keys are. It does NOT make the whole rendered object round-trip: id, tenant_id, created_at and updated_at are not accepted and DO get the 400. |
json
{
"e164": "+13125550142",
"us_state": "IL"
}Responses
201
Registered DID.
| Field | Type | Required | Description |
|---|---|---|---|
did | object (DID) | yes | |
did.id | string (uuid) | no | |
did.tenant_id | string (uuid) | no | |
did.e164 | string | no | Immutable after creation. |
did.npa | string | null | no | |
did.us_state | string | null | no | |
did.attestation | A · B · C · unknown | no | STIR/SHAKEN level the carrier signs. Read-only for the tenant: only the platform admin sets it. |
did.status | active · quarantine · retired | no | |
did.verified_at | string (date-time) | null | no | Proof of ownership. null = self-declared, never proved; house DIDs are born verified. |
did.verification_method | house · otp · admin | null | no | How the ownership was proved; null while verified_at is null. |
did.origin | house · customer | no | house rows are admin-minted platform inventory; the tenant surface only creates customer. |
did.labels | array of string | no | |
did.notes | string | null | no | |
did.created_at | string (date-time) | no | |
did.updated_at | string (date-time) | no |
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"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (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"
}
}Fetch one DID
http
GET /v1/dids/{id}Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The DID.
| Field | Type | Required | Description |
|---|---|---|---|
did | object (DID) | yes | |
did.id | string (uuid) | no | |
did.tenant_id | string (uuid) | no | |
did.e164 | string | no | Immutable after creation. |
did.npa | string | null | no | |
did.us_state | string | null | no | |
did.attestation | A · B · C · unknown | no | STIR/SHAKEN level the carrier signs. Read-only for the tenant: only the platform admin sets it. |
did.status | active · quarantine · retired | no | |
did.verified_at | string (date-time) | null | no | Proof of ownership. null = self-declared, never proved; house DIDs are born verified. |
did.verification_method | house · otp · admin | null | no | How the ownership was proved; null while verified_at is null. |
did.origin | house · customer | no | house rows are admin-minted platform inventory; the tenant surface only creates customer. |
did.labels | array of string | no | |
did.notes | string | null | no | |
did.created_at | string (date-time) | no | |
did.updated_at | string (date-time) | no |
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 (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"
}
}Update metadata/status (e164 immutable)
http
PATCH /v1/dids/{id}A different number is a different DID; CDR history is never re-attributed.
Role rules: a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob telephony_config_admin_only, default on). The GET of this resource is not affected.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
npa | string | no | |
us_state | string | no | |
attestation | A · B · C · unknown | no | Accepted and IGNORED: the STIR/SHAKEN attestation is a claim only the platform admin records (POST /v1/admin/tenants/{id}/dids/{did_id}/verify); it never changes through this route, which answers as if the key were absent. |
status | active · quarantine · retired | no | |
labels | array of string | no | |
notes | string | no | |
origin | house · customer | no | Accepted and IGNORED FOR SAFETY, same as on POST /v1/dids: origin is immutable and never cast, so any value here has no effect. Sending back the value a GET returned does not fail; the rest of the rendered object still does (id, tenant_id, e164, created_at, updated_at are a 400). |
json
{
"attestation": "A",
"labels": [
"postman"
],
"notes": "postman smoke"
}Responses
200
Updated DID.
| Field | Type | Required | Description |
|---|---|---|---|
did | object (DID) | yes | |
did.id | string (uuid) | no | |
did.tenant_id | string (uuid) | no | |
did.e164 | string | no | Immutable after creation. |
did.npa | string | null | no | |
did.us_state | string | null | no | |
did.attestation | A · B · C · unknown | no | STIR/SHAKEN level the carrier signs. Read-only for the tenant: only the platform admin sets it. |
did.status | active · quarantine · retired | no | |
did.verified_at | string (date-time) | null | no | Proof of ownership. null = self-declared, never proved; house DIDs are born verified. |
did.verification_method | house · otp · admin | null | no | How the ownership was proved; null while verified_at is null. |
did.origin | house · customer | no | house rows are admin-minted platform inventory; the tenant surface only creates customer. |
did.labels | array of string | no | |
did.notes | string | null | no | |
did.created_at | string (date-time) | no | |
did.updated_at | string (date-time) | no |
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 (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"
}
}Retire a DID (rows never delete)
http
DELETE /v1/dids/{id}Sets status retired, idempotent. Rows never leave the registry (DELETE is revoked in Postgres): CDR from_number provenance must keep resolving for the Defense Packet.
Role rules: a dd_ key or an account_admin login; a member login is 403 role_forbidden (per-tenant knob telephony_config_admin_only, default on). The GET of this resource is not affected.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The retired DID.
| Field | Type | Required | Description |
|---|---|---|---|
did | object (DID) | yes | |
did.id | string (uuid) | no | |
did.tenant_id | string (uuid) | no | |
did.e164 | string | no | Immutable after creation. |
did.npa | string | null | no | |
did.us_state | string | null | no | |
did.attestation | A · B · C · unknown | no | STIR/SHAKEN level the carrier signs. Read-only for the tenant: only the platform admin sets it. |
did.status | active · quarantine · retired | no | |
did.verified_at | string (date-time) | null | no | Proof of ownership. null = self-declared, never proved; house DIDs are born verified. |
did.verification_method | house · otp · admin | null | no | How the ownership was proved; null while verified_at is null. |
did.origin | house · customer | no | house rows are admin-minted platform inventory; the tenant surface only creates customer. |
did.labels | array of string | no | |
did.notes | string | null | no | |
did.created_at | string (date-time) | no | |
did.updated_at | string (date-time) | no |
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 (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"
}
}CDR-computed DID health
http
GET /v1/dids/{id}/healthHealth is COMPUTED from the append-only CDR at read time — every number is a row count, never a cached score. short_call_rate (answered calls ended in under 15 s / answered) is the strongest CDR-side "spam likely" signal. Rates are null when the denominator is zero. reputation.hiya/reputation.tns are explicit nulls until an external feed is contracted. A retired DID still serves health (provenance outlives rotation).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
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. |
Responses
200
Computed health for the period (default trailing 30 UTC days).
| Field | Type | Required | Description |
|---|---|---|---|
did_id | string (uuid) | no | |
e164 | string | no | |
status | active · quarantine · retired | no | |
period | object | no | |
period.from | string (date-time) | no | |
period.to | string (date-time) | no | |
dials | integer | no | |
answered | integer | no | |
answer_rate | number | null | no | |
short_calls | integer | no | |
short_call_rate | number | null | no | |
short_call_seconds | integer | no | |
daily | array of object | no | |
daily[].date | string (date) | no | |
daily[].dials | integer | no | |
daily[].answered | integer | no | |
daily[].short_calls | integer | no | |
reputation | object | no | Explicit nulls until an external feed is contracted — render as pending, not zero. |
reputation.hiya | number | null | no | |
reputation.tns | number | null | no | |
reputation.note | string | no |
json
{
"did_id": "4e3d2c1b-0a9f-4e8d-7c6b-5a4f3e2d1c0b",
"e164": "+13125550184",
"status": "active",
"period": {
"from": "2026-06-05T00:00:00Z",
"to": "2026-07-05T00:00:00Z"
},
"dials": 412,
"answered": 67,
"answer_rate": 0.1626,
"short_calls": 24,
"short_call_rate": 0.3582,
"short_call_seconds": 15,
"daily": [
{
"date": "2026-06-01",
"dials": 80,
"answered": 14,
"short_calls": 5
}
],
"reputation": {
"hiya": null,
"tns": null,
"note": "external reputation feeds not integrated yet"
}
}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 (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"
}
}Launch checklist of a number
http
GET /v1/dids/{id}/launch-checklistFour steps, each stating its source. attestation is derived from the registry (A/B/C = done; the value is what the tenant declared, not a carrier verdict). warmup is derived from the number's own CDR: done once it has dialled on warmup_min_active_days distinct UTC days of the trailing 30. registration and cnam are manual: the platform has no data for them, so they are what the operator confirmed. No external reputation feed is consulted.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The checklist.
| Field | Type | Required | Description |
|---|---|---|---|
did_id | string (uuid) | yes | |
e164 | string | yes | |
complete | boolean | yes | All four steps are done. |
warmup_min_active_days | integer | yes | |
steps | array of object | yes | |
steps[].step | registration · cnam · attestation · warmup | yes | |
steps[].source | derived · manual | yes | Where the state comes from; manual is the operator's word. |
steps[].state | done · pending | yes | |
steps[].evidence | object | yes | derived: {origin, ...} with the values behind the state (dids.attestation + value; cdr + active_days, required_days, dials). manual: the latest {confirmed_at, note}, empty if never confirmed. |
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 (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"
}
}Confirm (or un-confirm) a manual launch step
http
POST /v1/dids/{id}/launch-checklist/{step}/confirmationsAppends a confirmation for a manual step (registration or cnam); the latest one is the step's state and none is ever edited or deleted. A derived step (attestation, warmup) is 422: the platform computes it. An unknown step is 404. Returns the fresh checklist.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
step | path | registration · cnam · attestation · warmup | yes |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
confirmed | boolean | yes | |
note | string | no |
Responses
201
The checklist after the confirmation.
| Field | Type | Required | Description |
|---|---|---|---|
did_id | string (uuid) | yes | |
e164 | string | yes | |
complete | boolean | yes | All four steps are done. |
warmup_min_active_days | integer | yes | |
steps | array of object | yes | |
steps[].step | registration · cnam · attestation · warmup | yes | |
steps[].source | derived · manual | yes | Where the state comes from; manual is the operator's word. |
steps[].state | done · pending | yes | |
steps[].evidence | object | yes | derived: {origin, ...} with the values behind the state (dids.attestation + value; cdr + active_days, required_days, dials). manual: the latest {confirmed_at, note}, empty if never confirmed. |
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 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (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"
}
}Carrier tickets of a number
http
GET /v1/dids/{id}/carrier-ticketsTickets the tenant keeps about this number with its carrier, most recently active first, each with its full append-only thread. A retired number still serves its tickets.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The tickets.
| Field | Type | Required | Description |
|---|---|---|---|
tickets | array of DidCarrierTicket | yes | |
tickets[].id | string (uuid) | yes | |
tickets[].did_id | string (uuid) | yes | |
tickets[].carrier | string | yes | |
tickets[].subject | string | yes | |
tickets[].reference | string | yes | |
tickets[].status | open · in_progress · resolved · closed | yes | |
tickets[].opened_at | string (date-time) | yes | |
tickets[].updated_at | string (date-time) | yes | |
tickets[].events | array of object | yes | The append-only thread, oldest first; the first has kind opened. |
tickets[].events[].id | string (uuid) | yes | |
tickets[].events[].kind | opened · update | yes | |
tickets[].events[].status | open · in_progress · resolved · closed | yes | |
tickets[].events[].note | string | yes | |
tickets[].events[].created_at | string (date-time) | yes |
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 (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"
}
}Open a carrier ticket for a number
http
POST /v1/dids/{id}/carrier-ticketsParameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
carrier | string | yes | |
subject | string | yes | |
reference | string | no | The carrier's own ticket reference, if it gave one. |
note | string | no |
Responses
201
The opened ticket (status open).
| Field | Type | Required | Description |
|---|---|---|---|
ticket | object (DidCarrierTicket) | yes | |
ticket.id | string (uuid) | yes | |
ticket.did_id | string (uuid) | yes | |
ticket.carrier | string | yes | |
ticket.subject | string | yes | |
ticket.reference | string | yes | |
ticket.status | open · in_progress · resolved · closed | yes | |
ticket.opened_at | string (date-time) | yes | |
ticket.updated_at | string (date-time) | yes | |
ticket.events | array of object | yes | The append-only thread, oldest first; the first has kind opened. |
ticket.events[].id | string (uuid) | yes | |
ticket.events[].kind | opened · update | yes | |
ticket.events[].status | open · in_progress · resolved · closed | yes | |
ticket.events[].note | string | yes | |
ticket.events[].created_at | string (date-time) | yes |
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 (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"
}
}Add a note and/or move a carrier ticket's status
http
POST /v1/dids/{id}/carrier-tickets/{ticket_id}/eventsAppends to the ticket's thread; nothing is edited or deleted. status defaults to the current one. An event with neither a note nor a status change is 400.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
ticket_id | path | string (uuid) | yes | Ticket UUID. It must belong to the number in the path, else 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
status | open · in_progress · resolved · closed | no | |
note | string | no |
Responses
201
The ticket with the new event appended.
| Field | Type | Required | Description |
|---|---|---|---|
ticket | object (DidCarrierTicket) | yes | |
ticket.id | string (uuid) | yes | |
ticket.did_id | string (uuid) | yes | |
ticket.carrier | string | yes | |
ticket.subject | string | yes | |
ticket.reference | string | yes | |
ticket.status | open · in_progress · resolved · closed | yes | |
ticket.opened_at | string (date-time) | yes | |
ticket.updated_at | string (date-time) | yes | |
ticket.events | array of object | yes | The append-only thread, oldest first; the first has kind opened. |
ticket.events[].id | string (uuid) | yes | |
ticket.events[].kind | opened · update | yes | |
ticket.events[].status | open · in_progress · resolved · closed | yes | |
ticket.events[].note | string | yes | |
ticket.events[].created_at | string (date-time) | yes |
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 (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"
}
}