Webhooks
Verify avisa a tu backend cuando una verificación termina, y cómo fue cada paso de canal. Cada evento va firmado con Standard Webhooks y se reintenta hasta dos días.
Para qué sirven
La respuesta de comprobación te dice si el código es correcto. Pero hay finales que ocurren sin una llamada tuya: la verificación vence o se agotan los canales. El webhook te avisa de esos finales. Si quieres, también te cuenta cómo fue cada paso de canal por el camino.
Eventos
| Evento | Cuándo |
|---|---|
verification.approved | El usuario escribió el código correcto. |
verification.expired | Venció sin una comprobación correcta. |
verification.failed | Terminó sin aprobarse. data.reason dice por qué: max_attempts_reached o cascade_exhausted. |
verification.channel.sent | Un canal aceptó el mensaje. |
verification.channel.delivered | El operador confirmó la entrega. Hoy solo lo reporta SMS. |
verification.channel.failed | Un canal rechazó el envío, o el operador reportó que no pudo entregarlo. |
Recibes exactamente uno de los tres primeros por verificación, cuando llega a su estado final. Los eventos de canal llegan mientras está en curso, uno por envío; un endpoint suscrito a todos los tipos de evento los recibe todos.
Qué recibes
Un POST a tu URL con este cuerpo:
{
"id": "evt_01M2K81GC7W06ZRKDS9K21QNDJ",
"type": "verification.failed",
"occurred_at": "2026-09-29T14:37:05.000Z",
"sequence": null,
"data": {
"verification_id": "ver_01J9XQ4M7K2VBN3P",
"application_id": "app_01J9XQ2B8Y6T4R1M0K9H7G5F3D",
"environment": "live",
"status": "max_attempts_reached",
"reason": "max_attempts_reached",
"client_reference": "signup-8812"
}
}reason solo aparece en verification.failed. client_reference es el valor que enviaste al crear la verificación.
En los eventos de canal, data describe el envío:
{
"id": "evt_01M2K81GC7W06ZRKDS9K21QNDK",
"type": "verification.channel.failed",
"occurred_at": "2026-09-29T14:32:06.000Z",
"sequence": null,
"data": {
"verification_id": "ver_01J9XQ4M7K2VBN3P",
"application_id": "app_01J9XQ2B8Y6T4R1M0K9H7G5F3D",
"environment": "live",
"attempt_id": "atm_01J9XQ4M8A1B2C3D4E5F6G7H8J",
"channel": "whatsapp",
"step": 0,
"attempt": 0,
"send_state": "rejected_definitive"
}
}| Campo | Qué significa |
|---|---|
channel, step | El canal y su posición en la cascada, empezando en 0. |
attempt | 0 en el primer envío de ese paso; cada reenvío suma uno. |
send_state | En channel.sent, siempre accepted. En channel.failed, por qué falló el envío: rejected_definitive, unavailable_transient, channel_unavailable o denied. |
delivery_state | Solo cuando el operador reportó: delivered en channel.delivered; failed_definitive o failed_transient en channel.failed. |
Un channel.failed no significa que la verificación falló: la cascada puede seguir por el siguiente canal. Por correo, un envío incierto también llega como unavailable_transient mientras la verificación sigue pending. client_reference solo viaja en los eventos finales.
Con estos encabezados:
content-type: application/json
webhook-id: evt_01M2K81GC7W06ZRKDS9K21QNDJ
webhook-timestamp: 1790693825
webhook-signature: v1,g0hM9SsE+OTHF9QDLvhRAmMOCQ8Qm9ZQnJwoUCP0+Tk=Verificar la firma
La firma sigue Standard Webhooks: un HMAC-SHA256 en base64 sobre webhook-id.webhook-timestamp.cuerpo. La clave es el secreto sin el prefijo whsec_, decodificado de base64.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function isValidWebhook(rawBody: string, headers: Headers, secret: string): boolean {
const id = headers.get('webhook-id');
const timestamp = headers.get('webhook-timestamp');
const signatures = headers.get('webhook-signature');
if (!id || !timestamp || !signatures) return false;
// Rechaza eventos de hace más de 5 minutos: frena reenvíos maliciosos.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest();
// Durante una rotación llegan dos firmas separadas por un espacio.
return signatures.split(' ').some((candidate) => {
const [version, signature] = candidate.split(',');
if (version !== 'v1' || !signature) return false;
const received = Buffer.from(signature, 'base64');
return received.length === expected.length && timingSafeEqual(received, expected);
});
}# pip install standardwebhooks
from standardwebhooks.webhooks import Webhook
webhook = Webhook(os.environ["VERIFY_WEBHOOK_SECRET"]) # whsec_…
# Lanza una excepción si la firma o la marca de tiempo no son válidas.
event = webhook.verify(raw_body, request.headers)
AtenciónVerifica la firma sobre el cuerpo crudo, tal como llegó. Si tu framework ya lo convirtió a JSON y lo vuelves a serializar, la firma no coincidirá.
Responder y reintentos
- Responde con un
2xxen menos de 5 segundos. Si tienes trabajo pesado, encólalo y responde enseguida. - Un
408,425,429, un5xx, un timeout o un error de red, DNS o TLS se reintenta hasta 7 veces: 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h después del intento anterior, con una variación de ±20 %, unas 45 horas en total. Después, el evento se descarta. Si respondesRetry-After, podemos adelantar el reintento, nunca atrasarlo. - Cualquier otro
3xxo4xxdetiene los reintentos. Las redirecciones no se siguen. - Un mismo evento puede llegar más de una vez: descarta los repetidos por su
webhook-id. - Los eventos pueden llegar desordenados: usa
occurred_at.sequenceestá reservado y por ahora esnull. - Si tu endpoint sigue fallando, las entregas hacia él se pausan un rato, hasta una hora, y se reanudan solas.
- Tu URL debe resolver a una dirección pública; una dirección privada o de loopback se rechaza sin reintentos.
Registrar tu endpoint
Hoy el equipo de Inalambria registra tu URL: debe ser https, y se registra para un solo entorno, live o test. La misma URL puede registrarse una vez por entorno, cada una con su propio secreto. Un endpoint test solo recibe eventos del entorno de pruebas, y cada evento dice su entorno en data.environment. También puedes elegir qué tipos de evento recibe; si no eliges, recibe todos, eventos de canal incluidos.
Al registrarla recibes el secreto whsec_… una sola vez; guárdalo en tu gestor de secretos.
Cuando el secreto rota, durante 24 horas cada evento llega firmado con el secreto nuevo y con el anterior. El código de arriba acepta cualquiera de las dos.
Updated 19 days ago
