Developers
Webhook
Un webhook ti dice che un ordine ha cambiato stato. Non sostituisce la lettura dell'ordine: è una notifica, non una fonte di verità.
Contratto pubblicato, non ancora aperto
I webhook non vengono emessi oggi. La loro forma, firma e politica di retry sono stabilite e pubblicate qui, così il tuo ricevitore può essere scritto e testato in anticipo.
Eventi
Ogni evento ha lo stesso involucro: un id, un tipo, un timestamp e un oggetto data. Sottoscrivi i tipi che gestisci e ignora silenziosamente gli altri — verranno aggiunti nuovi tipi, e un ricevitore che fallisce su un tipo sconosciuto rompe se stesso.
| Tipo | Cosa significa |
|---|---|
order.created | L'ordine è creato e il tasso è bloccato. L'indirizzo di deposito è assegnato. |
order.deposit_detected | Una transazione in arrivo è vista sulla rete, prima della conferma. Usa questo evento per rassicurare l'utente, mai per consegnare qualcosa. |
order.confirming | Il contatore di conferme sta avanzando. Emesso a ogni passo, non a ogni blocco. |
order.deposit_confirmed | Il numero di conferme richiesto dalla rete è stato raggiunto. |
order.underpaid | L'importo ricevuto è inferiore all'importo previsto oltre la tolleranza. L'ordine non è perso: attende una scelta tra integrazione, continuazione con l'importo ricevuto o rimborso. |
order.overpaid | L'importo ricevuto supera l'importo previsto. L'importo iniziale rimane al tasso bloccato; l'eccedenza è gestita separatamente. |
order.payout_sent | Il trasferimento è partito sul circuito. Attenzione: inviato non è ricevuto — il ritardo residuo dipende dalla banca del beneficiario. |
order.completed | Il regolamento è confermato dal circuito. Stato terminale. |
order.payout_failed | Il circuito ha respinto o restituito il trasferimento, con il suo motivo. Nessun nuovo tentativo automatico viene effettuato su un ritorno bancario: la causa è quasi sempre nei dettagli. |
order.refunded | I fondi sono stati restituiti, solo all'indirizzo di origine, al netto delle commissioni di rete. |
Il payload
Un involucro costante, qualunque sia il 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
Ogni consegna porta un header di firma: un timestamp e un HMAC-SHA256 calcolato su quel timestamp, un punto e il corpo della richiesta RAW.
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)
}- Firma il corpo grezzo, prima di qualsiasi parsing. Ri-serializzare l'oggetto JSON cambia l'ordine delle chiavi e gli spazi bianchi: la firma non corrisponderà più e cercherai a lungo.
- Confronta in tempo costante. Un confronto === si ferma al primo byte diverso, il che consente a un attaccante di misurare la firma attesa byte per byte.
- Rifiuta oltre la finestra di tolleranza di 300 secondi. Senza questo controllo, una richiesta valida catturata ieri rimane riproducibile oggi.
- Rispondi 200 prima di elaborare. Metti l'evento nella tua coda e gestiscilo dopo: un'elaborazione lunga provoca un timeout e quindi un retry, anche se hai ricevuto l'evento.
Retry e ordinamento
Qualsiasi stato non-2xx, o nessuna risposta, attiva un retry secondo la pianificazione seguente.
| Tentativo | Ritardo |
|---|---|
| 1 | immediato |
| 2 | 30 s |
| 3 | 2 min |
| 4 | 10 min |
| 5 | 1 h |
| 6 | 6 h |
- L'ordine di arrivo non è garantito. Un evento di pagamento può arrivare prima dell'evento di conferma che logicamente lo precede: fidati dello stato dell'ordine, non dell'ordine di ricezione.
- Lo stesso evento può arrivare due volte. Conserva l'id dell'evento e ignora un id già gestito: l'idempotenza sul tuo ricevitore è una tua responsabilità, ed è facile da ottenere.
- Non fidarti mai degli importi nel payload per accreditare qualsiasi cosa dalla tua parte. Rileggi l'ordine tramite l'API: il payload dice che qualcosa è successo, l'API dice esattamente cosa.
- Dopo l'ultimo tentativo l'evento viene eliminato e rimane visibile nel tuo registro eventi. Puoi riprodurlo manualmente.