PSP Gateway API
版本 2026-07-01
商户服务端接入
PSP 商户 API 对接文档
本文档按 Apifox 常见接口文档方式组织:先说明公共规则,再逐个接口给出接口地址、请求参数、请求示例、返回响应和字段说明。商户按顺序完成即可自主接入。
接入流程
- 完成接入凭证配置:在 Merchant Portal 的「接入凭证」中创建 Gateway Secret,生成或粘贴商户 Ed25519 公钥。
- 保存一次性密钥:创建成功后立即保存 API ID、API Key 和商户私钥;API Key 和私钥不能从 PSP 恢复。
- 配置 Webhook:在「Webhook」页面按商户账户配置 HTTPS 回调地址,并复制该账户的 PSP 验签公钥。
- 实现公共请求头:请求带上
X-API-ID、X-API-Key、X-Timestamp 和签名;写请求再带幂等键。
- 实现签名与幂等:写请求必须带
Idempotency-Key,并把幂等键放入签名字符串。
- 优先联调余额与 PIX 代收:先验证认证链路,再联调异步 Webhook。
- 接入代付和其他支付方式:代收、代付最终状态以查询接口或 Webhook 为准。
公共信息
环境变量
| 变量 | 示例值 | 说明 |
GATEWAY_BASE_URL | https://gateway.example.com | PSP Gateway API 地址。 |
API_KEY | live_xxx / test_xxx | 商户后台创建,服务端保存。 |
MERCHANT_ACCOUNT_CODE | BR001 | 商户账户编码,每次调用 Gateway 必传。 |
ACCOUNT_ID | 550e8400-e29b-41d4-a716-446655440000 | 查询余额、配置账户相关功能时使用。 |
WEBHOOK_KEY_ID | prod-main-2026-06 | Webhook 账户卡片显示的 PSP 签名 Key ID,用于识别当前 Webhook 公钥。 |
PRIVATE_KEY_PEM | -----BEGIN OPENSSH PRIVATE KEY-----... | 商户 Ed25519 私钥,只保存在商户服务端。 |
PUBLIC_KEY_OPENSSH | ssh-ed25519 AAAA... | 上传到商户后台的 Ed25519 公钥。 |
WEBHOOK_PUBLIC_KEY_BASE64 | base64_ed25519_public_key | PSP Webhook 公钥,用于商户验签。 |
金额规则
| 字段 | 类型 | 说明 |
amount | integer | 金额使用最小货币单位。示例:BRL 100.00 传 10000。 |
currency | string | 币种大写三位 ISO 代码,例如 BRL、MXN、INR。 |
merchant_order_id | string | 商户订单号,建议全局唯一,便于对账和幂等排查。 |
metadata | object | 商户自定义信息,PSP 原样保存和返回。 |
后台配置
以下步骤对应 Merchant Portal 当前页面和弹框。文档中的“商户请求签名公钥”和“PSP Webhook 验签公钥”是两把不同的公钥,请分别按对应章节配置。
图 1:进入「接入凭证」后点击「创建 Gateway Secret」,在弹框中选择账户、用途和环境,并填写 Ed25519 公钥。
图 2:进入「Webhook」按账户配置 HTTPS 回调地址;保存后从账户卡片复制或下载 PSP 验签公钥。
1. 创建 Gateway Secret(接入凭证)
图 3:Gateway Secret 创建弹框。按账户选择密钥用途,粘贴或生成 Ed25519 公钥,再填写说明、IP 白名单和权限范围。
- 登录 Merchant Portal,进入「接入凭证」。在「Gateway Secret 调用凭证」区域点击「创建 Gateway Secret」。
- 在弹框中选择要调用的「商户账户」、密钥用途(「代收」或「转账」)和环境(
staging 或 production)。每个账户、用途和环境应使用对应的凭证。
- 在「Ed25519 公钥」字段中粘贴已有公钥,或点击「在浏览器中生成密钥对」由浏览器本地生成。生成私钥不会上传到 PSP。
- 按需填写「描述」「Gateway IP 白名单」、权限范围和过期时间。IP 白名单留空表示拒绝所有外部 Gateway 请求。
- 提交后保存弹框中的 API ID、API Key、Ed25519 Public Key 和 SHA-256 Fingerprint。API ID、API Key、公钥和权限创建后不能原地修改。
- 立即点击「下载私钥(仅此一次)」和「下载公钥」。API Key 和私钥只在创建流程中显示或下载一次;私钥只存放在商户服务端密钥系统。
PSP 不会接收、保存或恢复 Gateway 私钥。轮换凭证时,请创建新的 Gateway Secret 和密钥对,先完成服务端切换,再撤销旧凭证。
2. 配置账户级 Webhook
- 进入 Merchant Portal 的「Webhook」页面,使用账户筛选找到目标商户账户。Webhook 配置按账户分别生效。
- 在账户卡片中点击配置,填写完整的 HTTPS 回调 URL、需要接收的 event type、允许域名、自定义 headers JSON 和描述,然后启用配置并保存。
- 保存后,在同一账户卡片中点击「复制验签公钥」或「下载当前公钥」,将公钥配置到商户 Webhook 接收服务。
- 收到通知时,使用该账户的 PSP Webhook 公钥验证
X-Timestamp + "." + raw_body 的 Ed25519 签名。不要使用接入凭证页面上传的商户请求签名公钥。
认证方式
商户 Gateway 使用 Account Secret 认证:X-API-ID 定位账户凭证,X-API-Key 校验密钥,X-Timestamp 防止过期请求,X-Signature 使用 Ed25519 验证请求。Invoice、Transfer、Query Gateway 使用对应类型的 Account Secret。
公共 Header 参数
| 参数名 | 类型 | 必填 | 说明 |
X-API-ID | string | 是 | Account Secret 的 API ID。 |
X-API-Key | string | 是 | API Key,Gateway 实际认证来源。 |
X-Timestamp | integer | 是 | Unix 秒级时间戳,默认允许 60 秒时间差。 |
X-Signature | string | 是 | Ed25519 签名结果,标准 Base64 或 Raw Base64 编码。 |
Idempotency-Key | string | 写请求必填 | 幂等键。兼容头 X-Idempotency-Key 也可使用。 |
Content-Type | string | POST 必填 | 固定为 application/json。 |
请求签名
签名算法
| 算法 | Ed25519 |
| 编码 | X-Signature 使用标准 Base64 或 Raw Base64 编码。 |
| 签名内容 | 签名以下 9 行规范化内容,使用商户 Ed25519 私钥签名。 |
| 验签公钥 | 商户在后台上传的 Ed25519 公钥。 |
读请求签名字符串
读请求也必须签名。签名字符串固定为:
<HTTP_METHOD>
<API_ID>
<MERCHANT_ACCOUNT_ID>
<ESCAPED_PATH>
<CANONICAL_QUERY>
<X-Timestamp>
<IDEMPOTENCY_KEY_OR_EMPTY>
<SHA256_HEX_OF_RAW_BODY>
写请求签名字符串
完整签名串还需以 v1 作为第一行,方法使用大写;query 参数按键和值排序后 URL 编码。无 body 时使用空 body 的 SHA-256。
签名示例
v1
api_test_001
550e8400-e29b-41d4-a716-446655440000
POST
/api/v1/payments/intents
1781440000
ORD-10001
<sha256_hex_of_raw_body>
ESCAPED_PATH 只包含请求路径,CANONICAL_QUERY 是排序并编码后的 query。body 先按实际发送的 raw bytes 计算 SHA-256,再放入签名串。
签名示例代码
Node.js
const crypto = require("crypto");
function signRequest({ method, path, canonicalQuery = "", apiId, merchantAccountId, body = "", idempotencyKey = "", privateKeyPem }) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash("sha256").update(body, "utf8").digest("hex");
const payload = ["v1", apiId, merchantAccountId, method.toUpperCase(), path, canonicalQuery, timestamp, idempotencyKey, bodyHash].join("\n");
const signature = crypto.sign(null, Buffer.from(payload), privateKeyPem).toString("base64");
return { timestamp, signature, payload };
}
module.exports = { signRequest };
Java
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.security.MessageDigest;
import java.util.HexFormat;
public class PspSigner {
static PrivateKey loadPrivateKey(String privateKeyPem) throws Exception {
String pem = privateKeyPem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(pem);
return KeyFactory.getInstance("Ed25519").generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
}
static String sign(String apiId, String merchantAccountId, String method, String path, String canonicalQuery, long timestamp, String idempotencyKey, String body, PrivateKey key) throws Exception {
String bodyHash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(body.getBytes(StandardCharsets.UTF_8)));
String payload = "v1\n" + apiId + "\n" + merchantAccountId + "\n" + method.toUpperCase() + "\n" + path + "\n" + canonicalQuery + "\n" + timestamp + "\n" + idempotencyKey + "\n" + bodyHash;
Signature signer = Signature.getInstance("Ed25519");
signer.initSign(key);
signer.update(payload.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signer.sign());
}
}
PHP
<?php
function sign_psp_request($apiId, $merchantAccountId, $method, $path, $canonicalQuery, $timestamp, $idempotencyKey, $body, $privateKeyBase64) {
$privateKey = base64_decode($privateKeyBase64);
$bodyHash = hash('sha256', $body);
$payload = "v1\n" . $apiId . "\n" . $merchantAccountId . "\n" . strtoupper($method) . "\n" . $path . "\n" . $canonicalQuery . "\n" . $timestamp . "\n" . $idempotencyKey . "\n" . $bodyHash;
$signature = sodium_crypto_sign_detached($payload, $privateKey);
return base64_encode($signature);
}
?>
接口接入示例
以下接口示例都放在本文档内。创建类接口必须使用 Idempotency-Key,查询类接口不需要 body。
Account Secret 认证
写请求必须带 Idempotency-Key
签名使用 Base64 编码
签名包含 body SHA-256
POST
/api/v1/payments/intents
创建收款
创建一笔代收订单。PIX 返回二维码或复制码;SPEI 和 UPI 返回对应收款指令。支付结果以查询接口或 Webhook 为准。
接口信息
| 请求地址 | {GATEWAY_BASE_URL}/api/v1/payments/intents |
| 请求方式 | POST |
| Content-Type | application/json |
| 认证 | 需要公共 Header、签名和 Idempotency-Key。 |
Body 参数
| 参数名 | 类型 | 必填 | 说明 |
payment_method | string | 是 | pix、spei、upi、credit_card、apple_pay、google_pay、cash_app、boleto。 |
rail_variant | string | 是 | 支付通道变体,由 PSP 为对应支付方式配置。 |
amount | integer | 是 | 收款金额,最小货币单位。 |
currency | string | 是 | 币种,例如 BRL、MXN、INR。 |
instruction | object | 是 | 支付方式指令字段,不同方式字段不同。 |
payer_info | object | 否 | 付款人信息:name、tax_id、email、phone、account。 |
merchant_order_id | string | 是 | 商户订单号,最长 100 字符。 |
items | array | 否 | 订单商品明细。 |
return_url | string | 否 | 需要跳转的支付方式使用的返回地址。 |
webhook_url | string | 否 | 该订单的商户通知地址。 |
expires_in_minutes | integer | 否 | 过期分钟数,范围 1-1440。 |
allow_different_amount | boolean | 否 | 是否允许不同金额付款。 |
allow_multiple_payments | boolean | 否 | 是否允许多次付款。 |
metadata | object | 否 | 商户自定义数据。 |
instruction 参数示例
| payment_method | instruction 字段 | 说明 |
pix | pix_key、pix_key_type | PIX 收款 key 和 key 类型。 |
spei | beneficiary_clabe、beneficiary_name | SPEI 收款 CLABE 和收款方名称。 |
upi | upi_id、upi_id_type | UPI VPA 或其他 UPI 标识。 |
请求示例代码
curl -X POST "$GATEWAY_BASE_URL/api/v1/payments/intents" \
-H "X-API-Key: $API_KEY" \
-H "X-API-ID: $API_ID" \
-H "X-Timestamp: 1781440000" \
-H "X-Signature: <signature_base64>" \
-H "Idempotency-Key: ORD-10001" \
-H "Content-Type: application/json" \
--data-raw '{
"payment_method": "pix",
"rail_variant": "pix_qr",
"amount": 10000,
"currency": "BRL",
"instruction": {
"pix_key": "pay@example.com",
"pix_key_type": "email"
},
"payer_info": {
"name": "Joao Silva",
"tax_id": "12345678909",
"email": "joao@example.com"
},
"merchant_order_id": "ORD-10001",
"expires_in_minutes": 30,
"metadata": {
"order_id": "ORD-10001"
}
}'
返回响应
{
"id": "019ec682-e915-742a-8810-d4a888712b1b",
"collection_number": "PI_pix_20260614142208_d4a888712b1b",
"payment_method": "pix",
"amount": 10000,
"currency": "BRL",
"status": "pending",
"state": "awaiting_payment",
"merchant_order_id": "ORD-10001",
"provider_transaction_id": "BRADESCO-IN-xxx",
"instruction": {
"pix_key": "pay@example.com",
"pix_key_type": "email",
"pix_txid": "BRADESCO-IN-xxx",
"qr_code": "000201...",
"qr_code_url": "https://...",
"emv_code": "000201..."
},
"created_at": "2026-06-14T14:22:08Z",
"updated_at": "2026-06-14T14:22:08Z",
"expires_at": "2026-06-14T14:52:08Z",
"allow_different_amount": false,
"allow_multiple_payments": false,
"metadata": {
"order_id": "ORD-10001"
}
}
返回字段说明
| 字段 | 说明 |
collection_number | PSP 收款单号,查询详情和对账使用。 |
status | 收款主状态,例如 pending、paid、expired、cancelled、refunded。 |
state | 更细的业务状态,例如等待付款、处理中等。 |
instruction.qr_code | PIX 二维码内容或复制码。 |
expires_at | 订单过期时间。 |
其他支付方式 Body 示例
{
"payment_method": "spei",
"amount": 100000,
"currency": "MXN",
"instruction": {
"beneficiary_clabe": "012345678901234567",
"beneficiary_name": "Merchant Mexico SA"
},
"merchant_order_id": "ORD-MX-10001",
"expires_in_minutes": 1440
}
{
"payment_method": "upi",
"amount": 500000,
"currency": "INR",
"instruction": {
"upi_id": "merchant@upi",
"upi_id_type": "vpa"
},
"merchant_order_id": "ORD-IN-10001",
"expires_in_minutes": 15
}
GET
/api/v1/payments/intents
查询收款列表
按支付方式、状态、商户订单号和创建时间查询当前商户账户下的收款订单。
Query 参数
| 参数名 | 类型 | 必填 | 说明 |
payment_method | string | 否 | 支付方式筛选。 |
status | string | 否 | 状态筛选。 |
merchant_order_id | string | 否 | 商户订单号。 |
created_after | string | 否 | 创建时间起始,RFC3339 格式。 |
created_before | string | 否 | 创建时间截止,RFC3339 格式。 |
limit | integer | 否 | 每页数量,默认 20,最大 100。 |
offset | integer | 否 | offset 分页偏移量。 |
cursor | string | 否 | 游标分页。传该参数时响应为 items、next_cursor、has_more。 |
请求示例代码
curl -G "$GATEWAY_BASE_URL/api/v1/payments/intents" \
-H "X-API-Key: $API_KEY" \
-H "X-API-ID: $API_ID" \
-H "X-Timestamp: 1781440200" \
-H "X-Signature: <signature_base64>" \
--data-urlencode "payment_method=pix" \
--data-urlencode "status=pending" \
--data-urlencode "limit=20" \
--data-urlencode "offset=0"
返回响应
{
"items": [
{
"collection_number": "PI_pix_20260614142208_d4a888712b1b",
"payment_method": "pix",
"amount": 10000,
"currency": "BRL",
"status": "pending",
"merchant_order_id": "ORD-10001",
"created_at": "2026-06-14T14:22:08Z"
}
],
"limit": 20,
"offset": 0,
"total": 1
}
GET
/api/v1/payments/intents/{intent_id}
查询收款详情
根据 PSP 收款单号查询单笔收款详情。路径参数 intent_id 传 collection_number。
Path 参数
| 参数名 | 类型 | 必填 | 说明 |
intent_id | string | 是 | 收款系统单号,即 collection_number。 |
请求示例代码
curl "$GATEWAY_BASE_URL/api/v1/payments/intents/PI_pix_20260614142208_d4a888712b1b" \
-H "X-API-Key: $API_KEY" \
-H "X-API-ID: $API_ID" \
-H "X-Timestamp: 1781440250" \
-H "X-Signature: <signature_base64>"
POST
/api/v1/payments/transfers
创建出款
创建一笔代付订单。代付通常异步处理,商户应通过查询接口或 Webhook 获取最终结果。
接口信息
| 请求地址 | {GATEWAY_BASE_URL}/api/v1/payments/transfers |
| 请求方式 | POST |
| Content-Type | application/json |
| 认证 | 需要公共 Header、签名和 Idempotency-Key。 |
Body 参数
| 参数名 | 类型 | 必填 | 说明 |
payment_method | string | 是 | pix、spei、upi、america_ach。 |
rail_variant | string | 是 | 支付通道变体;PIX QR 出款使用 pix_qr。 |
webhook_url | string | 否 | 该订单的商户通知地址。 |
amount | integer | 是 | 出款金额,最小货币单位。 |
currency | string | 是 | 币种。 |
beneficiary | object | 是 | 收款人信息。至少包含 name 和 account。 |
instruction | object | 否 | 支付方式补充指令。 |
merchant_order_id | string | 否 | 商户订单号,最长 100 字符。 |
description | string | 否 | 订单描述,最长 500 字符。 |
metadata | object | 否 | 商户自定义数据。 |
beneficiary 参数
| 参数名 | 类型 | 必填 | 说明 |
name | string | 是 | 收款人名称。 |
account | string | 是 | 收款账户。PIX 代付时该字段就是实际 PIX key。 |
tax_id | string | 否 | 收款人税号或身份标识。 |
email | string | 否 | 收款人邮箱。 |
phone | string | 否 | 收款人手机号。 |
bank_code | string | 否 | 银行代码,SPEI 等银行转账方式常用。 |
branch_code | string | 否 | 支行代码。 |
PIX 出款中 beneficiary.account 是实际 PIX key。如同时提交 instruction.pix_key,必须与 beneficiary.account 完全一致。
请求示例代码
curl -X POST "$GATEWAY_BASE_URL/api/v1/payments/transfers" \
-H "X-API-Key: $API_KEY" \
-H "X-API-ID: $API_ID" \
-H "X-Timestamp: 1781440300" \
-H "X-Signature: <signature_base64>" \
-H "Idempotency-Key: PAYOUT-BR-20001" \
-H "Content-Type: application/json" \
--data-raw '{
"payment_method": "pix",
"rail_variant": "pix_qr",
"amount": 10000,
"currency": "BRL",
"beneficiary": {
"name": "Maria Silva",
"tax_id": "12345678909",
"email": "maria@example.com",
"account": "maria@example.com"
},
"instruction": {
"pix_key": "maria@example.com",
"pix_key_type": "email"
},
"merchant_order_id": "PAYOUT-BR-20001"
}'
返回响应
{
"id": "019ec6fb-e6a6-7cbd-bb84-36b51cdfda2d",
"payout_number": "TI_pix_20260614163417_36b51cdfda2d",
"payment_method": "pix",
"amount": 10000,
"currency": "BRL",
"status": "pending",
"state": "created",
"merchant_order_id": "PAYOUT-BR-20001",
"beneficiary": {
"name": "Maria Silva",
"tax_id": "12345678909",
"email": "maria@example.com",
"account": "maria@example.com"
},
"created_at": "2026-06-14T16:34:17Z",
"updated_at": "2026-06-14T16:34:17Z"
}
其他支付方式 Body 示例
{
"payment_method": "spei",
"amount": 10000,
"currency": "MXN",
"beneficiary": {
"name": "Carlos Garcia",
"tax_id": "GACR850101HDFRRL09",
"account": "012345678901234567",
"bank_code": "012"
},
"instruction": {
"concepto_pago": "Pago de comisiones",
"numeric_reference": "1234567"
},
"merchant_order_id": "PAYOUT-MX-20001"
}
{
"payment_method": "upi",
"amount": 100000,
"currency": "INR",
"beneficiary": {
"name": "Rajesh Kumar",
"account": "rajesh@upi",
"phone": "+919876543210"
},
"merchant_order_id": "PAYOUT-IN-20001"
}
GET
/api/v1/payments/transfers
查询出款列表
按支付方式、状态、商户订单号和创建时间查询当前商户账户下的出款订单。
Query 参数
| 参数名 | 类型 | 必填 | 说明 |
payment_method | string | 否 | 支付方式筛选。 |
status | string | 否 | 状态筛选:pending、processing、completed、failed、returned、cancelled。 |
merchant_order_id | string | 否 | 商户订单号。 |
created_after | string | 否 | 创建时间起始,RFC3339 格式。 |
created_before | string | 否 | 创建时间截止,RFC3339 格式。 |
limit | integer | 否 | 每页数量,默认 20,最大 100。 |
offset | integer | 否 | offset 分页偏移量。 |
请求示例代码
curl -G "$GATEWAY_BASE_URL/api/v1/payments/transfers" \
-H "X-API-Key: $API_KEY" \
-H "X-API-ID: $API_ID" \
-H "X-Timestamp: 1781440400" \
-H "X-Signature: <signature_base64>" \
--data-urlencode "payment_method=pix" \
--data-urlencode "status=processing" \
--data-urlencode "limit=20" \
--data-urlencode "offset=0"
返回响应
{
"items": [
{
"payout_number": "TI_pix_20260614163417_36b51cdfda2d",
"payment_method": "pix",
"amount": 10000,
"currency": "BRL",
"status": "processing",
"merchant_order_id": "PAYOUT-BR-20001",
"created_at": "2026-06-14T16:34:17Z"
}
],
"limit": 20,
"offset": 0,
"total": 1
}
GET
/api/v1/payments/transfers/{intent_id}
查询出款详情
根据 PSP 出款单号查询单笔出款详情。路径参数 intent_id 传 payout_number。
Path 参数
| 参数名 | 类型 | 必填 | 说明 |
intent_id | string | 是 | 出款系统单号,即 payout_number。 |
请求示例代码
curl "$GATEWAY_BASE_URL/api/v1/payments/transfers/TI_pix_20260614163417_36b51cdfda2d" \
-H "X-API-Key: $API_KEY" \
-H "X-API-ID: $API_ID" \
-H "X-Timestamp: 1781440450" \
-H "X-Signature: <signature_base64>"
GET
/api/v1/merchant-accounts/{account_id}/balance
查询账户余额
出款前建议先查询账户余额,重点关注 available_balance。
Path 参数
| 参数名 | 类型 | 必填 | 说明 |
account_id | string | 是 | 商户账户 ID。 |
请求示例代码
curl "$GATEWAY_BASE_URL/api/v1/merchant-accounts/$ACCOUNT_ID/balance" \
-H "X-API-Key: $API_KEY" \
-H "X-API-ID: $API_ID" \
-H "X-Timestamp: 1781440500" \
-H "X-Signature: <signature_base64>"
返回响应
{
"merchant_account_id": "550e8400-e29b-41d4-a716-446655440000",
"ledger_account_id": "ledger_001",
"processing_balance": 200000,
"settlement_balance": 1800000,
"frozen_balance": 100000,
"available_balance": 1500000,
"currency": "BRL",
"as_of": "2026-06-14T16:40:00Z",
"updated_at": "2026-06-14T16:40:00Z"
}
返回字段说明
| 字段 | 说明 |
processing_balance | 处理中余额。 |
settlement_balance | 已结算余额。 |
frozen_balance | 冻结余额。 |
available_balance | 可用余额,创建出款前重点判断。 |
as_of | 余额快照时间。 |
Webhook 通知
PSP 会将代收、代付结果推送到商户配置的 webhook_url。商户需要验证签名、按投递 ID 做幂等处理,并快速返回 HTTP 2xx。API Reference 中的 /api/v1/webhooks/* 是 PSP 接收渠道回调的内部入口,不是商户需要调用的地址。
获取 PSP Webhook 公钥
- 登录 Merchant Portal,进入「Webhook」,在账户筛选中找到接收通知的商户账户。
- 先确认该账户已保存 HTTPS endpoint URL。账户卡片会显示配置状态、event type、Key ID、签名算法和 Fingerprint。
- 点击「复制验签公钥」直接复制当前公钥,或点击「下载当前公钥」保存文件。PSP 只公开验签公钥,不会透露 Webhook 私钥。
- 在商户 Webhook 接收服务中按账户保存公钥,并使用对应账户的公钥验证通知。不同账户不要混用公钥。
Webhook 公钥轮换
- 在目标账户卡片点击「准备轮换」,然后点击「下载 next 公钥」。
- 先把 next 公钥部署到商户接收服务,使服务在过渡期同时支持当前公钥和 next 公钥。
- 确认服务已部署后,回到后台点击「启用 next 密钥」。确认新旧密钥切换完成后,再从服务端移除旧公钥。
Webhook Header
| 参数名 | 说明 |
X-Webhook-Event | 事件类型,例如 payment.intent.completed。 |
X-Webhook-Event-ID | 事件 ID。 |
X-Webhook-ID | 本次投递 ID,建议做唯一约束。 |
X-Webhook-Version | Webhook 版本。 |
X-Timestamp | 通知时间戳。 |
X-Signature | 对 <X-Timestamp>.<raw_body> 签名后的标准 Base64 签名。 |
X-Signature-Algorithm | 固定为 Ed25519。 |
Webhook Body 示例
{
"event": "payment.intent.completed",
"timestamp": "2026-06-14T14:30:00Z",
"data": {
"collection_number": "PI_pix_20260614142208_d4a888712b1b",
"payment_method": "pix",
"rail_variant": "pix_qr",
"amount": 10000,
"paid_amount": 10000,
"currency": "BRL",
"merchant_order_id": "ORD-10001",
"provider_transaction_id": "BRADESCO-IN-xxx",
"paid_at": "2026-06-14T14:30:00Z"
}
}
Webhook 事件
| 事件 | 说明 | 商户处理建议 |
payment.intent.completed | 代收完成 | 标记订单已支付,并用订单号做幂等。 |
payment.intent.failed | 代收失败 | 标记失败,可提示用户重新支付。 |
payment.transfer.completed | 代付完成 | 标记打款成功。 |
payment.transfer.failed | 代付失败 | 记录失败原因,按业务决定是否重试。 |
Node.js Webhook 验签示例
// npm install tweetnacl
const nacl = require("tweetnacl");
function verifyWebhook(publicKeyBase64, timestamp, rawBodyBuffer, signatureBase64) {
const publicKey = Buffer.from(publicKeyBase64, "base64");
const signature = Buffer.from(signatureBase64, "base64");
const payload = Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), rawBodyBuffer]);
return nacl.sign.detached.verify(payload, signature, publicKey);
}
function handleWebhook(req, res) {
const rawBody = req.rawBody; // 必须是未解析、未格式化的原始 body
const timestamp = req.headers["x-timestamp"];
const signature = req.headers["x-signature"];
const webhookId = req.headers["x-webhook-id"];
if (!verifyWebhook(process.env.WEBHOOK_PUBLIC_KEY_BASE64, timestamp, rawBody, signature)) {
res.statusCode = 401;
res.end("invalid signature");
return;
}
// 用 webhookId 做唯一约束,重复通知直接返回 2xx。
res.statusCode = 204;
res.end();
}
联调检查项
| 步骤 | 验收结果 |
| 创建 sandbox/test API Key | 商户服务端可以读取到 API Key。 |
| 上传签名公钥 | 请求签名校验通过。 |
| 配置 Webhook URL | 商户服务可收到 PSP 通知。 |
| 调用余额查询 | 返回账户余额,认证链路可用。 |
| 创建 PIX 代收 | 返回 collection_number 和二维码/复制码。 |
| 接收代收 Webhook | 验签通过,订单状态更新为已支付。 |
| 重复同一幂等键请求 | 同 key + 同 body 不重复创建订单。 |
| 创建 PIX 代付 | 最终查询状态和 Webhook 状态一致。 |
幂等重试示例
// 第一次请求
Idempotency-Key: ORD-10001
body_sha256: 6b3f...
// 安全重试:同 key + 同 body
Idempotency-Key: ORD-10001
body_sha256: 6b3f...
// 错误用法:同 key + 不同 body,会返回 422 VALIDATION_ERROR
Idempotency-Key: ORD-10001
body_sha256: 9a01...
错误码
| HTTP / Code | 常见原因 | 处理方式 |
400 INVALID_REQUEST | 缺少 API ID、时间戳或写请求幂等键,或请求体格式错误。 | 检查请求体和必需 Header。 |
401 UNAUTHORIZED | API ID/API Key 无效、签名无效、时间戳超出 60 秒窗口。 | 确认 Account Secret、签名串和服务器时间。 |
401 SIGNATURE_INVALID | 签名串、规范化 query、body SHA-256 或 Base64 编码不一致。 | 使用实际发送的 raw body 重新计算签名。 |
403 FORBIDDEN | API Key scope 不足、商户或账户状态不可用、IP 未进白名单。 | 检查权限、商户状态和出口 IP。 |
409 IDEMPOTENCY_CONFLICT | 幂等键冲突。 | 确认重试是否使用同一个 body。 |
422 VALIDATION_ERROR | 同一个 Idempotency-Key 搭配了不同 body。 | 重试必须保持 body 不变。 |
422 INSUFFICIENT_BALANCE | 可用余额不足。 | 先查询余额,充值或等待入账后重试。 |