Webhooks
Reciba en su sistema el resultado de cada comprobante ante el SRI, sin consultar la API: registro de endpoints, eventos, formato y verificación de la firma.
La emisión de comprobantes es asíncrona: al crear una factura, la API responde 202 Accepted con el
comprobante en camino al SRI, y el resultado llega después. Los webhooks le avisan a su sistema en cuanto
ese resultado existe, sin que tenga que consultar la API una y otra vez.
¿Webhooks o consultas?
Puede usar cualquiera de los dos. Consultar la URL de la cabecera Location funciona sin configurar nada;
los webhooks evitan las consultas repetidas y le avisan también de los comprobantes emitidos desde la
aplicación o desde su tienda WooCommerce.
Registrar un endpoint
Su endpoint debe ser una URL https accesible desde internet. Puede registrarlo de dos formas:
- Desde la aplicación, en Configuración → Integraciones → Webhooks. Allí también puede pausarlo, enviarle un evento de prueba y revisar o reenviar sus entregas.
- Con la API, usando una credencial con el scope
webhooks:write:
curl -X POST https://api.factunexo.com/v1/ec/webhooks \
-H "X-API-Key: ak_..." \
-H "X-API-Secret: ..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://su-sistema.com/factunexo/webhooks",
"description": "ERP producción",
"events": "receipt.authorized,receipt.rejected,receipt.returned,receipt.not_authorized"
}'events es una lista separada por comas. Si la omite o envía *, recibe todos los eventos.
La respuesta incluye el secret del endpoint:
{
"requestId": "...",
"timestamp": "...",
"data": {
"id": "5f0c2a1e-8b3d-4c1a-9e2f-7a6b5c4d3e2f",
"url": "https://su-sistema.com/factunexo/webhooks",
"description": "ERP producción",
"events": "receipt.authorized,receipt.rejected,receipt.returned,receipt.not_authorized",
"active": true,
"secret": "whsec_..."
}
}Guarde el secreto ahora
El secret se muestra una sola vez, en esta respuesta. No es posible volver a consultarlo. Si lo
pierde, elimine el endpoint y regístrelo de nuevo.
Para listar sus endpoints use GET /v1/ec/webhooks (scope webhooks:read), y para eliminar uno,
DELETE /v1/ec/webhooks/{id}.
Probar la integración
En Configuración → Integraciones → Webhooks, el botón Enviar prueba manda a su endpoint un evento
webhook.test, firmado igual que los reales. Úselo para comprobar que su sistema recibe el aviso y valida
la firma antes de emitir comprobantes:
{
"type": "webhook.test",
"timestamp": "2026-09-14T15:04:05Z",
"data": {
"message": "Evento de prueba enviado desde Factunexo.",
"webhookEndpointId": "5f0c2a1e-8b3d-4c1a-9e2f-7a6b5c4d3e2f"
}
}Su sistema debe responder 2xx e ignorar este evento en su lógica de negocio. El evento de prueba no se
envía a los endpoints pausados y no forma parte de los eventos a los que se suscribe.
Eventos
Solo se notifican los resultados del trámite ante el SRI. Los estados intermedios —en camino, recibido, en proceso— no generan avisos.
| Evento | Cuándo ocurre | Qué hacer |
|---|---|---|
receipt.authorized | El SRI autorizó el comprobante. | Es un documento tributario válido. |
receipt.returned | El SRI lo devolvió en la recepción (por ejemplo, un error de formato o de datos). | Revisar el motivo, corregir y volver a emitir. |
receipt.rejected | El SRI lo rechazó. | Revisar el motivo, corregir y volver a emitir. |
receipt.not_authorized | El SRI lo recibió pero no lo autorizó. | Revisar el motivo, corregir y volver a emitir. |
El motivo detallado de una devolución o un rechazo está en los eventos del comprobante, que puede consultar con la API.
Formato de la entrega
Cada aviso es un POST a su endpoint con un cuerpo JSON y tres cabeceras. El formato sigue la
especificación abierta Standard Webhooks, para la que existen
librerías de verificación en la mayoría de lenguajes.
POST /factunexo/webhooks HTTP/1.1
Content-Type: application/json
webhook-id: 3b8f1c2d-6a4e-4f7b-9c1d-2e3f4a5b6c7d
webhook-timestamp: 1789412345
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4={
"type": "receipt.authorized",
"timestamp": "2026-09-14T15:32:25.184Z",
"data": {
"receiptId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"receiptType": "INVOICE",
"status": "SRI_AUTHORIZED",
"fullSequential": "001-001-000001010",
"authorizationNumber": "1409202601179214673900110010010000010101234567811",
"emissionDate": "2026-09-14",
"authorizationDate": "2026-09-14T15:32:21Z"
}
}| Cabecera | Contenido |
|---|---|
webhook-id | Identificador único del aviso. Es el mismo en cada reintento del mismo aviso. |
webhook-timestamp | Momento del envío, en segundos desde 1970 (UTC). |
webhook-signature | Firma del aviso, en la forma v1,<firma en Base64>. |
Verificar la firma
Verifique la firma antes de actuar sobre el aviso. Sin esa verificación, cualquiera que conozca la URL de su endpoint podría enviarle avisos falsos.
Obtenga la clave
Quite el prefijo whsec_ del secreto y decodifique el resto en Base64. El resultado son los bytes de la
clave.
Arme el contenido firmado
Concatene webhook-id, un punto, webhook-timestamp, un punto, y el cuerpo tal como llegó:
{webhook-id}.{webhook-timestamp}.{cuerpo}.
Calcule y compare
Calcule un HMAC-SHA256 del contenido con la clave, codifíquelo en Base64 y compárelo con la parte que
sigue a v1, en la cabecera. Use una comparación de tiempo constante.
Descarte avisos viejos
Rechace el aviso si webhook-timestamp difiere en más de 5 minutos de su hora actual. El timestamp forma
parte de lo firmado, así que no puede alterarse sin invalidar la firma.
Use el cuerpo sin procesar
La firma se calcula sobre los bytes exactos del cuerpo. Si su framework lo interpreta como JSON y usted lo vuelve a serializar, el orden o los espacios pueden cambiar y la firma dejará de coincidir.
import crypto from 'node:crypto';
// rawBody: el cuerpo tal como llegó, como string (por ejemplo, con express.raw()).
export function verifyWebhook(secret, headers, rawBody) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signatures = headers['webhook-signature'];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
throw new Error('Aviso fuera de la ventana de tiempo');
}
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = crypto
.createHmac('sha256', key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest('base64');
const valid = signatures.split(' ').some((part) => {
const [version, signature] = part.split(',');
return (
version === 'v1' &&
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
});
if (!valid) throw new Error('Firma inválida');
return JSON.parse(rawBody);
}Responder y reintentos
Responda con cualquier código 2xx en menos de 15 segundos. Cualquier otra respuesta, un timeout o un error de conexión se consideran fallidos, y el aviso se reintenta con esperas crecientes: a los 2 minutos, 4, 8, 16, y así hasta un máximo de 6 horas entre intentos. Tras 12 intentos fallidos —poco más de un día— el aviso queda como fallido.
Mientras un endpoint está pausado no recibe avisos; los pendientes salen cuando lo vuelve a activar.
En la aplicación, Entregas muestra cada aviso con su estado, el número de intentos, el último código
HTTP y el error. Desde allí puede reenviar un aviso entregado o fallido: sale con el mismo
webhook-id, así que si su sistema ya lo había procesado lo reconoce como repetido.
Responda primero, procese después
Si procesar el aviso toma tiempo, guárdelo, responda 200 y hágalo en segundo plano. Una respuesta lenta
cuenta como fallida y provoca reintentos.
Avisos repetidos y fuera de orden
La entrega es al menos una vez: un mismo aviso puede llegar más de una vez, por ejemplo si su endpoint
lo procesó pero la respuesta se perdió. Registre los webhook-id que ya procesó y descarte los repetidos.
Tampoco se garantiza el orden de llegada. Si necesita el estado vigente de un comprobante, consúltelo con la API en lugar de deducirlo del orden de los avisos.