Webhooks
Un webhook es una petición POST que Emitay hace a tu servidor cuando algo ocurre: SUNAT
aceptó una factura, rechazó una boleta, un correo no se pudo enviar. Es la forma de enterarse
que usa un backend, porque llega aunque SUNAT tarde y aunque tu proceso se haya reiniciado.
Los eventos
Sección titulada «Los eventos»type |
Cuándo | data.object |
|---|---|---|
document.accepted |
SUNAT aceptó el comprobante | El comprobante |
document.observed |
SUNAT lo aceptó con observaciones | El comprobante |
document.rejected |
SUNAT lo rechazó, o venció su plazo sin respuesta | El comprobante |
document.voided |
SUNAT aceptó la anulación | El comprobante |
document.void_failed |
SUNAT rechazó la anulación | El comprobante |
email.sent |
El servidor de correo aceptó el mensaje | El correo |
email.failed |
El correo no se pudo enviar | El correo |
El cuerpo es el evento, con el recurso completo tal como quedó. No hace falta consultarlo después:
{ "id": "evt_…", "object": "event", "type": "document.accepted", "created_at": "2026-10-08T22:32:56.000Z", "livemode": false, "data": { "object": { "id": "doc_…", "object": "document", "series": "F001", "number": 2, "status": "accepted" } }}Probar en tu máquina
Sección titulada «Probar en tu máquina»No necesitas un túnel ni una URL pública. emitay listen sigue los eventos de tu cuenta y los
reenvía a tu servidor local, firmados igual que una entrega real:
npx emitay listen --forward-to http://localhost:3000/webhooks/emitayAl arrancar muestra el secreto con que firma; ponlo en EMITAY_WEBHOOK_SECRET y tu receptor
ya puede verificar. El recorrido completo está en el
inicio rápido.
Verificar la firma
Sección titulada «Verificar la firma»Cualquiera puede hacer un POST a tu URL. La firma prueba que la entrega es de Emitay y que
nadie cambió el cuerpo. Verifica siempre, y con el cuerpo tal como llegó: lo firmado son
sus bytes, no el JSON vuelto a escribir.
import { EmitayWebhookError, verifyWebhook } from "emitay";
// Un route handler de Next.js; con Express, usa express.raw() en esta ruta.export async function POST(request: Request): Promise<Response> { try { const event = await verifyWebhook({ payload: await request.text(), headers: request.headers, secret: process.env.EMITAY_WEBHOOK_SECRET!, }); switch (event.type) { case "document.accepted": console.log(event.data.object.links.pdf); // event.data.object es un Document break; case "document.rejected": console.error(event.data.object.sunat?.description); break; case "email.failed": console.error(event.data.object.last_error); // aquí es un Email break; } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof EmitayWebhookError) return new Response(error.message, { status: 400 }); throw error; }}El type del evento dice qué trae: dentro de cada case, event.data.object ya tiene su
tipo.
Sin el SDK
Sección titulada «Sin el SDK»Las entregas siguen Standard Webhooks, así que cualquier librería de ese estándar las verifica. A mano:
| Cabecera | Contenido |
|---|---|
webhook-id |
El id del evento (evt_…). Es el mismo en cada reintento |
webhook-timestamp |
La hora del intento, en segundos Unix |
webhook-signature |
v1,<firma en base64> |
- Une
webhook-id,webhook-timestampy el cuerpo con puntos:id.timestamp.cuerpo. - Calcula su HMAC-SHA256. La clave son los bytes que resultan de decodificar en base64 el
secreto sin su prefijo
whsec_. - Compara el resultado, en base64, con lo que sigue a
v1,, en tiempo constante. - Rechaza la entrega si
webhook-timestampdifiere de tu reloj en más de cinco minutos.
Responder
Sección titulada «Responder»- Responde
2xxen menos de 10 segundos. Cualquier otra cosa —otro estado, una redirección, ninguna respuesta— cuenta como intento fallido. - Haz el trabajo pesado después de responder: guarda el evento, contesta
204y procésalo en segundo plano. - Un mismo evento puede llegar más de una vez. Guarda su
id(elwebhook-id) y sáltate el que ya procesaste. - No cuentes con el orden. Dos eventos pueden llegar en otro orden del que ocurrieron;
cada uno trae el recurso completo, con su
updated_at.
Reintentos
Sección titulada «Reintentos»Si tu servidor no responde 2xx, Emitay reintenta a los 5 segundos, 5 minutos, 30 minutos,
2 horas, 5 horas, 10 horas y 10 horas más: ocho intentos en total, en algo más de un día.
Después la entrega queda failed.
Cada intento queda registrado con lo que respondió tu servidor:
const entregas = await emitay.webhookEndpoints.deliveries.list("we_…", { status: "failed" });for (const entrega of entregas.data) { console.log(entrega.event_id, entrega.response_status, entrega.last_error); await emitay.webhookEndpoints.retryDelivery(entrega.id); // la reenvía ya}Registrar tu endpoint
Sección titulada «Registrar tu endpoint»Para producción, registra la URL pública de tu receptor. En el panel, en Webhooks, con Agregar endpoint; o por la API:
curl https://api.emitay.com/v1/webhook_endpoints \ -H "Authorization: Bearer $EMITAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://erp.miempresa.pe/webhooks/emitay", "events": ["document.accepted", "document.observed", "document.rejected"] }'$cabeceras = @{ Authorization = "Bearer $env:EMITAY_API_KEY"}$cuerpo = @'{ "url": "https://erp.miempresa.pe/webhooks/emitay", "events": ["document.accepted", "document.observed", "document.rejected"]}'@Invoke-RestMethod -Method Post ` -Uri https://api.emitay.com/v1/webhook_endpoints ` -Headers $cabeceras ` -ContentType "application/json; charset=utf-8" ` -Body $cuerpoimport { Emitay } from "emitay";
const emitay = new Emitay(); // toma la llave de EMITAY_API_KEY
const endpoint = await emitay.webhookEndpoints.create({ url: "https://erp.miempresa.pe/webhooks/emitay", events: ["document.accepted", "document.observed", "document.rejected"],});- La respuesta trae el secreto (
whsec_…). Se muestra esta única vez: guárdalo enEMITAY_WEBHOOK_SECRET. Si lo pierdes,rotate_secretgenera otro e invalida el anterior. eventsson los tipos que quieres recibir; sin él, o con["*"], todos.- La URL debe ser
httpsy pública. Emitay no entrega a redes privadas. - Cada modo tiene sus endpoints: uno creado con una llave de prueba solo recibe eventos de prueba.
- Deshabilitar un endpoint (
enabled: false) detiene sus entregas, también las pendientes.
La referencia está en Webhooks, y lo que llega a tu servidor, en Recursos.