Skip to content

Webhook endpoints (provider-facing)

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.

These are the public, signature-authenticated endpoints the platform exposes to SMS providers (delivery receipts and mobile-originated messages). They do not take a bearer key — every request is verified against the per-provider signature before anything else happens. For the verification walkthrough, see Webhooks.

Delivery receipt — POST /webhooks/sms/{provider_id}/dlr

http
POST /webhooks/sms/{provider_id}/dlr

Served at POST /webhooks/sms/{provider_id}/dlr where {provider_id} is a row of /v1/sms/providers. NO bearer auth — per-provider signature over the RAW request body, verified in constant time BEFORE any JSON decode:

  • Internal contract (http/mock adapters): header x-dd-signature = HMAC-SHA256-hex(webhook_secret, raw_body).
  • telnyx adapter: Ed25519 over "{telnyx-timestamp}|{raw_body}" against the row's public key, headers telnyx-signature-ed25519 + telnyx-timestamp, ±5 min replay window; payloads are the Telnyx v2 envelope normalized by the adapter.

Unknown/inactive/non-uuid provider id → 404 (no existence leak); bad/missing signature or no configured secret → 401 with ZERO table touches. Idempotent: DLR replays collapse (200 {"status": "duplicate"}); events that verify but cannot be attributed answer 200 {"status": "ignored"}. A ledger write failure answers 500 so the provider retries. Bodies above 1 MB → 413.

Auth: none — authenticated by request signature (see below).

Parameters

NameInTypeRequiredDescription
x-dd-signatureheaderstringnoInternal contract — HMAC-SHA256 hex over the raw body.
telnyx-signature-ed25519headerstringnoTelnyx — Ed25519 signature (base64).
telnyx-timestampheaderstringnoTelnyx — unix timestamp signed with the body (±5 min window).

Request body (JSON, required)

FieldTypeRequiredDescription
message_idstringyesProvider message id from the send ACCEPT.
statussent · delivered · undelivered · failedyes
json
{
  "message_id": "mock-0001",
  "status": "delivered"
}

Responses

200

Recorded as a compensating status row (or collapsed replay / unattributable event).

FieldTypeRequiredDescription
statusok · duplicate · ignoredyesduplicate = replay collapsed on the ledger's unique indexes; ignored = authenticated but unattributable (logged + counted, not retryable).

ok

json
{
  "status": "ok"
}

duplicate

json
{
  "status": "duplicate"
}

ignored

json
{
  "status": "ignored"
}

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

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

Mobile-originated message — POST /webhooks/sms/{provider_id}/inbound

http
POST /webhooks/sms/{provider_id}/inbound

Served at POST /webhooks/sms/{provider_id}/inbound. Same signature model as the DLR webhook (see smsDeliveryReceipt). Keyword handling (normalized, EN+ES): STOP/ALTO/UNSUBSCRIBE/CANCEL/QUIT/END → internal-DNC listing (the same durable store the gates read — the next send blocks) + consent revocation + canned confirmation reply; HELP/AYUDA → canned help reply; START → re-opt-in (consent re-granted for SMS, internal-DNC listing removed — regulatory lists are never touched); anything else → ledger row + masked sms.received live-floor frame. All idempotent; unattributable events answer 200 {"status": "ignored"}.

Auth: none — authenticated by request signature (see below).

Parameters

NameInTypeRequiredDescription
x-dd-signatureheaderstringnoInternal contract — HMAC-SHA256 hex over the raw body.
telnyx-signature-ed25519headerstringnoTelnyx — Ed25519 signature (base64).
telnyx-timestampheaderstringnoTelnyx — unix timestamp signed with the body (±5 min window).

Request body (JSON, required)

FieldTypeRequiredDescription
fromstringyesConsumer's number, E.164.
tostringyesThe tenant DID that received the message.
bodystringyes
message_idstringyes
json
{
  "from": "+19995550123",
  "to": "+13125550199",
  "body": "STOP",
  "message_id": "mock-mo-0001"
}

Responses

200

Handled (keyword action, ledger row, replay collapse or ignore).

FieldTypeRequiredDescription
statusok · duplicate · ignoredyesduplicate = replay collapsed on the ledger's unique indexes; ignored = authenticated but unattributable (logged + counted, not retryable).

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

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

Outbound — one v1 event POSTed to a registered webhook endpoint (MT-CTI-04)

http
POST (webhook: crmEvent)

The platform is the CALLER here: the body is what a /v1/webhooks endpoint subscribed to the event receives. Phase 2 (core#960, ADR #122) defines the envelope and records one row per (endpoint, event) in the outbox webhook_deliveries, written in the SAME transaction as the domain fact; NOTHING is sent yet — the dispatcher (phase 3, core#961) drains the outbox, signs the body with the endpoint's secret (X-DD-Signature, plus X-DD-Signature-Previous during a rotation window) and retries with a lease.

data is the public projection of the resource, byte-for-byte the shape its GET answers, serialized at the moment of the fact and never rebuilt: call.ended / call.answeredCallAttempt (the finalize correction row for call.ended; the original row with answered_at for call.answered), promise.recordedPromise, sms.received / sms.optoutSmsMessage (list projection, 40-char body_preview), recording.availableRecording. event_id is the receiver's idempotency key: the same fact is never announced twice to the same endpoint.

Request body (JSON, required)

FieldTypeRequiredDescription
eventcall.ended · call.answered · promise.recorded · sms.received · sms.optout · recording.availableyesThe closed v1 catalog of subscribable events (ADR
event_idstringyesIdempotency key, unique per endpoint: dedupe on it. The id of the row the event announces (call.ended: the finalize correction; promise.recorded: the promise; sms.*: the ledger row; recording.available: the recording); call.answered has no row of its own and uses <original attempt id>:answered.
occurred_atstring (date-time)yesWhen the fact happened (the call's ended_at/answered_at, the message's occurred_at, ...), not when it was sent.
tenant_idstring (uuid)yes
dataCallAttempt | Promise | SmsMessage | RecordingyesThe public projection of the resource — the same shape its GET answers.
json
{
  "event": "promise.recorded",
  "event_id": "019968d2-6b1e-7c4a-9b0e-3f2a1c5d7e90",
  "occurred_at": "2026-09-17T20:15:30.123456Z",
  "tenant_id": "5a1c3e7d-2f4b-4c8e-9d1a-0b2c3d4e5f60",
  "data": {
    "id": "019968d2-6b1e-7c4a-9b0e-3f2a1c5d7e90",
    "tenant_id": "5a1c3e7d-2f4b-4c8e-9d1a-0b2c3d4e5f60",
    "debt_id": "019968d1-0000-7000-8000-000000000001",
    "call_attempt_id": null,
    "agent_id": null,
    "amount_cents": 12500,
    "currency": "USD",
    "promised_date": "2026-09-27",
    "status": "pending",
    "note": null,
    "created_at": "2026-09-17T20:15:30.123456Z",
    "updated_at": "2026-09-17T20:15:30.123456Z"
  }
}

Responses

2XX

Acknowledged. Any other status (or a timeout) is retried by the dispatcher (phase 3).

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