Skip to content

Webhooks ​

Versión en español: 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, END; also REVOKE, OPT OUT/OPTOUT/OPT-OUT, a stop word followed by ALL (STOP ALL), and phrases of up to 6 words containing a stop word (please stop)The 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 and only the internal listing an SMS STOP wrote (reason sms_stop_keyword) removed. A voice, keypress, inbound-call or CSV opt-out, and a listing with no recorded reason, stay. Regulatory DNC lists are never touched.
anything elseRecorded in the SMS ledger and streamed (masked) to the live floor as sms.received. A message of more than 6 words that contains a stop word does not unsubscribe, but its frame carries possible_optout: true.

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.