Skip to content

Webhooks

La API de Emitia es asincrónica: enviar una factura a la DIAN puede tardar desde milisegundos hasta minutos (la DIAN tiene incidentes). Los webhooks son el mecanismo para que tu servidor reciba el resultado sin polling.


Terminal window
curl -X POST https://api.emitia.co/v1/webhook-endpoints \
-H "Authorization: Bearer sk_test_TU_CLAVE" \
-H "Content-Type: application/json" \
-H "Emitia-Version: 2026-01-01" \
-d '{
"url": "https://tuapp.co/hooks/emitia",
"subscribed_events": ["invoice.accepted_by_dian", "invoice.rejected_by_dian"],
"description": "Producción"
}'

La respuesta incluye el secret con prefijo whsec_ — guárdalo de forma segura, se muestra solo una vez:

{
"id": "we_01HX…",
"object": "webhook_endpoint",
"url": "https://tuapp.co/hooks/emitia",
"subscribed_events": ["invoice.accepted_by_dian", "invoice.rejected_by_dian"],
"is_active": true,
"livemode": false,
"secret": "whsec_...",
"created_at": "2026-01-15T10:30:00-05:00"
}

{
"id": "evt_01HX…",
"object": "event",
"type": "invoice.accepted_by_dian",
"created": "2026-01-15T10:30:04-05:00",
"livemode": false,
"account": "ten_01HX…",
"data": {
"object": {
"id": "inv_01HX…",
"object": "invoice",
"status": "accepted_by_dian",
"cufe": "fe9c3a1b…",
"pdf_url": "https://files.emitia.co/…"
}
}
}

Emitia emite exactamente 21 event types. Usa estos strings verbatim en subscribed_events.

EventoCuándo
invoice.accepted_by_dianDIAN aceptó la factura
invoice.rejected_by_dianDIAN rechazó (ver dian_errors en el objeto)
invoice.accepted_contingencyEmitida en contingencia Tipo 04
invoice.failedError interno no DIAN
credit_note.accepted_by_dianNota crédito aceptada por DIAN
credit_note.rejected_by_dianNota crédito rechazada por DIAN
credit_note.accepted_contingencyNota crédito en contingencia Tipo 04
credit_note.failedError interno no DIAN
debit_note.accepted_by_dianNota débito aceptada por DIAN
debit_note.rejected_by_dianNota débito rechazada por DIAN
debit_note.accepted_contingencyNota débito en contingencia Tipo 04
debit_note.failedError interno no DIAN
EventoCuándo
invoice.email.deliveredEmail al adquirente entregado
invoice.email.bouncedEmail rebotó (dirección inválida)
invoice.email.complainedAdquirente marcó como spam
credit_note.email.deliveredEmail de nota crédito entregado
credit_note.email.bouncedEmail de nota crédito rebotó
credit_note.email.complainedEmail de nota crédito marcado spam
debit_note.email.deliveredEmail de nota débito entregado
debit_note.email.bouncedEmail de nota débito rebotó
debit_note.email.complainedEmail de nota débito marcado spam

Cada entrega incluye estos headers:

Emitia-Event-Id: evt_01HX…
Emitia-Event-Type: invoice.accepted_by_dian
Emitia-Signature: t=1716480600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Emitia-Webhook-Id: we_01HX…
User-Agent: Emitia/1.0 (+https://emitia.co/webhooks)
Content-Type: application/json

  1. Emitia toma el timestamp de entrega (t) y el cuerpo del request.
  2. Concatena: {t}.{body_raw}.
  3. Calcula HMAC-SHA256 de esa cadena usando tu whsec_....
  4. Envía el resultado en el header Emitia-Signature: t={t},v1={hex}.

Tu servidor debe reproducir el mismo cálculo y comparar con v1 usando comparación en tiempo constante (para evitar timing attacks).

import Emitia from '@emitia/node';
import type { Request, Response } from 'express';
const emitia = new Emitia(process.env.EMITIA_KEY!);
const webhookSecret = process.env.EMITIA_WEBHOOK_SECRET!; // tu whsec_...
export async function webhookHandler(req: Request, res: Response) {
const signature = req.headers['emitia-signature'] as string;
let event;
try {
event = emitia.webhooks.constructEvent(
req.body, // Buffer o string raw (sin parsear)
signature,
webhookSecret,
);
} catch (err) {
console.error('Firma inválida:', err);
return res.status(400).send('Webhook signature verification failed');
}
switch (event.type) {
case 'invoice.accepted_by_dian':
// Actualizar estado en tu base de datos
console.log('Factura aceptada:', event.data.object.id);
break;
case 'invoice.rejected_by_dian':
console.error('Factura rechazada:', event.data.object.id);
break;
}
res.status(200).json({ received: true });
}

Si tu endpoint responde con un código distinto de 2xx, Emitia reintentará la entrega con backoff exponencial:

IntentoEspera
1Inmediato
2~30 segundos
3~5 minutos
4~30 minutos