Skip to content

Webhooks

Dialer Digital exposes public, signed webhook endpoints so SMS providers can push delivery receipts and inbound (mobile-originated) messages into your account:

POST /webhooks/sms/{provider_id}/dlr        # delivery receipts
POST /webhooks/sms/{provider_id}/inbound    # consumer replies (STOP, HELP, …)

{provider_id} is the id of a provider you registered at POST /v1/sms/providers. These endpoints take no bearer key — every request is authenticated by a cryptographic signature over the raw body, verified in constant time before the JSON is even parsed. Exact schemas: webhook endpoint reference.

Direction of travel

These are endpoints we host and your SMS provider calls. Outbound webhooks — platform events pushed to your servers — are a separate surface with its own signature scheme: see Outbound webhooks (CRM integration).

Step 0 — register the provider and its secret

When you register (or update) an SMS provider you set its webhook_secret. The field is write-only: responses only ever tell you has_secret: true.

bash
curl -s -X POST $API/v1/sms/providers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "adapter": "http",
    "region": "us",
    "name": "my-sms-gateway",
    "base_url": "https://gateway.example.com/send",
    "webhook_secret": "example-shared-secret-not-real"
  }'

The response contains the provider id — your webhook URLs are derived from it:

https://api.usa.dialerdigital.com/webhooks/sms/<provider-id>/dlr
https://api.usa.dialerdigital.com/webhooks/sms/<provider-id>/inbound

Signing requests — the http adapter contract

For http (and mock) providers, each webhook request must carry one header:

x-dd-signature: <hex of HMAC-SHA256(webhook_secret, raw_body)>

Step by step:

  1. Serialize the JSON payload once and keep the exact bytes — the signature covers the raw body, byte for byte. Re-serializing (key order, whitespace) breaks it.
  2. Compute HMAC-SHA256 over those bytes with the shared webhook_secret.
  3. Hex-encode the digest (lowercase) into x-dd-signature.
  4. POST with Content-Type: application/json.

Worked example

Payload:

json
{"message_id":"prov-000123","status":"delivered"}

Shell:

bash
BODY='{"message_id":"prov-000123","status":"delivered"}'
SECRET='example-shared-secret-not-real'

SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -s "https://api.usa.dialerdigital.com/webhooks/sms/$PROVIDER_ID/dlr" \
  -H "Content-Type: application/json" \
  -H "x-dd-signature: $SIG" \
  --data-raw "$BODY"

Node.js:

js
import { createHmac } from 'node:crypto'

const body = JSON.stringify({ message_id: 'prov-000123', status: 'delivered' })
const signature = createHmac('sha256', process.env.WEBHOOK_SECRET)
  .update(body, 'utf8')
  .digest('hex')

await fetch(`https://api.usa.dialerdigital.com/webhooks/sms/${providerId}/dlr`, {
  method: 'POST',
  headers: { 'content-type': 'application/json', 'x-dd-signature': signature },
  body
})

Python:

python
import hashlib, hmac, json, os, urllib.request

body = json.dumps({"message_id": "prov-000123", "status": "delivered"}).encode()
sig = hmac.new(os.environ["WEBHOOK_SECRET"].encode(), body, hashlib.sha256).hexdigest()

req = urllib.request.Request(
    f"https://api.usa.dialerdigital.com/webhooks/sms/{provider_id}/dlr",
    data=body,
    headers={"Content-Type": "application/json", "x-dd-signature": sig},
)
urllib.request.urlopen(req)

Telnyx providers — Ed25519, zero code on your side

For adapter: "telnyx" you do not compute anything:

  1. Copy your account's Ed25519 public key from the Telnyx portal (base64).
  2. Store it as the provider's webhook_secret (yes, the public key goes in that field for Telnyx rows).
  3. Point Telnyx's webhook URLs at the /dlr and /inbound endpoints above.

The platform verifies each event exactly as Telnyx specifies: the Ed25519 signature in telnyx-signature-ed25519 is checked over "{telnyx-timestamp}|{raw_body}" against your public key, with a ±5 minute replay window on telnyx-timestamp.

What the endpoint answers

ResponseMeaning
200 {"status": "ok"}Recorded.
200 {"status": "duplicate"}Replay of an already-recorded event — safely collapsed (idempotent).
200 {"status": "ignored"}Signature verified but the event could not be attributed to a message — acknowledged so the provider stops retrying.
401 unauthorizedBad or missing signature, or no secret configured. Verification happens before any database access.
404 not_foundUnknown, inactive or malformed provider id — existence is never leaked.
413Body over 1 MB.
500The ledger write failed — retry; the endpoint is idempotent, so replays are safe.

Inbound keywords: opt-outs handled for you

Consumer replies to /inbound are processed by the platform itself (English and Spanish, case-insensitive):

KeywordEffect
STOP, ALTO, UNSUBSCRIBE, CANCEL, QUIT, ENDThe number is placed on your internal do-not-contact list — the same durable store the compliance gates read, so the very next send to it blocks (consent_revoked / dnc_listed). Consent is revoked and a confirmation reply is sent.
HELP, AYUDAA canned help reply is sent.
STARTRe-opt-in: SMS consent re-granted, internal listing removed. Regulatory DNC lists are never touched.
anything elseRecorded in the SMS ledger and streamed (masked) to the live floor as sms.received.

You do not have to build opt-out handling to be compliant — replies take effect in the gate store before your code ever sees them.

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