Recibir webhooks y verificar firmas
Ody envía eventos del espacio de trabajo — nuevos mensajes, resultados de llamadas, cambios de contacto — a los puntos finales HTTPS que registras a través de la API, con cada entrega firmada según la especificación de Standard Webhooks para que puedas probar que provienen de Ody.
Antes de empezar
- Necesitas una clave API con el alcance
webhooks:manage— consulta Obtener tu clave API de Ody. - Tu punto final debe ser una URL HTTPS que responda con un estado
2xxrápidamente (las entregas agotan el tiempo de espera después de 10 segundos). - Cada espacio de trabajo puede registrar hasta 50 puntos finales.
Paso a paso
- Enumera los tipos de eventos a los que puedes suscribirte:
curl https://api.ody.co/v1/api/webhook-events \
-H "Authorization: Bearer ody_live_…"
El catálogo actual: message.received, message.delivered, message.failed, call.ringing, call.answered, call.completed, call.forwarded, call.missed, call.recording.completed, call.summary.completed, call.transcript.completed, call.voicemail.completed, call.flow.started, call.flow.completed, call.flow.aborted, contact.updated, contact.deleted. Suscribirse a "*" cubre todos los tipos de eventos actuales y futuros.
Los eventos call.flow.* se activan cuando una llamada es respondida por un flujo de llamadas: started cuando la persona que llama entra en el flujo, completed cuando llega a un destino (con el tipo de destino — timbre, asistente de IA, buzón de voz, desvío o colgar), y aborted si el flujo no pudo finalizar.
- Registra tu punto final:
curl https://api.ody.co/v1/api/webhooks \
-H "Authorization: Bearer ody_live_…" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/ody-webhook","events":["message.received"],"label":"prod"}'
- Guarda el
secretde la respuesta inmediatamente. El secreto de firmawhsec_…se devuelve solo al crear y rotar — nunca se muestra de nuevo. - Envía un evento de prueba firmado y confirma que tu punto final lo verifica y acepta:
curl -X POST https://api.ody.co/v1/api/webhooks/WEBHOOK_ID/test \
-H "Authorization: Bearer ody_live_…"
La prueba publica un evento {"type":"ping"} firmado exactamente como una entrega real.
Cómo se ve una entrega
Cada entrega es un POST con un cuerpo JSON y tres encabezados de firma:
{
"id": "evt_…",
"apiVersion": "2026-06-01",
"createdAt": "2026-08-16T12:00:00.000Z",
"type": "message.received",
"data": { "conversationId": "…", "activityId": "…", "numberE164": "+1…" }
}
| Encabezado | Significado |
|---|---|
webhook-id |
El ID del evento — desduplicar en él, ya que los reintentos lo reutilizan |
webhook-timestamp |
Segundos Unix en el momento del envío |
webhook-signature |
v1,<base64 HMAC-SHA256> sobre <id>.<timestamp>.<raw body> |
Verificación de la firma
La clave HMAC es la parte decodificada en base64 de tu secreto después del prefijo whsec_, y el mensaje firmado es <webhook-id>.<webhook-timestamp>.<raw body>. Siempre verifica contra los bytes de solicitud sin procesar — volver a serializar el JSON analizado rompe la firma. Rechaza las marcas de tiempo de más de unos pocos minutos para bloquear las repeticiones.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
app.post("/ody-webhook", express.raw({ type: "application/json" }), (req, res) => {
const secret = process.env.ODY_WEBHOOK_SECRET; // whsec_…
const id = req.headers["webhook-id"];
const timestamp = req.headers["webhook-timestamp"];
const signature = req.headers["webhook-signature"];
// Replay guard: reject deliveries older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end();
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const expected = "v1," + createHmac("sha256", key)
.update(`${id}.${timestamp}.${req.body}`)
.digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(String(signature));
if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
console.log("verified event:", event.type);
res.status(200).end();
});
Los SDK oficiales incluyen esto como una sola línea — verifyWebhookSignature (TypeScript) y verify_webhook_signature (Python), ambos con comparación de tiempo constante y la protección contra repeticiones de 5 minutos incorporada. Consulta Inicio rápido del SDK: TypeScript y Python.
Reintentos, historial y gestión
- Reintentos: una entrega fallida (no 2xx, tiempo de espera o error de conexión) se reintenta aproximadamente 5 segundos y luego 30 segundos después del primer intento — tres intentos en total, con un tiempo de espera de 10 segundos cada uno.
- Historial:
GET /v1/api/webhooks/:id/deliverieslista las entregas recientes de la más nueva a la más antigua, con códigos de estado, errores y duraciones por intento — tu primera parada cuando parezca que faltan eventos. - Administrar puntos finales:
GET /v1/api/webhookslos lista (nunca el secreto),PATCH /v1/api/webhooks/:idactualizaurl,events,labelostatus(enabled/disabled), yDELETE /v1/api/webhooks/:idelimina uno junto con su historial de entregas. - Rotar el secreto en cualquier momento con
POST /v1/api/webhooks/:id/rotate— el nuevowhsec_…se devuelve una vez, y las firmas antiguas dejan de ser válidas inmediatamente.
Solución de problemas
- La verificación de la firma siempre falla — probablemente estás verificando un cuerpo re-serializado. Usa los bytes de la solicitud sin procesar y confirma que decodificaste en base64 el secreto después de quitar
whsec_. - No llegan entregas — verifica que el
statusdel punto final estéenabled, que su lista deeventsincluya el tipo que esperas (o"*"), y el historial de entregas para ver errores por intento. - Eventos duplicados — los reintentos reutilizan el mismo
webhook-id. Desduplica en él. webhook limit reached— has alcanzado los 50 puntos finales para el espacio de trabajo; elimina uno no utilizado primero.
Artículos relacionados
Preguntas frecuentes
¿Cómo se firman las entregas de webhook?
Según la especificación de Standard Webhooks: un encabezado webhook-signature con el formato v1,<base64 HMAC-SHA256> calculado sobre '<webhook-id>.<webhook-timestamp>.<raw body>' con tu secreto whsec_.
¿Qué sucede si mi punto final está inactivo?
Ody reintenta dos veces — aproximadamente 5 segundos y luego 30 segundos después del primer fallo. Consulta GET /v1/api/webhooks/:id/deliveries para ver los códigos de estado y errores por intento.
Perdí mi secreto de firma, ¿ahora qué?
Rótalo con POST /v1/api/webhooks/:id/rotate. El nuevo secreto whsec_ se devuelve una vez; las firmas antiguas dejan de ser válidas inmediatamente.