Webhook 通知
允许使用 Webhooks 4pay.online 将实时交易状态更新到您的服务器中 。
如何运作
- 您的平台操作员设置了通知 URL 在您的终端
- 当交易状态发生变化时,平台发送签名
POST给那个 URL - 您的服务器用任意
2xx以确认收到 - 其他一切——非-2xx,超时,连接错误——触发重试
设置网点
通知 URL is a 终端设置,在管理员中配置过一次
控制台( E)notify_url),适用于终端上的每一笔交易.
没有按请求通知字段:a notifyUrl 在创建的正文是
。如果需要不同的目的地,请使用
不同的终端。
除了我们... URL,设置 网络隐藏秘诀 在终端,没有它 平台没有签名,你没有办法告诉真人 由伪造者通知。
也可以在合伙人账户上而不是终端上设定一个秘密;终端是自己的 询问您的操作员, 哪个操作员在您的交货上签名 。 对错误的秘密 失败完全像伪造。
Webhook 有效载荷
交易数据被包裹在一个信封里。读取您的字段
data,而不是从顶层。
{
"event_id": "4d9d0e6c-1d0a-4a2e-9a2f-0c9a1b0e8f21",
"timestamp": 1772668800,
"type": "transaction.charged",
"data": {
"type": "payment",
"txid": "order-12345",
"id": "550e8400-e29b-41d4-a716-446655440000",
"amount": "1000",
"amount_dest": "950",
"amount_with_fee": "1050",
"status": "charged",
"error_description": ""
}
}
信封
| 外地 | 类型 | 说明 |
|---|---|---|
event_id | string | 每个事件都独一无二。 使用它作为调试键 |
timestamp | integer | 交付形成时的 Unix 秒 |
type | string | transaction.<status> |
data | object | 交易有效载荷 |
data 字段
| 外地 | 类型 | 说明 |
|---|---|---|
type | string | 交易类型 : payment, payout |
txid | string | 你的原订单 ID |
id | string | 平台交易 UUID |
amount | string | 以最小货币单位计算的最初数额 |
amount_dest | string | 贷记你账下的数额(费用之后) |
amount_with_fee | string | 数额,包括平台费 |
status | string | 新交易状况 |
error_description | string | 错误描述( 成功时空) |
数量为十进制 字符串,而不是数字——a 18- 小数资产不符合 a JSON 号码安全。
webhooks 中的状况值
| 状态 | 最终 | 含义 |
|---|---|---|
created | no | 创建交易, 等待处理 |
charging | no | 进行中 |
action_required | no | 要求付款人采取的行动 |
charged | ** 对** | 已成功解决 |
rejected | ** 对** | 因限额、反欺诈或提供者而减少 |
failed | ** 对** | 处理错误,包括在结算前取消 |
reversed | ** 对** | 退回了一笔已结清的交易 |
refunded | no | 退款完成——仍在审查最终结果 |
以上列表不封闭:铁路承载自身中间状态.
(cancelled, payment_suspended, pre_authorized_3ds 以及其他人),而一个人可能
将您不承认的状态视为 ** 非最后决定** 保留
等待着——一个严格的enum会在第一个不寻常的铁路上失败.
仅在最终状态下运行您的业务逻辑 。 三种最终结果不同 。 钱的下场:
charged- 和解发生,资金归你;rejected和failed:未完成结算,付款人的预留资金已释放;reversed——和解发生并归还,只能随之而来charged,所以您的商业逻辑已经运行了:reversed需要吗? 撤销 它(召回交付,禁用服务)。
结算到来前的注销 failed,从未像 reversed.
正在验证签名
每一份签名的送货都带三个信头:
x-webhook-event-id: 4d9d0e6c-1d0a-4a2e-9a2f-0c9a1b0e8f21
x-webhook-timestamp: 1772668800
x-webhook-signature: 6b1f… HMAC-SHA256, hex
签名是 HMAC-SHA256 超过字符串 <timestamp>.<raw body>键入
以您的终端的 Webhook 密钥。有两个细节来决定您的支票是否
作品 :
- 签名 原始请求正文正文如收款到 重新序列化 JSON 更改字节并打破比较;
- 签名 整个信封,没有
data.
如果交付的 timestamp 与您的
拥有时钟,并在恒定时间内比较签名.
回应一个网点
回答任何问题 2xx. 身体被忽视——空的 200 是一个有效的
承认。
其他一切——非-2xx状态,超时,拒绝连接——算作a. 失败并排程重试 。
同步处理 Webhook 。 立即确认收件, 并在背景工作中处理业务逻辑。 长期运行的处理器可能超时并造成重复交付 。
重试策略
交付失败在完成后重试 30 秒时, 2 分钟, 10 分钟, 1 小时 和 6 小时。在第六次尝试后,将手动运送。 检查,不再自动复试。
让你的处理器能胜任:同一事件可能不止一次到达.
复制在 event_id,交易时不使用 UUID 一项交易
在其一生中制造了几起事件,它们自然是不同的。
送货没有定单,如果定单对您有关系,请读一下交易
带回 GET /api/v1/transactions/:id 而不是信任到达命令。
本地测试网点
使用隧道工具(ngrok, localtunnel, Cloudflare Tunnel)在开发过程中将您的本地服务器暴露在互联网上:
ngrok http 3000
# → Forwarding: https://abc123.ngrok.io → localhost:3000
请您的操作员设置 https://abc123.ngrok.io/webhooks/payment 作为
notify_url 在你的测试终端。
见 沙盒测试( T) 用于测试交易指令。
示例处理器(节点.js)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post(
'/webhooks/payment',
express.raw({ type: 'application/json' }), // raw body — needed for the signature
(req, res) => {
const timestamp = req.get('x-webhook-timestamp');
const signature = req.get('x-webhook-signature') ?? '';
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest('hex');
// Compare lengths BEFORE timingSafeEqual: it throws a RangeError on buffers
// of different sizes, so a forged header would produce a 500 with a stack
// trace instead of a 401.
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
if (!ok) return res.sendStatus(401);
const { event_id, data } = JSON.parse(req.body);
// Deduplicate on event_id, then hand off to a background job
// processPaymentEvent(event_id, data);
console.log(`Transaction ${data.txid} (${data.id}): ${data.status}`);
res.sendStatus(200);
}
);
app.listen(3000);
示例处理器( Python)
import hashlib
import hmac
import json
import os
import time
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/payment', methods=['POST'])
def webhook():
raw = request.get_data() # raw body — needed for the signature
timestamp = request.headers.get('x-webhook-timestamp', '')
signature = request.headers.get('x-webhook-signature', '')
expected = hmac.new(
os.environ['WEBHOOK_SECRET'].encode(),
f"{timestamp}.{raw.decode()}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
return '', 401
if abs(time.time() - int(timestamp or 0)) > 300:
return '', 401
envelope = json.loads(raw)
data = envelope['data']
# Deduplicate on envelope['event_id'], then hand off to a background job
print(f"Transaction {data['txid']} ({data['id']}): {data['status']}")
return '', 200
正式合同
本页是实用指南。 签名、 重试时间表和状态词汇表是 通用交易协议.