Developers
Webhooks
Um webhook informa que um pedido mudou de estado. Ele não substitui a leitura do pedido: é uma notificação, não uma fonte de verdade.
Contrato publicado, ainda não aberto
Webhooks não são emitidos hoje. Sua forma, assinatura e política de nova tentativa estão definidas e publicadas aqui para que seu receptor possa ser escrito e testado antecipadamente.
Eventos
Cada evento carrega o mesmo envelope: um id, um tipo, um timestamp e um objeto de dados. Assine os tipos que você trata e ignore silenciosamente o resto — novos tipos serão adicionados, e um receptor que falha em um tipo desconhecido quebra a si mesmo.
| Tipo | O que significa |
|---|---|
order.created | A ordem é criada e a taxa é bloqueada. O endereço de depósito é atribuído. |
order.deposit_detected | Uma transação recebida é vista na rede, antes da confirmação. Use este evento para tranquilizar o usuário, nunca para entregar qualquer coisa. |
order.confirming | O contador de confirmações está progredindo. Emitido a cada etapa, não a cada bloco. |
order.deposit_confirmed | O número de confirmações exigido pela rede é atingido. |
order.underpaid | O valor recebido está abaixo do valor esperado além da tolerância. A ordem não está perdida: ela aguarda uma escolha entre complementar, continuar com o valor recebido ou um reembolso. |
order.overpaid | O valor recebido excede o valor esperado. O valor inicial permanece na taxa bloqueada; o excesso é tratado separadamente. |
order.payout_sent | A transferência saiu no canal. Cuidado: enviado não é recebido — o atraso restante depende do banco do beneficiário. |
order.completed | A liquidação é confirmada pelo canal. Estado terminal. |
order.payout_failed | O canal rejeitou ou devolveu a transferência, com seu motivo. Nenhuma nova tentativa automática é feita em uma devolução bancária: a causa está quase sempre nos detalhes. |
order.refunded | Os fundos foram devolvidos, somente para o endereço de origem, líquidos de taxas de rede. |
O payload
Um envelope constante, qualquer que seja o 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"
}
}Assinatura
Cada entrega carrega um cabeçalho de assinatura: um timestamp e um HMAC-SHA256 calculado sobre esse timestamp, um ponto e o corpo bruto da solicitação.
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)
}- Assine o corpo bruto, antes de qualquer análise. Re-serializar o objeto JSON muda a ordem das chaves e os espaços em branco: a assinatura não corresponderá mais, e você procurará por um longo tempo.
- Compare em tempo constante. Uma comparação === para no primeiro byte diferente, o que permite que um atacante meça a assinatura esperada byte a byte.
- Rejeite além da janela de tolerância de 300 segundos. Sem essa verificação, uma solicitação válida capturada ontem permanece reproduzível hoje.
- Responda 200 antes de processar. Coloque o evento na sua fila e trate-o depois: processamento longo aciona um timeout e, portanto, uma nova tentativa, mesmo que você tenha recebido o evento.
Novas tentativas e ordenação
Qualquer status não-2xx, ou nenhuma resposta, aciona uma nova tentativa no cronograma abaixo.
| Tentativa | Atraso |
|---|---|
| 1 | imediato |
| 2 | 30 s |
| 3 | 2 min |
| 4 | 10 min |
| 5 | 1 h |
| 6 | 6 h |
- A ordem de chegada não é garantida. Um evento de pagamento pode chegar antes do evento de confirmação que logicamente o precede: confie no estado do pedido, não na ordem de recebimento.
- O mesmo evento pode chegar duas vezes. Armazene o id do evento e ignore um id já tratado: a idempotência no seu receptor é sua responsabilidade e é fácil de obter.
- Nunca confie nos valores do payload para creditar qualquer coisa do seu lado. Releia o pedido através da API: o payload diz que algo aconteceu, a API diz exatamente o quê.
- Após a última tentativa, o evento é descartado e permanece visível no seu log de eventos. Você pode reproduzi-lo manualmente.