Webhooks
Registrar un endpoint, comprobar la firma, y todos los tipos de evento que emite la plataforma.
Los webhooks son la bitácora de eventos, entregada a una dirección tuya, firmada para que tu servicio pueda probar que vino de Nomi, y reintentada cuando tu servicio tiene una mala tarde.
POST/v1/webhook_endpoints
Registra una dirección. El secreto de firma se devuelve una sola vez, aquí.
Registrar
curl -X POST https://api.nomi-tech.com/v1/webhook_endpoints \
-H "Authorization: Bearer nomi_sk_live_EXAMPLE" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example/hooks/nomi",
"description": "Production listener",
"eventTypes": ["credential.*"]
}'Un eventTypes vacío significa todos los eventos. credential.* calza con una familia entera, así que un tipo nuevo no obliga a volver a listarlos.
Comprobar la firma
Cada entrega lleva un encabezado nomi-signature con la forma t=<segundos unix>,v1=<hex>. v1 es un HMAC-SHA256 sobre ${t}.${rawBody} con el secreto del endpoint. La marca de tiempo va dentro del HMAC, así que una firma válida no se puede repetir para siempre.
Un verificador
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string, secret: string) {
const parts = new Map(
header.split(",").map((p) => {
const [k, ...rest] = p.trim().split("=");
return [k, rest.join("=")] as const;
}),
);
const t = parts.get("t");
const v1 = parts.get("v1");
if (!t || !v1) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(v1, "hex");
const mine = Buffer.from(expected, "hex");
if (given.length !== mine.length || !timingSafeEqual(given, mine)) return false;
// Frescura. La firma prueba el origen; la marca de tiempo prueba que no es una repetición.
return Math.abs(Date.now() / 1000 - Number(t)) <= 300;
}Tipos de evento
GET /v1/webhook_event_types devuelve la lista a la que tu cuenta se puede suscribir, que es contra la que hay que programar. Las familias de abajo son las que normalmente quiere una integración:
| Familia | Tipos |
|---|---|
credential.* | created, issued, updated, suspended, resumed, revoked, expired, verified, verify_refused, shared, viewed |
credential.channel.* | provisioned, installed, removed, failed |
subject.* | created, updated, deactivated, deleted |
template.version.* | published, archived, unarchived |
achievement.* | created, updated, version.published, version.archived |
policy.* | applied, halted, failed |
group.* | created, updated, deleted, members_added, members_removed |
webhook.endpoint.* | created, updated, deleted, disabled |
Entrega y fallo
Una entrega que tu servicio no acepta se reintenta, así que una tarde caído no se convierte en un día de eventos perdidos. Lo que a ti te toca está en esta página y en Seguridad de webhooks: comprobar la firma, contestar rápido y hacer idempotente el manejador, porque un reintento significa que vas a ver el mismo evento dos veces.
La mitad operativa —mirar qué se intentó, volver a mandar uno después de arreglar algo, mandar una entrega de prueba y rotar el secreto de firma— se hace desde la consola, y la hace quien administra la organización. Nada de eso necesita un despliegue de tu lado.
Un endpoint que falla una y otra vez termina desactivado, y eso emite webhook.endpoint.disabled — así el silencio es algo de lo que te avisan y no algo que descubres.