Fiatside

Developpeurs

Webhooks

Un webhook vous previent qu’une commande a change d’etat. Il ne remplace pas la lecture de la commande : c’est une notification, pas une source de verite.

Contrat publie, pas encore ouvert

Les webhooks ne sont pas emis aujourd’hui. Leur forme, leur signature et leur politique de reessai sont arretees et publiees ici pour que votre recepteur puisse etre ecrit et teste en avance.

01

Evenements

Tous les evenements portent la meme enveloppe : un identifiant, un type, un horodatage et un objet data. Abonnez-vous aux types que vous traitez, et ignorez silencieusement les autres — de nouveaux types seront ajoutes, et un recepteur qui echoue sur un type inconnu se met en panne tout seul.

Evenements
TypeCe qu’il signifie
order.createdLa commande est creee et le taux verrouille. L’adresse de depot est attribuee.
order.deposit_detectedUne transaction entrante est vue sur le reseau, avant confirmation. C’est l’evenement a utiliser pour rassurer l’utilisateur, jamais pour livrer quoi que ce soit.
order.confirmingLe compteur de confirmations avance. Emis a chaque palier, pas a chaque bloc.
order.deposit_confirmedLe nombre de confirmations exige par le reseau est atteint.
order.underpaidLe montant recu est inferieur au montant attendu au-dela de la tolerance. La commande n’est pas perdue : elle attend un choix entre completer, poursuivre au montant recu, ou etre remboursee.
order.overpaidLe montant recu depasse le montant attendu. Le montant initial reste au taux verrouille ; l’excedent est traite separement.
order.payout_sentLe virement est parti sur le rail. Attention : parti n’est pas recu — le delai restant depend de la banque du beneficiaire.
order.completedLe reglement est confirme par le rail. Etat terminal.
order.payout_failedLe rail a rejete ou retourne le virement, avec son motif. Aucune nouvelle tentative automatique n’est faite sur un retour bancaire : la cause est presque toujours dans les coordonnees.
order.refundedLes fonds ont ete restitues, uniquement vers l’adresse d’origine, frais de reseau deduits.
02

La charge utile

Une enveloppe constante, quel que soit le type.

Exemplejson
{
  "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

Signature

Chaque envoi porte un en-tete de signature : un horodatage et un HMAC-SHA256 calcule sur la concatenation de cet horodatage, d’un point, et du corps BRUT de la requete.

Fiatside-Signaturehttp
Fiatside-Signature: t=<timestamp unix>,v1=<hex hmac-sha256>
Verification, 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)
}
  • Signez le corps brut, avant tout parsing. Re-serialiser l’objet JSON change l’ordre des cles et les espaces : la signature ne correspondra plus, et vous chercherez longtemps.
  • Comparez en temps constant. Une comparaison avec === s’arrete au premier octet different, ce qui laisse mesurer la signature attendue octet par octet.
  • Rejetez au-dela de la fenetre de tolerance de 300 secondes. Sans cette verification, une requete valide capturee hier reste rejouable aujourd’hui.
  • Repondez 200 avant de traiter. Mettez l’evenement dans votre file et traitez-le ensuite : un traitement long declenche un delai d’attente et donc un reessai, alors que vous aviez bien recu l’evenement.
04

Reessais et ordre

Tout code de reponse different de 2xx, ou toute absence de reponse, declenche un reessai selon le calendrier ci-dessous.

Reessais et ordre
TentativeDelai
1immediat
230 s
32 min
410 min
51 h
66 h
  • L’ordre d’arrivee n’est pas garanti. Un evenement de payout peut arriver avant l’evenement de confirmation qui le precede logiquement : fiez-vous a l’etat de la commande, pas a l’ordre de reception.
  • Le meme evenement peut arriver deux fois. Stockez l’identifiant de l’evenement et ignorez un identifiant deja traite : l’idempotence de votre recepteur est votre responsabilite, et elle est simple a obtenir.
  • Ne faites jamais confiance aux montants de la charge utile pour crediter quelque chose chez vous. Relisez la commande par l’API : la charge utile dit qu’il s’est passe quelque chose, l’API dit quoi exactement.
  • Apres la derniere tentative, l’evenement est abandonne et consultable dans votre journal d’evenements. Vous pouvez le rejouer manuellement.