Appearance
Webhooks
English version: Webhooks.
Dialer Digital expone endpoints de webhook públicos y firmados para que los proveedores de SMS envíen a tu cuenta los acuses de entrega y los mensajes entrantes (originados en el móvil):
POST /webhooks/sms/{provider_id}/dlr # delivery receipts
POST /webhooks/sms/{provider_id}/inbound # consumer replies (STOP, HELP, …){provider_id} es el id de un proveedor que registraste en POST /v1/sms/providers. Estos endpoints no llevan clave bearer — cada petición se autentica con una firma criptográfica sobre el cuerpo en bruto, verificada en tiempo constante antes de que el JSON siquiera se analice. Esquemas exactos: referencia de los endpoints de webhook.
Sentido de la comunicación
Estos son endpoints que alojamos nosotros y que llama tu proveedor de SMS. Los webhooks salientes — eventos de la plataforma enviados a tus servidores — son otra superficie, con su propio esquema de firma: consulta Webhooks salientes (integración con CRM).
Paso 0 — registra el proveedor y su secreto
Cuando registras (o actualizas) un proveedor de SMS fijas su webhook_secret. El campo es solo de escritura: las respuestas solo te dicen 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"
}'La respuesta contiene el id del proveedor — tus URL de webhook se derivan de él:
https://api.usa.dialerdigital.com/webhooks/sms/<provider-id>/dlr
https://api.usa.dialerdigital.com/webhooks/sms/<provider-id>/inboundFirmar las peticiones — el contrato del adaptador http
Para los proveedores http (y mock), cada petición de webhook debe llevar una cabecera:
x-dd-signature: <hex of HMAC-SHA256(webhook_secret, raw_body)>Paso a paso:
- Serializa la carga JSON una sola vez y conserva los bytes exactos — la firma cubre el cuerpo en bruto, byte a byte. Volver a serializar (orden de claves, espacios) la rompe.
- Calcula
HMAC-SHA256sobre esos bytes con elwebhook_secretcompartido. - Codifica el resumen en hexadecimal (minúsculas) en
x-dd-signature. - Haz
POSTconContent-Type: application/json.
Ejemplo resuelto
Carga:
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)Proveedores Telnyx — Ed25519, cero código de tu lado
Con adapter: "telnyx" no calculas nada:
- Copia la clave pública Ed25519 de tu cuenta desde el portal de Telnyx (base64).
- Guárdala como
webhook_secretdel proveedor (sí, en las filas de Telnyx la clave pública va en ese campo). - Apunta las URL de webhook de Telnyx a los endpoints
/dlre/inboundde arriba.
La plataforma verifica cada evento exactamente como lo especifica Telnyx: la firma Ed25519 de telnyx-signature-ed25519 se comprueba sobre "{telnyx-timestamp}|{raw_body}" contra tu clave pública, con una ventana anti-repetición de ±5 minutos sobre telnyx-timestamp.
Qué responde el endpoint
| Respuesta | Significado |
|---|---|
200 {"status": "ok"} | Registrado. |
200 {"status": "duplicate"} | Repetición de un evento ya registrado — colapsada sin riesgo (idempotente). |
200 {"status": "ignored"} | Firma verificada pero el evento no se pudo atribuir a un mensaje — se acusa recibo para que el proveedor deje de reintentar. |
401 unauthorized | Firma incorrecta o ausente, o sin secreto configurado. La verificación ocurre antes de cualquier acceso a la base de datos. |
404 not_found | Id de proveedor desconocido, inactivo o mal formado — nunca se filtra la existencia. |
413 | Cuerpo de más de 1 MB. |
500 | Falló la escritura en el libro mayor — reintenta; el endpoint es idempotente, así que repetir es seguro. |
Palabras clave entrantes: las bajas las gestionamos por ti
Las respuestas de los consumidores a /inbound las procesa la propia plataforma (en inglés y español, sin distinguir mayúsculas):
| Palabra clave | Efecto |
|---|---|
STOP, ALTO, UNSUBSCRIBE, CANCEL, QUIT, END; también REVOKE, OPT OUT/OPTOUT/OPT-OUT, una palabra de baja seguida de ALL (STOP ALL) y frases de hasta 6 palabras que contienen una palabra de baja (please stop) | El número pasa a tu lista interna de no contactar — el mismo almacén duradero que leen las compuertas de cumplimiento, así que el siguiente envío a él se bloquea (consent_revoked / dnc_listed). Se revoca el consentimiento y se envía una respuesta de confirmación. |
HELP, AYUDA | Se envía una respuesta de ayuda predefinida. |
START | Nuevo consentimiento (re-opt-in): se vuelve a conceder el consentimiento de SMS y sólo se quita el listado interno que escribió un STOP por SMS (motivo sms_stop_keyword). La baja por voz, tecla, llamada entrante o CSV, y un listado sin motivo registrado, se conservan. Las listas DNC regulatorias nunca se tocan. |
| cualquier otra cosa | Se registra en el libro mayor de SMS y se transmite (enmascarada) al piso en vivo como sms.received. Un mensaje de más de 6 palabras que contiene una palabra de baja no da de baja, pero su frame lleva possible_optout: true. |
No tienes que construir la gestión de bajas para cumplir: las respuestas surten efecto en el almacén de las compuertas antes de que tu código las vea.