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.
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.
| Type | Ce qu’il signifie |
|---|---|
order.created | La commande est creee et le taux verrouille. L’adresse de depot est attribuee. |
order.deposit_detected | Une 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.confirming | Le compteur de confirmations avance. Emis a chaque palier, pas a chaque bloc. |
order.deposit_confirmed | Le nombre de confirmations exige par le reseau est atteint. |
order.underpaid | Le 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.overpaid | Le montant recu depasse le montant attendu. Le montant initial reste au taux verrouille ; l’excedent est traite separement. |
order.payout_sent | Le virement est parti sur le rail. Attention : parti n’est pas recu — le delai restant depend de la banque du beneficiaire. |
order.completed | Le reglement est confirme par le rail. Etat terminal. |
order.payout_failed | Le 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.refunded | Les fonds ont ete restitues, uniquement vers l’adresse d’origine, frais de reseau deduits. |
La charge utile
Une enveloppe constante, quel que soit le type.
{
"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"
}
}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-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)
}- 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.
Reessais et ordre
Tout code de reponse different de 2xx, ou toute absence de reponse, declenche un reessai selon le calendrier ci-dessous.
| Tentative | Delai |
|---|---|
| 1 | immediat |
| 2 | 30 s |
| 3 | 2 min |
| 4 | 10 min |
| 5 | 1 h |
| 6 | 6 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.