Webhooks
Recibe el resultado de SUNAT para cada comprobante en tiempo real, con firma HMAC y reintentos.
La emisión es asíncrona: POST /invoices responde al instante y SUNAT contesta después.
Los webhooks son la forma de enterarte sin consultar.
Cómo funciona
Registra un endpoint HTTPS con POST /webhook_endpoints, suscríbete a los eventos que te
interesen y responde con 2xx en menos de 5 segundos. Si tu servidor falla, reintentamos
con backoff exponencial hasta 24 horas.
Eventos
| Evento | Cuándo se dispara | Qué hacer |
|---|---|---|
invoice.sent | El XML firmado salió hacia SUNAT. | Nada. Informativo. |
invoice.accepted | SUNAT aceptó el comprobante (CDR 0). | Marca la venta como facturada; descarga el PDF para el cliente. |
invoice.observed | Aceptado con observaciones (CDR 4xxx). | Es válido. Revisa el mensaje y corrige en los siguientes. |
invoice.rejected | SUNAT lo rechazó (CDR 2xxx/3xxx). | Corrige los datos y emite un comprobante nuevo. |
invoice.voided | La comunicación de baja fue aceptada. | Revierte la venta en tu sistema. |
credit_note.accepted | SUNAT aceptó la nota de crédito. | Aplica el ajuste al comprobante afectado. |
credit_note.rejected | SUNAT rechazó la nota. | Revisa el motivo y vuelve a emitirla. |
Formato del envío
POST /webhooks/krezka HTTP/1.1
Content-Type: application/json
Krezka-Event: invoice.accepted
Krezka-Invoice-Id: inv_3xK8p2Qm
Krezka-Signature: t=1762095845,v1=8b7c…e2
Krezka-Attempt: 1{
"id": "evt_5Qp8Lx2m",
"type": "invoice.accepted",
"created_at": "2026-11-02T15:04:09Z",
"data": {
"id": "inv_3xK8p2Qm",
"object": "invoice",
"full_number": "F001-00001234",
"status": "accepted",
"external_id": "INV-10482",
"sunat": { "response_code": "0", "accepted_at": "2026-11-02T15:04:09Z", "cdr_available": true }
}
}El data es el comprobante completo, igual que en GET /invoices/{id}.
Verifica la firma
Cada request incluye la cabecera Krezka-Signature. Calcula el HMAC-SHA256 del
cuerpo crudo con el secret de tu endpoint y compáralo en tiempo constante.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string, secret: string) {
const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Idempotencia del lado tuyo
Un evento puede llegar más de una vez. Usa id del evento para descartar duplicados.
