Skip to content

SMS

Generated from DialerDigital/core commit 6e9f026f0a72 — spec openapi/dialer-v1.yaml v0.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

MethodPathSummary
GET/v1/sms/messagesSMS ledger query
POST/v1/sms/messagesOne-off gated SMS send
GET/v1/sms/messages/{id}Collapsed single-message view

SMS ledger query

http
GET /v1/sms/messages

Ledger 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

NameInTypeRequiredDescription
directionqueryoutbound · inboundno
statusqueryqueued · sent · delivered · undelivered · failed · receivedno
campaign_idquerystring (uuid)no
debt_idquerystring (uuid)no
limitqueryintegernoPage size (default 100, max 1000).
cursorquerystringnoOpaque keyset cursor from the previous page's next_cursor.

Responses

200

One page of ledger rows.

FieldTypeRequiredDescription
sms_messagesarray of SmsMessageyes
sms_messages[].idstring (uuid)no
sms_messages[].directionoutbound · inboundno
sms_messages[].statusqueued · sent · delivered · undelivered · failed · receivedno
sms_messages[].from_e164string | nullno
sms_messages[].to_e164stringno
sms_messages[].body_previewstring | nullno
sms_messages[].segmentsinteger | nullno
sms_messages[].client_refstring | nullno
sms_messages[].provider_idstring (uuid) | nullno
sms_messages[].provider_message_idstring | nullno
sms_messages[].campaign_idstring (uuid) | nullno
sms_messages[].debt_idstring (uuid) | nullno
sms_messages[].occurred_atstring (date-time)no
sms_messages[].corrects_idstring (uuid) | nullno
sms_messages[].recorded_atstring (date-time)no
next_cursorstring | nullyes

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).

json
{
  "error": {
    "code": "unauthorized",
    "message": "invalid or revoked API key"
  }
}

One-off gated SMS send

http
POST /v1/sms/messages

Routes 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)

FieldTypeRequiredDescription
tostringyesDestination, E.164.
bodystringyesMessage text (include the STOP opt-out notice).
fromstringnoSender E.164 / short code.
client_refstringnoIdempotency key (default a fresh uuid).
debt_idstring (uuid)noAttribution (with campaign_id, also lands the durable gate_blocks row on a block).
campaign_idstring (uuid)no
consumer_refstringno
timezonestringnoIANA timezone for quiet-hours; NPA fallback when absent.
json
{
  "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).

FieldTypeRequiredDescription
sms_messageSmsMessageView | nullyes
duplicatebooleanyes
json
{
  "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).

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).

json
{
  "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.)

json
{
  "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

json
{
  "error": {
    "code": "sms_spend_cap_exceeded",
    "message": "month-to-date SMS spend is at the tenant cap"
  }
}

throttled

json
{
  "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.

json
{
  "error": {
    "code": "provider_error",
    "message": "the SMS provider rejected the message (:timeout)",
    "sms_message_id": "0e9f8a7b-6c5d-4e3f-2a1b-0c9d8e7f6a5b"
  }
}

Collapsed single-message view

http
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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200

The collapsed view.

FieldTypeRequiredDescription
sms_messageobject (SmsMessageView)yesThe COLLAPSED single-message view — current status, per-status timestamps, frozen compliance snapshot, FULL body, ordered transitions.
sms_message.idstring (uuid)no
sms_message.directionoutbound · inboundno
sms_message.statusqueued · sent · delivered · undelivered · failed · receivedno
sms_message.from_e164string | nullno
sms_message.to_e164stringno
sms_message.bodystring | nullno
sms_message.segmentsinteger | nullno
sms_message.client_refstring | nullno
sms_message.provider_idstring (uuid) | nullno
sms_message.provider_message_idstring | nullno
sms_message.campaign_idstring (uuid) | nullno
sms_message.debt_idstring (uuid) | nullno
sms_message.compliance_snapshotobject | nullno
sms_message.occurred_atstring (date-time)no
sms_message.timestampsobjectnoPer-status timestamps (e.g. queued, sent, delivered).
sms_message.transitionsarray of SmsMessageno
sms_message.transitions[].idstring (uuid)no
sms_message.transitions[].directionoutbound · inboundno
sms_message.transitions[].statusqueued · sent · delivered · undelivered · failed · receivedno
sms_message.transitions[].from_e164string | nullno
sms_message.transitions[].to_e164stringno
sms_message.transitions[].body_previewstring | nullno
sms_message.transitions[].segmentsinteger | nullno
sms_message.transitions[].client_refstring | nullno
sms_message.transitions[].provider_idstring (uuid) | nullno
sms_message.transitions[].provider_message_idstring | nullno
sms_message.transitions[].campaign_idstring (uuid) | nullno
sms_message.transitions[].debt_idstring (uuid) | nullno
sms_message.transitions[].occurred_atstring (date-time)no
sms_message.transitions[].corrects_idstring (uuid) | nullno
sms_message.transitions[].recorded_atstring (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).

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"
  }
}

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.