Fiatside

Developers

Webhooks

Webhook 通知您订单状态变更。它不能替代读取订单:它是通知,不是事实来源。

已发布的契约,尚未开放

目前不发送 Webhook。其结构、签名和重试策略已确定并在此发布,以便您的接收器可以提前编写和测试。

01

事件

每个事件都带有相同的信封:id、类型、时间戳和数据对象。订阅您处理的类型,并静默忽略其余类型——将来会添加新类型,如果接收器因未知类型而失败,则会导致自身故障。

事件
类型含义
order.created订单已创建,汇率已锁定。存款地址已分配。
order.deposit_detected在网络上看到一笔传入交易,但尚未确认。使用此事件安抚用户,切勿交付任何内容。
order.confirming确认计数器正在推进。在每一步发出,而非每个区块。
order.deposit_confirmed已达到网络要求的确认数量。
order.underpaid收到的金额低于预期金额且超出容差范围。订单不会丢失:它等待在补足差额、按收到金额继续或退款之间做出选择。
order.overpaid收到的金额超过预期金额。初始金额保持锁定汇率;超出部分另行处理。
order.payout_sent转账已通过支付通道发出。注意:已发送不等于已收到——剩余延迟取决于收款人的银行。
order.completed结算已由支付通道确认。终态。
order.payout_failed支付通道已拒绝或退回转账,并附有原因。银行退回不会自动重试:原因几乎总是在细节中。
order.refunded资金已退回,仅退至原始地址,扣除网络费用。
02

负载

无论类型如何,信封都是恒定的。

示例json
{
  "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

签名

每次投递都带有签名头:时间戳和基于该时间戳、点号和原始请求正文计算的 HMAC-SHA256。

Fiatside-Signaturehttp
Fiatside-Signature: t=<timestamp unix>,v1=<hex hmac-sha256>
验证,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)
}
  • 对原始正文签名,在任何解析之前。重新序列化 JSON 对象会改变键顺序和空白:签名将不再匹配,您将花费很长时间排查。
  • 使用恒定时间比较。=== 比较在第一个不同字节处停止,这使攻击者可以逐字节测量预期签名。
  • 拒绝超出 300 秒的容差窗口。如果没有此检查,昨天捕获的有效请求今天仍然可以重放。
  • 在处理之前返回 200。将事件放入队列,然后处理:长时间处理会触发超时,从而导致重试,即使您确实收到了事件。
04

重试和排序

任何非 2xx 状态,或没有响应,都会触发按以下计划重试。

重试和排序
尝试延迟
1立即
230 s
32 min
410 min
51 h
66 h
  • 到达顺序不保证。付款事件可能在逻辑上先于它的确认事件之前到达:信任订单状态,而不是接收顺序。
  • 同一事件可能到达两次。请存储事件 ID,并忽略已处理的 ID:接收方的幂等性是您的责任,而且很容易实现。
  • 切勿信任负载中的金额来为您这边入账。通过 API 重新读取订单:负载说明发生了某事,API 则准确说明发生了什么。
  • 在最后一次尝试后,事件被丢弃,但仍保留在您的事件日志中。您可以手动重放它。