SMS
Generated from
DialerDigital/corecommit6e9f026f0a72— specopenapi/dialer-v1.yamlv0.36.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
Gated one-off SMS send + unified ledger.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/sms/messages | SMS ledger query |
POST | /v1/sms/messages | One-off gated SMS send |
GET | /v1/sms/messages/{id} | Collapsed single-message view |
SMS ledger query
GET /v1/sms/messagesLedger ROWS — originals AND status transitions (corrects_id distinguishes) — newest first, keyset-paginated like /v1/call_attempts. Bodies are 40-char previews (body_preview) in lists.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
direction | query | outbound · inbound | no | |
status | query | queued · sent · delivered · undelivered · failed · received | no | |
campaign_id | query | string (uuid) | no | |
debt_id | query | string (uuid) | no | |
limit | query | integer | no | Page size (default 100, max 1000). |
cursor | query | string | no | Opaque keyset cursor from the previous page's next_cursor. |
Responses
200
One page of ledger rows.
| Field | Type | Required | Description |
|---|---|---|---|
sms_messages | array of SmsMessage | yes | |
sms_messages[].id | string (uuid) | no | |
sms_messages[].direction | outbound · inbound | no | |
sms_messages[].status | queued · sent · delivered · undelivered · failed · received | no | |
sms_messages[].from_e164 | string | null | no | |
sms_messages[].to_e164 | string | no | |
sms_messages[].body_preview | string | null | no | |
sms_messages[].segments | integer | null | no | |
sms_messages[].client_ref | string | null | no | |
sms_messages[].provider_id | string (uuid) | null | no | |
sms_messages[].provider_message_id | string | null | no | |
sms_messages[].campaign_id | string (uuid) | null | no | |
sms_messages[].debt_id | string (uuid) | null | no | |
sms_messages[].occurred_at | string (date-time) | no | |
sms_messages[].corrects_id | string (uuid) | null | no | |
sms_messages[].recorded_at | string (date-time) | no | |
next_cursor | string | null | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}One-off gated SMS send
POST /v1/sms/messagesRoutes ONE message through THE choke point (Dialer.Sms.Sender, the same Originator as campaigns): spend cap → compliance engine (Consent → DNC → QuietHours → SmsFrequency, fail-closed) → evidence-first queued ledger row → provider adapter → sms_segment debit at provider ACCEPT. The API can no more bypass a gate than a campaign can.
client_ref is the caller idempotency key — retries MUST reuse it; a replayed client_ref answers 202 with duplicate: true and the provider is NOT contacted again. When timezone is absent quiet-hours resolves the destination's NPA — unresolvable numbers are blocked fail-closed. Provider resolution walks sede → empresa-wide → deployment config; no provider → 422 sms_provider_unconfigured. A row that resolution selects but whose base_url no longer answers as an https endpoint on port 443 at a public address → 422 sms_provider_base_url_rejected: the SSRF guard re-resolves the host at SEND time (not just at registration), so a name re-pointed at loopback, RFC1918, CGNAT or link-local afterwards is caught here and the send fails closed instead of falling back to another endpoint.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
to | string | yes | Destination, E.164. |
body | string | yes | Message text (include the STOP opt-out notice). |
from | string | no | Sender E.164 / short code. |
client_ref | string | no | Idempotency key (default a fresh uuid). |
debt_id | string (uuid) | no | Attribution (with campaign_id, also lands the durable gate_blocks row on a block). |
campaign_id | string (uuid) | no | |
consumer_ref | string | no | |
timezone | string | no | IANA timezone for quiet-hours; NPA fallback when absent. |
{
"to": "+19995550123",
"body": "postman probe. Responda STOP para no recibir mas mensajes.",
"client_ref": "postman-1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
}Responses
202
Accepted (queued → sent, billed at ACCEPT) — or replayed client_ref (duplicate: true, provider NOT contacted again).
| Field | Type | Required | Description |
|---|---|---|---|
sms_message | SmsMessageView | null | yes | |
duplicate | boolean | yes |
{
"sms_message": {
"id": "0e9f8a7b-6c5d-4e3f-2a1b-0c9d8e7f6a5b",
"direction": "outbound",
"status": "sent",
"from_e164": "+13125550199",
"to_e164": "+19995550123",
"body": "postman probe. Responda STOP para no recibir mas mensajes.",
"segments": 1,
"client_ref": "postman-1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"provider_id": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
"provider_message_id": "mock-0001",
"campaign_id": null,
"debt_id": null,
"compliance_snapshot": {
"decision_id": "2c1d0e9f-8a7b-6c5d-4e3f-2a1b0c9d8e7f"
},
"occurred_at": "2026-07-05T10:00:00Z",
"timestamps": {
"queued": "2026-07-05T10:00:00Z",
"sent": "2026-07-05T10:00:01Z"
},
"transitions": []
},
"duplicate": false
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}422 — ComplianceBlocked
The compliance engine refused the operation (fail-closed). gate names the refusing gate; decision_id cites the audit-chain decision — attach it to any dispute. ZERO provider calls, ZERO ledger rows happened. Do NOT retry unchanged: the block is evidence, not a transient error. (This 422 is distinct from the generic unprocessable 422 and from sms_provider_unconfigured, which share the status code but differ in code.)
{
"error": {
"code": "compliance_blocked",
"message": "the compliance engine refused this send (dnc_listed)",
"gate": "dnc_listed",
"decision_id": "2c1d0e9f-8a7b-6c5d-4e3f-2a1b0c9d8e7f"
}
}429 — SmsRateLimited
Money/rate brakes, both retryable — but with different semantics. codesms_spend_cap_exceeded: month-to-date SMS spend is at the tenant cap; retry only after a cap raise or the month rollover. code throttled: rate brake; retry later with backoff. No Retry-After header is set.
spend_cap
{
"error": {
"code": "sms_spend_cap_exceeded",
"message": "month-to-date SMS spend is at the tenant cap"
}
}throttled
{
"error": {
"code": "throttled",
"message": "SMS send throttled; retry later"
}
}502 — SmsProviderError
The SMS provider rejected the message: the send was queued (evidence-first) and is evidenced as a failed transition — never billed. sms_message_id points at the ledger row.
{
"error": {
"code": "provider_error",
"message": "the SMS provider rejected the message (:timeout)",
"sms_message_id": "0e9f8a7b-6c5d-4e3f-2a1b-0c9d8e7f6a5b"
}
}Collapsed single-message view
GET /v1/sms/messages/{id}The original's identity + CURRENT status, per-status timestamps, frozen compliance_snapshot, full body and the ordered transitions rows.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The collapsed view.
| Field | Type | Required | Description |
|---|---|---|---|
sms_message | object (SmsMessageView) | yes | The COLLAPSED single-message view — current status, per-status timestamps, frozen compliance snapshot, FULL body, ordered transitions. |
sms_message.id | string (uuid) | no | |
sms_message.direction | outbound · inbound | no | |
sms_message.status | queued · sent · delivered · undelivered · failed · received | no | |
sms_message.from_e164 | string | null | no | |
sms_message.to_e164 | string | no | |
sms_message.body | string | null | no | |
sms_message.segments | integer | null | no | |
sms_message.client_ref | string | null | no | |
sms_message.provider_id | string (uuid) | null | no | |
sms_message.provider_message_id | string | null | no | |
sms_message.campaign_id | string (uuid) | null | no | |
sms_message.debt_id | string (uuid) | null | no | |
sms_message.compliance_snapshot | object | null | no | |
sms_message.occurred_at | string (date-time) | no | |
sms_message.timestamps | object | no | Per-status timestamps (e.g. queued, sent, delivered). |
sms_message.transitions | array of SmsMessage | no | |
sms_message.transitions[].id | string (uuid) | no | |
sms_message.transitions[].direction | outbound · inbound | no | |
sms_message.transitions[].status | queued · sent · delivered · undelivered · failed · received | no | |
sms_message.transitions[].from_e164 | string | null | no | |
sms_message.transitions[].to_e164 | string | no | |
sms_message.transitions[].body_preview | string | null | no | |
sms_message.transitions[].segments | integer | null | no | |
sms_message.transitions[].client_ref | string | null | no | |
sms_message.transitions[].provider_id | string (uuid) | null | no | |
sms_message.transitions[].provider_message_id | string | null | no | |
sms_message.transitions[].campaign_id | string (uuid) | null | no | |
sms_message.transitions[].debt_id | string (uuid) | null | no | |
sms_message.transitions[].occurred_at | string (date-time) | no | |
sms_message.transitions[].corrects_id | string (uuid) | null | no | |
sms_message.transitions[].recorded_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).
{
"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).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}