跳转到主内容

Webhook 通知

允许使用 Webhooks 4pay.online 将实时交易状态更新到您的服务器中 。


如何运作​

  1. 您的平台操作员设置了通知 URL 在您的终端
  2. 当交易状态发生变化时,平台发送签名 POST 给那个 URL
  3. 您的服务器用任意 2xx 以确认收到
  4. 其他一切——非-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_idstring每个事件都独一无二。 使用它作为调试键
timestampinteger交付形成时的 Unix 秒
typestringtransaction.<status>
dataobject交易有效载荷

data 字段​

外地类型说明
typestring交易类型 : payment, payout
txidstring你的原订单 ID
idstring平台交易 UUID
amountstring以最小货币单位计算的最初数额
amount_deststring贷记你账下的数额(费用之后)
amount_with_feestring数额,包括平台费
statusstring新交易状况
error_descriptionstring错误描述( 成功时空)

数量为十进制 字符串,而不是数字——a 18- 小数资产不符合 a JSON 号码安全。

webhooks 中的状况值​

状态最终含义
createdno创建交易, 等待处理
chargingno进行中
action_requiredno要求付款人采取的行动
charged** 对**已成功解决
rejected** 对**因限额、反欺诈或提供者而减少
failed** 对**处理错误,包括在结算前取消
reversed** 对**退回了一笔已结清的交易
refundedno退款完成——仍在审查最终结果

以上列表不封闭:铁路承载自身中间状态. (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

正式合同​

本页是实用指南。 签名、 重试时间表和状态词汇表是 通用交易协议.