Webhook
把成交 POST 到你的服务器,带签名和重试。
Webhook
在我的推送登记一个 HTTPS 地址,我们把每笔成交 POST 过去。签名密钥只在登记那一刻显示一次,请及时保存,之后无法再次查看。每个账户 1 个地址,更换地址前需先删除旧地址。如需发给两个系统, 请在接收端分发,并分别监控状态。
登记和删除需在网站上操作,不能用 API key:webhook 是一个投递目的地, 允许 API key 更改目的地会让持有者永久重定向成交流。即使之后撤销 key, 已创建的 webhook 仍会继续接收。
每个请求带 x-pmw-signature:用你的密钥对原始 body 做 HMAC-SHA256,结果按十六进制编码。验签需使用原始字节。解析 JSON 后重新序列化可能改变字节,导致验签失败。
import { createHmac, timingSafeEqual } from "node:crypto"; app.post("/hook", express.raw({ type: "application/json" }), async (req, res) => { const sent = Buffer.from(req.header("x-pmw-signature") ?? "", "hex"); const mine = createHmac("sha256", process.env.PMW_SECRET).update(req.body).digest(); // compare the RAW body, before any JSON parsing: re-serialising changes the bytes if (sent.length !== mine.length || !timingSafeEqual(sent, mine)) return res.sendStatus(401); // store it durably BEFORE answering: a 2xx means "done" and we stop retrying, // so a fill acknowledged and then lost is gone for good. Anything else is retried. const { type, data: fill } = JSON.parse(req.body.toString()); // { "type": "fill", "data": { … } } if (type !== "fill") return res.sendStatus(200); try { await saveOnce(fill.eventId, fill); } catch { return res.sendStatus(503); } res.sendStatus(200); });
投递是至少一次。请求等待 2xx 响应的超时为 5 秒,最多重试 3 次; 发出请求后、保存记录前崩溃也会触发重放。同一笔成交可能重复出现,接收端需幂等处理,按 eventId 去重,它在每次重试中都不变。
我们不跟随重定向,也只接受公网地址,填 localhost 或内网段的地址会在注册时拒绝。
连续失败会停用。一个地址连续失败超过 24 小时、且期间连续失败至少 20 次(中间没有任何一次成功),我们就停止向它投递,并发邮件通知你。 修好接收端后,可在我的推送点「重新启用」,或调用以下接口(支持 API key):
curl -X POST https://api.pmwallets.com/v1/account/webhooks/$WEBHOOK_ID/reactivate \ -H "Authorization: Bearer $PMW_KEY"
停用期间未发出的成交不会补发,只有下面几种办法。只走 webhook 的订阅在停用约一分钟后暂停(不扣费);如果重新启用时它还没暂停,就照常继续,空档可用 GET /v1/account/fills 补拉。已暂停的在重新启用后从链头往回 65 个块恢复: 恢复前最后约 96 秒的成交可能会推过来,请按 eventId 去重;停用期里更早的成交 用 GET /v1/account/fills 也拉不到(请用历史导出)。同时走 WebSocket 的订阅从未暂停(照常推送、照常扣费), 它漏掉的 webhook 成交可以用 GET /v1/account/fills 按游标补拉(见怎么补)。GET /v1/account/webhooks 会返回 disabledAt(停用时间)和 consecutiveFailures(当前连续失败次数),可用于监控。
停用期间,只走 webhook 通道的订阅会自动暂停(pausedReason: "webhook_disabled"),不再按小时扣费; 同时走 WebSocket 的订阅不受影响。重新启用 webhook 时,这些订阅会自动恢复并扣第一个小时,余额不够的保持暂停,充值后手动恢复即可。只走 webhook 却没有配置任何 webhook(或删掉了)的订阅同样不扣费:新建请求会拒绝,已有的在续费时暂停(pausedReason: "no_webhook"),配好 webhook 后手动恢复。