Fiatside

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.

01

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.

Eventos
TipoQué significa
order.createdLa orden se crea y el tipo de cambio queda fijado. Se asigna la dirección de depósito.
order.deposit_detectedSe 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.confirmingEl contador de confirmaciones está avanzando. Se emite en cada paso, no en cada bloque.
order.deposit_confirmedSe alcanza el número de confirmaciones requerido por la red.
order.underpaidEl 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.overpaidEl importe recibido supera el esperado. El importe inicial se mantiene al tipo fijado; el exceso se gestiona por separado.
order.payout_sentLa transferencia ha salido por el canal. Cuidado: enviado no es recibido — el retraso restante depende del banco del beneficiario.
order.completedEl canal confirma la liquidación. Estado terminal.
order.payout_failedEl 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.refundedLos fondos han sido devueltos, solo a la dirección de origen, netos de comisiones de red.
02

La carga útil

Un sobre constante, sea cual sea el tipo.

Ejemplojson
{
  "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"
  }
}
03

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-Signaturehttp
Fiatside-Signature: t=<timestamp unix>,v1=<hex hmac-sha256>
Verificación, Node.jstypescript
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.
04

Reintentos y orden

Cualquier estado que no sea 2xx, o ninguna respuesta, provoca un reintento según el calendario siguiente.

Reintentos y orden
IntentoRetraso
1inmediato
230 s
32 min
410 min
51 h
66 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.