Skip to content

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>/inbound

Firmar 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:

  1. 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.
  2. Calcula HMAC-SHA256 sobre esos bytes con el webhook_secret compartido.
  3. Codifica el resumen en hexadecimal (minúsculas) en x-dd-signature.
  4. Haz POST con Content-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:

  1. Copia la clave pública Ed25519 de tu cuenta desde el portal de Telnyx (base64).
  2. Guárdala como webhook_secret del proveedor (sí, en las filas de Telnyx la clave pública va en ese campo).
  3. Apunta las URL de webhook de Telnyx a los endpoints /dlr e /inbound de 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 ​

RespuestaSignificado
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 unauthorizedFirma incorrecta o ausente, o sin secreto configurado. La verificación ocurre antes de cualquier acceso a la base de datos.
404 not_foundId de proveedor desconocido, inactivo o mal formado — nunca se filtra la existencia.
413Cuerpo de más de 1 MB.
500Falló 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 claveEfecto
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, AYUDASe envía una respuesta de ayuda predefinida.
STARTNuevo 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 cosaSe 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.

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