Fiatside

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.

01

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.

Eventos
TipoO que significa
order.createdA ordem é criada e a taxa é bloqueada. O endereço de depósito é atribuído.
order.deposit_detectedUma transação recebida é vista na rede, antes da confirmação. Use este evento para tranquilizar o usuário, nunca para entregar qualquer coisa.
order.confirmingO contador de confirmações está progredindo. Emitido a cada etapa, não a cada bloco.
order.deposit_confirmedO número de confirmações exigido pela rede é atingido.
order.underpaidO 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.overpaidO valor recebido excede o valor esperado. O valor inicial permanece na taxa bloqueada; o excesso é tratado separadamente.
order.payout_sentA transferência saiu no canal. Cuidado: enviado não é recebido — o atraso restante depende do banco do beneficiário.
order.completedA liquidação é confirmada pelo canal. Estado terminal.
order.payout_failedO 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.refundedOs fundos foram devolvidos, somente para o endereço de origem, líquidos de taxas de rede.
02

O payload

Um envelope constante, qualquer que seja o tipo.

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

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-Signaturehttp
Fiatside-Signature: t=<timestamp unix>,v1=<hex hmac-sha256>
Verificação, 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)
}
  • 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.
04

Novas tentativas e ordenação

Qualquer status não-2xx, ou nenhuma resposta, aciona uma nova tentativa no cronograma abaixo.

Novas tentativas e ordenação
TentativaAtraso
1imediato
230 s
32 min
410 min
51 h
66 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.