Ir al contenido

Webhooks y sincronización

Un webhook es un aviso HTTP que Boletura envía a tu sistema cuando cambia el estado de un comprobante, para que no tengas que preguntar por cada uno. También puedes trabajar sin webhooks, sincronizando por consulta, y conviene hacerlo de respaldo.

Para muchos RUC, sincroniza de forma incremental en vez de consultar cada UUID: GET /v1/documents con order=updated_at, updated_from y, si quieres, updated_to y company_id. Guarda los UUID ya vistos y avanza la marca solo después de procesar todas las páginas. Solapa dos minutos para absorber cambios concurrentes: en tu sistema, updated_from es tu último updated_at menos dos minutos. Con la boleta de Primeros pasos basta su creación:

Ventana de terminal
UPDATED_FROM=$(campo boleta-respuesta.json created_at)
curl -sS -G -o cambios.json -w '%{http_code}\n' \
-H "Authorization: Bearer $BOLETURA_API_KEY" \
--data-urlencode 'order=updated_at' --data-urlencode "updated_from=$UPDATED_FROM" \
--data-urlencode 'limit=200' "$BOLETURA_BASE_URL/v1/documents"
python -m json.tool cambios.json

Espera 200. Sigue next_cursor con los mismos filtros y el mismo orden; no reutilices el cursor de otro listado. Deduplica por id, actualizando solo con un updated_at igual o posterior. Tolera campos, códigos y estados nuevos en las respuestas.

En el back office, en Configuración → Webhooks, con el modo pruebas activo, registra una URL HTTPS pública de tu servidor y los eventos que quieres (sin ninguno marcado, recibe todos). No se aceptan localhost, redes privadas ni la propia plataforma (app.boletura.com y api.boletura.com). La API key no crea endpoints: lo hace un admin en el back office. Guarda el secreto whsec_… cuando aparece: no se vuelve a mostrar y no es la API key. Los webhooks de test y de live se registran por separado, cada uno con su secreto.

Eventos: document.accepted, document.accepted_with_observations, document.rejected, document.voided, document.void_rejected y document.deadline_missed.

Para comprobar el receptor sin emitir, pulsa Probar en la pantalla del endpoint: llega un webhook.test firmado igual que los demás, con data vacío y sin comprobante, y la pantalla muestra el código y la duración de la respuesta. Respóndele 2xx como a cualquier evento, sin procesarlo como un comprobante; sirve también en live. Para probar el recorrido completo, emite después una factura con SIMULAR OBSERVACIONES. Cada entrega se lista con su comprobante; una que sigue intentándose figura como «Reintentando», con el tipo de error en Respuesta, y Ver intentos muestra cada intento con su hora, su duración, su resultado y el cuerpo enviado. Una entrega fallida se puede Reenviar: sale una vez, con el mismo cuerpo y el mismo X-Boletura-Delivery. Probar y reenviar suman hasta 10 por minuto por cuenta.

POST /tu-ruta HTTP/1.1
Content-Type: application/json
X-Boletura-Event: document.accepted
X-Boletura-Delivery: 0192f2c4-7d1e-7a3b-9c4d-5e6f7a8b9c0d
X-Boletura-Timestamp: 1793433600
X-Boletura-Environment: test
X-Boletura-Signature: v1=64f3ebf3712824645dc472174ac6470783f6a7a0e778f208f10307a35f5ada1b

El cuerpo tiene id (el de la entrega), event, created_at y data.document, el documento completo, como lo devuelve GET /v1/documents/{id}. data.document es el documento al publicarse el aviso, segundos después del evento: si en ese lapso volvió a cambiar, el aviso ya lo muestra con el estado nuevo, y el segundo cambio llega en su propio aviso. El estado vigente es siempre el de data.document, y event dice qué cambio originó el aviso.

  1. Conserva los bytes originales del cuerpo y verifica X-Boletura-Timestamp (hasta 5 minutos de diferencia de reloj) y X-Boletura-Signature antes de leer el JSON. La firma es v1= seguido del HMAC-SHA256 en hexadecimal de timestamp + "." + cuerpo, con el secreto completo en UTF-8 como clave. Compara en tiempo constante.
  2. Comprueba X-Boletura-Environment y guarda el aviso de forma duradera, deduplicando por X-Boletura-Delivery. Responde 2xx en menos de 10 segundos y procésalo después. Un duplicado ya guardado también responde 2xx.
  3. Aplica el estado de data.document, sin suponer que coincide con el nombre del evento ni que los avisos llegan en orden. No sobrescribas un estado más reciente; ante la duda, consulta el UUID.

Cada reintento conserva el cuerpo y el identificador de entrega, y lleva un timestamp firmado nuevo. Toda respuesta que no sea 2xx, un tiempo de espera o una falla de red se reintenta durante 24 horas (1, 5, 15 y 30 minutos; 1, 2 y 4 horas; luego cada 6 horas); no se siguen redirecciones. Desde que una entrega llega a su séptimo intento, unas 4 horas después del primero, y hasta que se agota, cada intento fallido puede avisar a los owner y admin de la cuenta con un correo, como máximo uno por endpoint al día. Pasado ese plazo, la sincronización por consulta recupera los cambios, y una entrega concreta se puede reenviar. Un reenvío repite el estado que tenía el documento cuando se generó la entrega: por eso el paso 3 no sobrescribe un estado más reciente.

delivery_status del documento resume las entregas de su último evento: pending mientras alguna sigue en curso, failed si alguna se agotó o su endpoint se deshabilitó, delivered si todas llegaron, y none mientras no haya ninguna.

Cada programa trae una función para tu receptor y se prueba con un aviso de ejemplo y su firma real: la firma válida pasa y se rechazan un cuerpo alterado, otro secreto y una firma vencida. Al ejecutarlo imprime Todo correcto. El reloj fijo es solo para que la prueba se pueda repetir: en tu receptor usa el reloj actual, tu secreto y las cabeceras que llegaron. Solo usan la biblioteca estándar de su lenguaje.

verify-webhook.sh
#!/bin/sh
# Calcula la firma de un aviso con openssl, para depurar un receptor desde la terminal. No sirve como receptor: la
# comparación no es de tiempo constante y no revisa la diferencia de reloj.
set -eu
# Aviso de ejemplo con su firma real. En tu caso: el cuerpo tal como llegó, tu secreto whsec_… y las cabeceras
# X-Boletura-Timestamp y X-Boletura-Signature.
body='{"id":"0192f2c4-7d1e-7a3b-9c4d-5e6f7a8b9c0d","event":"document.accepted","created_at":"2026-11-09T15:00:00.000000Z","data":{"document":{"customer":{"name":"José Pérez"}}}}'
secret='whsec_ejemplo_solo_para_pruebas'
timestamp='1793433600'
signature='v1=64f3ebf3712824645dc472174ac6470783f6a7a0e778f208f10307a35f5ada1b'
expected="v1=$(printf '%s.%s' "$timestamp" "$body" | openssl dgst -sha256 -hmac "$secret" | sed 's/.*= //')"
if [ "$expected" != "$signature" ]; then
echo "La firma no coincide: se esperaba $expected" >&2
exit 1
fi
echo "Todo correcto: la firma coincide"

La versión de terminal solo calcula la firma con openssl, para depurar: no compara en tiempo constante ni revisa la diferencia de reloj. La prueba verifica la firma y su vencimiento; no reemplaza recibir una entrega real en tu servidor.