Developers
Webhooks
Webhook 通知您订单状态变更。它不能替代读取订单:它是通知,不是事实来源。
已发布的契约,尚未开放
目前不发送 Webhook。其结构、签名和重试策略已确定并在此发布,以便您的接收器可以提前编写和测试。
事件
每个事件都带有相同的信封: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 | 资金已退回,仅退至原始地址,扣除网络费用。 |
负载
无论类型如何,信封都是恒定的。
{
"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"
}
}签名
每次投递都带有签名头:时间戳和基于该时间戳、点号和原始请求正文计算的 HMAC-SHA256。
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)
}- 对原始正文签名,在任何解析之前。重新序列化 JSON 对象会改变键顺序和空白:签名将不再匹配,您将花费很长时间排查。
- 使用恒定时间比较。=== 比较在第一个不同字节处停止,这使攻击者可以逐字节测量预期签名。
- 拒绝超出 300 秒的容差窗口。如果没有此检查,昨天捕获的有效请求今天仍然可以重放。
- 在处理之前返回 200。将事件放入队列,然后处理:长时间处理会触发超时,从而导致重试,即使您确实收到了事件。
重试和排序
任何非 2xx 状态,或没有响应,都会触发按以下计划重试。
| 尝试 | 延迟 |
|---|---|
| 1 | 立即 |
| 2 | 30 s |
| 3 | 2 min |
| 4 | 10 min |
| 5 | 1 h |
| 6 | 6 h |
- 到达顺序不保证。付款事件可能在逻辑上先于它的确认事件之前到达:信任订单状态,而不是接收顺序。
- 同一事件可能到达两次。请存储事件 ID,并忽略已处理的 ID:接收方的幂等性是您的责任,而且很容易实现。
- 切勿信任负载中的金额来为您这边入账。通过 API 重新读取订单:负载说明发生了某事,API 则准确说明发生了什么。
- 在最后一次尝试后,事件被丢弃,但仍保留在您的事件日志中。您可以手动重放它。