Webhooks
Un webhook es una solicitud HTTP que Invoise envía a tu servidor cuando un pago cambia. Puedes empezar con comprobaciones de estado GET y añadir webhooks cuando necesites actualizaciones automáticas.
1. Registra tu endpoint
Sección titulada «1. Registra tu endpoint»curl -X POST 'https://platform.invoise.me/api/v1/shops/{shop_id}/webhooks' \ -H 'Authorization: Bearer ivk_...' \ -H 'Idempotency-Key: <saved-unique-key>' \ -H 'Content-Type: application/json' \ -d '{"url":"https://example.com/invoise","filters":["transfer","invoice","payout"]}'Usa tu propia URL HTTPS pública. Las direcciones locales y privadas se rechazan. Guarda el secret que se devuelve al crear el endpoint: lo necesitas para verificar solicitudes.
Una tienda puede tener hasta 10 endpoints. La URL de un endpoint se puede cambiar, así que reutiliza uno en vez de añadir otro. Consulta Límites.
2. Elige los eventos que necesitas
Sección titulada «2. Elige los eventos que necesitas»| Evento | Significado |
|---|---|
transfer.observed |
Se detectó una transferencia; aún no está confirmada. |
transfer.confirmed |
Una transferencia entrante tiene confirmaciones suficientes. Puede ser solo un pago parcial de la factura. |
transfer.reverted |
Se revirtió un evento de transferencia anterior. |
invoice.closed |
La factura se cerró en la cadena. |
invoice.cancelled |
La página de pago se canceló o la factura caducó; esto no es un reembolso. |
payout.sent |
El pago se transmitió o se detectó su transferencia saliente. |
payout.confirmed |
La contabilidad completa de la liquidación está disponible. |
payout.reverted |
Se revirtió una confirmación de pago anterior. |
payout.failed |
Un fallo terminal del pago necesita intervención. |
Para invoice.cancelled, data.reason indica el motivo: expired cuando se agotó la vigencia de la factura, merchant_blocked cuando el personal de Invoise bloqueó el comercio. Una cancelación por parte del comercio no tiene reason.
Suscríbete por grupo: transfer, invoice, payout. Las transferencias entrantes y los pagos salientes son eventos distintos. Un retraso temporal de RPC o de gas no es payout.failed.
gas.wait y sweep.failed se aceptan como filtros, pero Invoise no los envía a tu endpoint. Son señales internas: gas.wait significa que un pago está esperando fondos para la comisión de red y continúa por su cuenta; sweep.failed significa que falló un intento de mover los fondos a tu destinatario. No necesitas actuar en ninguno de los dos casos. Un pago que no puede completarse te llega como payout.failed.
En una tienda sandbox puedes simular ambos mediante POST /shops/{shop_id}/sandbox/simulate para comprobar que un pago retrasado no rompe tu flujo. Solo cambian el estado del pago y no envían ningún webhook.
3. Verificar antes de procesar
Sección titulada «3. Verificar antes de procesar»Invoise-Signature tiene la forma t=<timestamp>,v1=<hex>. La firma es HMAC-SHA256 de timestamp + "." + raw_body, usando tu secreto de webhook.
El siguiente ejemplo en Node.js también rechaza las marcas de tiempo que difieren más de cinco minutos del reloj del servidor. Mantén ese reloj sincronizado y elige tu tolerancia de forma deliberada.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret) { const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? ''); if (!match) return false; const [, timestamp, signature] = match; const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (!Number.isFinite(age) || age > 300) return false; const expected = createHmac('sha256', secret) .update(timestamp + '.') .update(rawBody) .digest(); return timingSafeEqual(Buffer.from(signature, 'hex'), expected);}Pasa los bytes originales de la solicitud como rawBody. No analices el JSON ni lo conviertas de nuevo en cadena antes de comprobar la firma. Rechaza las firmas no válidas antes de guardar o actuar sobre su contenido.
4. Procesa una sola vez
Sección titulada «4. Procesa una sola vez»Un payload abreviado tiene este aspecto:
{ "id": "<event-id>", "type": "transfer.confirmed", "created_at": "2026-09-19T10:00:00Z", "data": { "issuance_id": "<issuance-id>", "shop_id": "<shop-id>", "external_id": "order-123", "type": "invoice", "invoice_cancelled": false }}| Caso | Manejo |
|---|---|
| Duplicados | Evita duplicados usando Invoise-Event-ID, que coincide con el id del sobre. Guarda el ID y la actualización de negocio de forma atómica. |
| Aceptar un evento | Guarda los eventos aceptados de forma duradera, devuelve 2xx con prontitud y procesa el trabajo en cola después. |
| Reintentos y repeticiones | Los reintentos y las repeticiones manuales repiten el ID del evento. Invoise-Attempt-ID cambia y no es una clave de deduplicación. |
| Orden de los eventos | Los eventos pueden llegar desordenados. Usa su contenido y concilia con el estado actual cuando sea necesario. |
Para las reversiones, chain_event.reference_event_id apunta al evento de cadena anterior. Los payloads de pago incluyen un payout_id estable, el hash de la transacción, los importes y los detalles de la comisión; los importes desconocidos son null. Su lista de ID de transferencias entrantes tiene un tope de 100 y marca el truncamiento.
Los eventos guardados más antiguos pueden usar project_id; lee primero shop_id y recurre a él si es necesario. Las repeticiones conservan los bytes originales.
Inspecciona una entrega fallida
Sección titulada «Inspecciona una entrega fallida»Lista con GET /api/v1/shops/{shop_id}/deliveries. Repite una entrega con POST /api/v1/shops/{shop_id}/deliveries/{id}/replay y una clave de idempotencia guardada. Repetir no crea un nuevo evento de negocio.