Developers
Webhooks
Un webhook le indica que un pedido cambió de estado. No sustituye a la lectura del pedido: es una notificación, no una fuente de verdad.
Contrato publicado, aún no abierto
Los webhooks no se emiten hoy. Su forma, firma y política de reintentos están definidas y publicadas aquí para que su receptor pueda escribirse y probarse con antelación.
Eventos
Cada evento lleva el mismo sobre: un id, un tipo, una marca de tiempo y un objeto de datos. Suscríbase a los tipos que maneja e ignore silenciosamente el resto: se añadirán nuevos tipos, y un receptor que falla con un tipo desconocido se rompe a sí mismo.
| Tipo | Qué significa |
|---|---|
order.created | La orden se crea y el tipo de cambio queda fijado. Se asigna la dirección de depósito. |
order.deposit_detected | Se ve una transacción entrante en la red, antes de la confirmación. Use este evento para tranquilizar al usuario, nunca para entregar nada. |
order.confirming | El contador de confirmaciones está avanzando. Se emite en cada paso, no en cada bloque. |
order.deposit_confirmed | Se alcanza el número de confirmaciones requerido por la red. |
order.underpaid | El importe recibido está por debajo del esperado más allá de la tolerancia. La orden no se pierde: queda a la espera de una elección entre completar, continuar con el importe recibido o un reembolso. |
order.overpaid | El importe recibido supera el esperado. El importe inicial se mantiene al tipo fijado; el exceso se gestiona por separado. |
order.payout_sent | La transferencia ha salido por el canal. Cuidado: enviado no es recibido — el retraso restante depende del banco del beneficiario. |
order.completed | El canal confirma la liquidación. Estado terminal. |
order.payout_failed | El canal rechazó o devolvió la transferencia, con su motivo. No se realiza ningún reintento automático en una devolución bancaria: la causa casi siempre está en los detalles. |
order.refunded | Los fondos han sido devueltos, solo a la dirección de origen, netos de comisiones de red. |
La carga útil
Un sobre constante, sea cual sea el tipo.
{
"id": "evt_01J9ZQF3K8N2M4X7",
"type": "order.payout_sent",
"created": 1788356981,
"data": {
"reference": "K7Q4-M2XB",
"state": "PAYOUT_SENT",
"railId": "sepa_instant",
"netMinor": 91228,
"currency": "EUR",
"sentAt": "2026-09-02T12:21:44Z"
}
}Firma
Cada entrega lleva una cabecera de firma: una marca de tiempo y un HMAC-SHA256 calculado sobre esa marca de tiempo, un punto y el CUERPO DE LA SOLICITUD en bruto.
Fiatside-Signature: t=<timestamp unix>,v1=<hex hmac-sha256>import { createHmac, timingSafeEqual } from 'node:crypto'
// IMPORTANT : le corps doit etre le corps BRUT, avant tout parsing JSON.
// Re-serialiser l'objet change l'ordre des cles et invalide la signature.
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=') as [string, string]))
const timestamp = Number(parts['t'])
const signature = parts['v1']
if (!timestamp || !signature) return false
// Rejeu : une signature valide capturee hier ne doit pas etre rejouable aujourd'hui.
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(signature, 'hex')
// Comparaison a temps constant : un === laisse fuiter la signature octet par octet.
return a.length === b.length && timingSafeEqual(a, b)
}- Firme el cuerpo en bruto, antes de cualquier análisis. Re-serializar el objeto JSON cambia el orden de las claves y los espacios en blanco: la firma ya no coincidirá y buscará durante mucho tiempo.
- Compare en tiempo constante. Una comparación === se detiene en el primer byte diferente, lo que permite a un atacante medir la firma esperada byte a byte.
- Rechace más allá de la ventana de tolerancia de 300 segundos. Sin esa comprobación, una solicitud válida capturada ayer sigue siendo reproducible hoy.
- Responda 200 antes de procesar. Ponga el evento en su cola y manéjelo después: un procesamiento largo provoca un tiempo de espera y, por tanto, un reintento, aunque haya recibido el evento.
Reintentos y orden
Cualquier estado que no sea 2xx, o ninguna respuesta, provoca un reintento según el calendario siguiente.
| Intento | Retraso |
|---|---|
| 1 | inmediato |
| 2 | 30 s |
| 3 | 2 min |
| 4 | 10 min |
| 5 | 1 h |
| 6 | 6 h |
- El orden de llegada no está garantizado. Un evento de pago puede llegar antes que el evento de confirmación que lo precede lógicamente: confíe en el estado del pedido, no en el orden de recepción.
- El mismo evento puede llegar dos veces. Almacene el id del evento e ignore un id ya gestionado: la idempotencia en su receptor es su responsabilidad, y es fácil de conseguir.
- Nunca confíe en los importes del payload para acreditar nada en su lado. Vuelva a leer la orden a través de la API: el payload dice que algo ha ocurrido, la API dice exactamente qué.
- Tras el último intento, el evento se descarta y permanece visible en su registro de eventos. Puede reproducirlo manualmente.