# SeaFintech Gateway API —— 完整接入资料 > 面向商户的 SeaFintech Gateway API 接入文档:Invoice(代收)、Transfer(代付)、Query(查询)三个服务,Ed25519 请求签名、幂等重试与 Webhook。 ## 给 AI 编程助手的接入规则 开始写代码前请先阅读并遵守以下规则;与下文细节冲突时,以 OpenAPI 规范为准。 1. **先问再写。** 缺少 Host、凭证、`merchant_account_id`、回调域名或要对接的功能时,向用户提问;不要猜测,不要调用 `*.example.com`(它们只是文档占位地址)。 2. **三个服务三套配置。** Invoice(代收写、卡支付、Tokenization)、Transfer(代付写)、Query(只读)各自使用独立的 base URL 和独立凭证,客户端也分开实现;不要配置一个全局 `PSP_BASE_URL` 再动态拼服务路径。Checkout 地址使用 Invoice 响应返回的值,不要自行拼接。 3. **每个请求携带** `X-API-ID`、`X-API-Key`、`X-Timestamp`(Unix 秒,默认允许 ±60 秒偏差)、`X-Signature`(Ed25519 签名后的标准 Base64);所有写请求另需 `X-Idempotency-Key` 或 `Idempotency-Key`,同时发送时必须相等。 4. **签名 payload** 由以下 9 行用单个 `\n` 连接,末尾不加换行:`v1`、`api_id`、`merchant_account_id`(凭证所属账户 UUID,小写带连字符)、`METHOD`(大写)、`escaped_path`(与实际发送的 path 完全一致,不含 query)、`canonical_query`(URL 解码后,同名参数的值升序、再按参数名升序,按 UTF-8 字节序,拼成 `key=value&…`;重新转义时保留 `A-Za-z0-9-_.~`、空格写 `+`、其余写大写 `%XX`;无值参数写成 `key=`;无 query 则为空串;含非法 `%` 转义或未转义 `;` 的 query 会被服务端以 `400` 拒绝,分号请写成 `%3B`)、`timestamp`、`idempotency_key`(优先取 `X-Idempotency-Key`,只读请求为空串)、`sha256(body)`(对实际发送的原始字节计算,**64 位小写十六进制**;空 body 也要计算,不要省略这一行)。对整个 payload 做 Ed25519 签名并输出标准 Base64。Invoice / Transfer / Query 三个服务使用同一种 payload 格式。**写完签名模块后必须用「认证与签名」页的测试向量验证,签名逐字符一致才算正确。** 5. **幂等。** 超时或连接断开后用相同 body、相同商户订单号、相同幂等键重试,只刷新 timestamp 并重新签名;不要换新订单号。结果仍不确定时用 Query API 确认。`400/401/403/409/422` 不自动重试;`429` 遵循 `Retry-After`;`5xx` 有限重试并加入退避与 jitter。 6. **金额使用最小货币单位的整数**(`10000 BRL` 表示 `R$ 100.00`)。 7. **Webhook。** 读取原始 body 验签,不要对解析后重新序列化的 JSON 验签:待验证消息 = `X-Timestamp` + `.` + 原始 body 字节,`X-Signature` 为标准 Base64 的 Ed25519 签名,用 `X-Webhook-Key-ID` 选择对应的 PSP 公钥(公钥与 key_id 来自账户 Webhook 配置,轮换时新旧两把要并存,不要写死);同样用文档中的 Webhook 测试向量验证。去重键用 `X-Webhook-Event-ID`(不要用 `X-Timestamp`/`X-Signature`,它们每次重试都变);先入库再返回 `2xx`,10 秒内返回响应头,耗时处理放到异步任务;验签失败返回 `401`(4xx 不会被自动重试)。投递是至少一次、可能重复、不保证顺序,终态不可回退;金额、币种、商户订单号和账户都与本地订单匹配后才能入账。**自动重试只有 5 次(退避 1/2/4/8/16 秒,约 31 秒窗口),所以必须实现定时 Query API 对账。** 未知的 `event` 记录后返回 `2xx`;各事件的 `data` 字段以「Webhook 事件目录」为准(`merchant_settlement.completed.v1` 的信封与其他事件不同)。回调地址必须是 HTTPS/443 的域名(不能是 IP 或内网地址)。 8. **密钥安全。** 私钥、API Key、签名、完整请求体和敏感付款信息一律不写入源码、日志或对话;只从环境变量或 Secret Manager 读取,分环境、分账户管理。 9. **文档没写明的细节不要猜。** 仍不明确的(例如某个字段的取值范围、具体业务规则)先查 OpenAPI 与文档,再向用户或接入团队确认,并用沙箱验证。 10. **交付前自检。** 对照「上线检查清单」逐项核对,输出未满足项。 --- # 快速开始 完成第一笔请求需要一个商户账户、一组账户级 API 凭证和 Ed25519 私钥。PSP 只保存你上传的公钥;私钥必须留在你的安全环境中。 ## 1. 选择服务 | 业务动作 | 服务 | 凭证类型 | |---|---|---| | 创建或取消代收、代收退款 | Invoice | Invoice account secret | | 创建或取消代付、同步代付订单 | Transfer | Transfer account secret | | 查询订单、退款、账户或余额 | Query | Invoice 或 Transfer account secret | ## 2. 准备请求 以下是创建 PIX 代收意图的最小示例。金额使用最小货币单位;`10000 BRL` 表示 `R$ 100.00`。 ```json { "payment_method": "pix", "rail_variant": "pix_qr", "amount": 10000, "currency": "BRL", "merchant_order_id": "ORDER_20260827_001" } ``` ## 3. 生成签名 请求签名前先计算 body 的 SHA-256,并严格按换行顺序拼接 payload: ```text v1 {api_id} {merchant_account_id} {METHOD} {escaped_path} {canonical_query} {timestamp} {idempotency_key} {sha256(body)} ``` 使用 Ed25519 私钥签名 UTF-8 payload,再将签名编码为标准 Base64。完整规则见[认证与签名](https://docs.potabank.com/guide/authentication.md)。 ## 4. 发送请求 ```bash curl --request POST "${PSP_INVOICE_BASE_URL}/api/v1/payments/intents" \ --header "Content-Type: application/json" \ --header "X-API-ID: ${PSP_API_ID}" \ --header "X-API-Key: ${PSP_API_KEY}" \ --header "X-Timestamp: ${PSP_TIMESTAMP}" \ --header "X-Signature: ${PSP_SIGNATURE}" \ --header "X-Idempotency-Key: invoice:create:ORDER_20260827_001" \ --data-binary @request.json ``` > **不要直接复制 Host** > 生产与测试 Host 由接入团队提供。OpenAPI 中的 `*.example.com` 只用于描述服务边界,不是可调用地址。 ## 5. 保存关键标识 成功后至少保存 PSP 返回的 `id`、`invoice_number`、商户订单号、请求幂等键和创建时间。查询、取消及退款使用 `invoice_number`;状态确认应调用 Query API,不应只依赖同步响应或 Webhook 到达顺序。 ## 下一步 1. 对照[服务与 Host](https://docs.potabank.com/guide/service-routing.md)拆分客户端。 2. 按[幂等与重试](https://docs.potabank.com/guide/idempotency.md)实现超时恢复。 3. 接入[Webhook](https://docs.potabank.com/guide/webhooks.md)并完成验签和去重。 4. 在申请生产权限前完成[上线检查清单](https://docs.potabank.com/guide/go-live.md)。 --- # AI 辅助对接 不用逐页阅读文档:把本站的 **AI 资料包**交给 Claude、ChatGPT、Cursor 等编程助手,再配合下面的提示词,它就能按我们的签名、幂等和 Webhook 规范生成接入代码。 ## 三步完成 1. **给 AI 资料。** 任选一种: - AI 能联网:把 `https://docs.potabank.com/llms-full.txt` 这个链接发给它。 - AI 不能联网:下载 [llms-full.txt](https://docs.potabank.com/llms-full.txt)(全部指南与接口参考合并为一个文件),连同需要的 [OpenAPI 规范](https://docs.potabank.com/reference/index.md)一起上传。 2. **发送提示词。** 复制下面的提示词,把【】里的内容改成你自己的情况。 3. **提供凭证,但不要贴给 AI。** 让 AI 生成从环境变量读取凭证的代码,你在本地或部署环境里填写密钥。 ## 提示词 ```text title="对接提示词(复制后修改【】部分)" wrap 你是资深后端工程师。请帮我把 SeaFintech Gateway API 对接到我的系统。 资料: - 如果你能访问网络,请先完整阅读 https://docs.potabank.com/llms-full.txt - 否则使用我随后上传的 llms-full.txt 和 OpenAPI 文件 我的技术栈:【语言 / 框架 / 数据库,例如 Java 21 + Spring Boot + PostgreSQL】 我要对接的功能:【例如:PIX 代收 + 状态查询 + Webhook 入账】 工作方式: 1. 先用你自己的话复述接入方案:要调用哪些服务,各自用什么凭证和 Host,签名与幂等如何实现,Webhook 如何验签去重。同时列出你还缺哪些信息,等我确认后再写代码。 2. 凭证、私钥、Host 一律从环境变量读取。不要让我把它们贴给你,缺少的信息用占位符并向我提问,不要猜测。 3. 先实现签名模块,并写单元测试:用固定输入验证 payload 的 9 行拼接。 4. 再实现 Invoice / Transfer / Query 客户端(各自独立的 Host 和凭证)、幂等重试,以及 Webhook 接收端(原始 body 验签、事件去重、快速返回 2xx、异步处理)。 5. 最后对照文档中的「上线检查清单」逐项自检,输出未满足项。 ``` ## AI 必须遵守的规则 这些规则已经写在 `llms-full.txt` 的开头,AI 读取资料包时会自动看到;你也可以把它们直接贴进 `AGENTS.md`、`CLAUDE.md` 或 Cursor Rules,让 AI 在整个项目里持续遵守。 1. **先问再写。** 缺少 Host、凭证、`merchant_account_id`、回调域名或要对接的功能时,向用户提问;不要猜测,不要调用 `*.example.com`(它们只是文档占位地址)。 2. **三个服务三套配置。** Invoice(代收写、卡支付、Tokenization)、Transfer(代付写)、Query(只读)各自使用独立的 base URL 和独立凭证,客户端也分开实现;不要配置一个全局 `PSP_BASE_URL` 再动态拼服务路径。Checkout 地址使用 Invoice 响应返回的值,不要自行拼接。 3. **每个请求携带** `X-API-ID`、`X-API-Key`、`X-Timestamp`(Unix 秒,默认允许 ±60 秒偏差)、`X-Signature`(Ed25519 签名后的标准 Base64);所有写请求另需 `X-Idempotency-Key` 或 `Idempotency-Key`,同时发送时必须相等。 4. **签名 payload** 由以下 9 行用单个 `\n` 连接,末尾不加换行:`v1`、`api_id`、`merchant_account_id`(凭证所属账户 UUID,小写带连字符)、`METHOD`(大写)、`escaped_path`(与实际发送的 path 完全一致,不含 query)、`canonical_query`(URL 解码后,同名参数的值升序、再按参数名升序,按 UTF-8 字节序,拼成 `key=value&…`;重新转义时保留 `A-Za-z0-9-_.~`、空格写 `+`、其余写大写 `%XX`;无值参数写成 `key=`;无 query 则为空串;含非法 `%` 转义或未转义 `;` 的 query 会被服务端以 `400` 拒绝,分号请写成 `%3B`)、`timestamp`、`idempotency_key`(优先取 `X-Idempotency-Key`,只读请求为空串)、`sha256(body)`(对实际发送的原始字节计算,**64 位小写十六进制**;空 body 也要计算,不要省略这一行)。对整个 payload 做 Ed25519 签名并输出标准 Base64。Invoice / Transfer / Query 三个服务使用同一种 payload 格式。**写完签名模块后必须用「认证与签名」页的测试向量验证,签名逐字符一致才算正确。** 5. **幂等。** 超时或连接断开后用相同 body、相同商户订单号、相同幂等键重试,只刷新 timestamp 并重新签名;不要换新订单号。结果仍不确定时用 Query API 确认。`400/401/403/409/422` 不自动重试;`429` 遵循 `Retry-After`;`5xx` 有限重试并加入退避与 jitter。 6. **金额使用最小货币单位的整数**(`10000 BRL` 表示 `R$ 100.00`)。 7. **Webhook。** 读取原始 body 验签,不要对解析后重新序列化的 JSON 验签:待验证消息 = `X-Timestamp` + `.` + 原始 body 字节,`X-Signature` 为标准 Base64 的 Ed25519 签名,用 `X-Webhook-Key-ID` 选择对应的 PSP 公钥(公钥与 key_id 来自账户 Webhook 配置,轮换时新旧两把要并存,不要写死);同样用文档中的 Webhook 测试向量验证。去重键用 `X-Webhook-Event-ID`(不要用 `X-Timestamp`/`X-Signature`,它们每次重试都变);先入库再返回 `2xx`,10 秒内返回响应头,耗时处理放到异步任务;验签失败返回 `401`(4xx 不会被自动重试)。投递是至少一次、可能重复、不保证顺序,终态不可回退;金额、币种、商户订单号和账户都与本地订单匹配后才能入账。**自动重试只有 5 次(退避 1/2/4/8/16 秒,约 31 秒窗口),所以必须实现定时 Query API 对账。** 未知的 `event` 记录后返回 `2xx`;各事件的 `data` 字段以「Webhook 事件目录」为准(`merchant_settlement.completed.v1` 的信封与其他事件不同)。回调地址必须是 HTTPS/443 的域名(不能是 IP 或内网地址)。 8. **密钥安全。** 私钥、API Key、签名、完整请求体和敏感付款信息一律不写入源码、日志或对话;只从环境变量或 Secret Manager 读取,分环境、分账户管理。 9. **文档没写明的细节不要猜。** 仍不明确的(例如某个字段的取值范围、具体业务规则)先查 OpenAPI 与文档,再向用户或接入团队确认,并用沙箱验证。 10. **交付前自检。** 对照「上线检查清单」逐项核对,输出未满足项。 ## 可交给 AI 的资料 | 资料 | 地址 | 用途 | |---|---|---| | 完整资料包 | [/llms-full.txt](https://docs.potabank.com/llms-full.txt) | 全部指南 + 接口参考,推荐优先使用 | | 资料索引 | [/llms.txt](https://docs.potabank.com/llms.txt) | 按页面列出的索引,适合能自行抓取链接的 AI | | 单页 Markdown | 任意页面的 URL 后加 `.md`,或点击页面标题旁的「复制 Markdown」 | 只想让 AI 阅读某一页 | | OpenAPI 规范 | [Invoice](https://docs.potabank.com/openapi/invoice.openapi.yaml) · [Transfer](https://docs.potabank.com/openapi/transfer.openapi.yaml) · [Query](https://docs.potabank.com/openapi/query.openapi.yaml) · [Webhook](https://docs.potabank.com/openapi/merchant-webhook.openapi.yaml) | 生成强类型客户端 | ## 让 AI 验收自己的代码 代码生成后,把下面这段发给 AI,再附上它写的代码: ```text title="自检提示词" wrap 请对照 https://docs.potabank.com/guide/go-live.md 的上线检查清单,逐项检查我的接入代码, 并重点核对:签名 payload 的 9 行顺序、幂等键在重试时是否沿用、Webhook 是否使用原始 body 验签、 日志中是否出现密钥或完整请求体。对每一项给出「通过 / 不通过 / 无法判断」和对应代码位置。 ``` > **仍需人工把关** > AI 生成的代码必须在测试环境用真实凭证联调,并由你们的工程师评审后再上线。测试 Host 与凭证由接入团队提供,AI 无法替你获取。 --- # 服务与 Host Gateway 的公开流量面彼此隔离。每个客户端应绑定一个明确的 base URL、credential kind 和签名 service namespace。 | 服务 | 用途 | 接受的凭证 | 最小 Scope 示例 | |---|---|---|---| | Invoice | 代收、卡支付、Tokenization 写操作 | Invoice account secret | `invoices:write` | | Transfer | 代付写操作 | Transfer account secret | `transfers:write` | | Query | 所有商户查询操作 | Invoice 或 Transfer account secret | `invoices:read` / `transfers:read` / `accounts:read` | | Merchant Webhook | PSP 向商户推送事件 | PSP 事件签名 | 不适用 | | Provider Webhook | 上游通道回调 PSP | Provider 签名 | 商户不得调用 | | Hosted Checkout | 托管收银台订单与状态 | 会话或托管订单令牌 | 不适用 | ## 强制隔离规则 - Invoice Host 不接受 Transfer credential。 - Transfer Host 不接受 Invoice credential。 - Query Host 只有读路由,即使凭证有写 Scope 也不能执行写操作。 - Provider Webhook Host 不接受商户账户密钥。 - Checkout URL 应使用 Invoice 响应返回的地址,不要自行拼接。 服务或凭证类型不匹配时会 fail-close,通常返回 `401`;认证成功但 Scope 不足时通常返回 `403`。 ## 推荐客户端结构 ```text paymentGateway ├── invoice → PSP_INVOICE_BASE_URL + invoice credential ├── transfer → PSP_TRANSFER_BASE_URL + transfer credential └── query → PSP_QUERY_BASE_URL + invoice/transfer credential ``` 不要配置全局 `PSP_BASE_URL` 后动态拼服务路径,这会放大凭证误用和错误 Host 调用的风险。 --- # 认证与签名 Invoice、Transfer 和 Query 使用账户级 API Key + Ed25519 请求签名。API Key 用于定位凭证,Ed25519 用于证明请求来自持有私钥的商户系统。 ## 必需 Header | Header | 必填 | 说明 | |---|---:|---| | `X-API-ID` | 是 | 账户级 API ID | | `X-API-Key` | 是 | 账户级 API Key | | `X-Timestamp` | 是 | Unix 秒级时间戳,默认允许 ±60 秒偏差 | | `X-Signature` | 是 | Ed25519 签名的标准 Base64 编码 | | `X-Idempotency-Key` 或 `Idempotency-Key` | 写请求二选一 | 两者同时发送时必须相等,否则返回 `400 INVALID_REQUEST` | ## 签名 Payload ```text v1 {api_id} {merchant_account_id} {METHOD} {escaped_path} {canonical_query} {timestamp} {idempotency_key} {sha256(body)} ``` 字段之间使用单个 `\n`,末尾不额外追加换行。`METHOD` 使用大写;body hash 使用原始发送字节计算,不要先解析再序列化。 ## 规范化要求 - `escaped_path` 必须与 HTTP 请求实际发送的 path 完全一致,不含 query。 - `canonical_query` 的生成规则见下表;没有 query 时使用空字符串。 - 空 body 也要计算 SHA-256,不要省略最后一行。 - `{idempotency_key}` 使用 `X-Idempotency-Key` 的值;未发送该 Header 时才使用 `Idempotency-Key`。两者都没有的只读请求使用空字符串。Header 值两端空白会被忽略。 - 每次重试可刷新 timestamp 和签名,但必须沿用原幂等键与完全相同的业务 payload。 ## 字段取值与编码 | 项 | 规则 | |---|---| | `api_id` | `X-API-ID` 的值,去除两端空白 | | `merchant_account_id` | 凭证所属商户账户的 UUID(小写、带连字符),由 PSP 在签发凭证时提供 | | `METHOD` | 大写,如 `POST` | | `escaped_path` | 请求 URL 中已转义的 path,按实际发送的值,不要重新编码 | | `canonical_query` | 先对 query 做 URL 解码,同名参数的值升序排列,再按参数名升序拼成 `key=value&key=value`(按 UTF-8 字节序比较);键和值重新做标准 query 转义:保留 `A-Z a-z 0-9 - _ . ~`,空格写 `+`,其余字节写大写 `%XX`(例如 `*` 写 `%2A`,`/` 写 `%2F`)。无值参数 `flag` 视为 `flag=`,空片段(`&&`)忽略;含非法 `%` 转义(如 `%ZZ`、结尾的 `%`、不完整的 `%2`)或未转义分号 `;` 的 query 会被拒绝并返回 `400 INVALID_REQUEST`,需要分号时请写成 `%3B` | | `timestamp` | `X-Timestamp` 的十进制字符串,与 Header 完全一致 | | `sha256(body)` | 对**实际发送的原始 body 字节**计算 SHA-256,输出 **64 位小写十六进制**;空 body 为 `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | | 签名 | 对整个 payload 的 UTF-8 字节做 Ed25519 签名,结果用标准 Base64(带 `=` 填充);服务端也接受无填充的 Base64 | | 公钥 | 上传给 PSP 的是 Ed25519 公钥原始 32 字节的标准 Base64 | Invoice、Transfer 和 Query 使用**同一种** payload 格式;服务之间的隔离靠 Host 与凭证类型(见[服务与 Host](https://docs.potabank.com/guide/service-routing.md)),payload 中没有 service 字段。body 以实际发送的字节为准(JSON 通常是 UTF-8),不要在签名后再改动。 ## 测试向量 用下面的固定输入验证你的实现。向量由 PSP 的参考签名工具生成,并用独立实现交叉验证。 > **仅用于测试** > 下列私钥种子是公开的测试值,不要用于任何真实环境。 - 私钥种子(32 字节,Base64):`AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=` - 对应公钥(Base64):`A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=` - `api_id`:`api_demo`;`merchant_account_id`:`00000000-0000-7000-8000-000000000001`;`timestamp`:`1767225600` **向量 1:POST,带 body 与幂等键** body(共一行,无尾部换行): ```json {"payment_method":"pix","rail_variant":"pix_qr","amount":10000,"currency":"BRL","merchant_order_id":"ORDER_20260827_001"} ``` 幂等键 `invoice:create:ORDER_20260827_001`,待签名 payload: ```text v1 api_demo 00000000-0000-7000-8000-000000000001 POST /api/v1/payments/intents 1767225600 invoice:create:ORDER_20260827_001 b78e856a2e4eb24adf9807614503946dd9fc51d2403259858c7666d46c6ffeb7 ``` 期望签名: ```text pHBYN6FdiNc1as4N/doyvJO4n9jXfm4cIIcXcnS2N/mOWxxwwNJsoW2AmrjqtnZeOwgiHpfRXKfaZNy5ipXWAA== ``` **向量 2:GET,带 query,无 body、无幂等键** 实际发送的 URL 为 `/api/v1/payments/intents?status=pending&limit=20&status=paid`,规范化后的 `canonical_query` 是 `limit=20&status=paid&status=pending`。待签名 payload(第 8 行为空串): ```text v1 api_demo 00000000-0000-7000-8000-000000000001 GET /api/v1/payments/intents limit=20&status=paid&status=pending 1767225600 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ``` 期望签名: ```text cih5j8cxplQmgoxQjvWiitnV7dJKguV23HLCxbqENcP5W+WMc9Gl48unNTmXUQGFyBCrpXgG29lrDhlhbyYmCA== ``` ## 密钥安全 - 私钥放入 KMS、HSM 或受控 Secret Manager,不要写入源码、镜像或日志。 - API Key、签名、完整请求体和敏感付款信息不得作为常规日志字段。 - 分环境、分商户账户、分 credential kind 管理密钥。 - 轮换时保留短暂双公钥窗口,并先验证新密钥再撤销旧密钥。 --- # 幂等与重试 所有改变状态的请求都必须携带 `X-Idempotency-Key` 或 `Idempotency-Key`,至少一个。两者同时发送时必须相等;签名优先覆盖 `X-Idempotency-Key`。 ## 生成规则 为一次业务操作生成稳定的键,并在重试时沿用。`merchant_order_id` 是商户订单身份,与 Header 幂等键是两个不同字段。创建 Invoice 以账户内的 `merchant_order_id` 和请求内容识别重试,不使用网关的 Header 响应缓存;Transfer 与退款还会以 Header 键判断业务重试。信用卡授权、捕获和退款另有请求体 `idempotency_key`,建议与 Header 使用相同值。 ```text invoice:create:ORDER_20260827_001 transfer:create:PAYOUT_20260827_0042 invoice:refund:REFUND_20260827_0007 ``` ## 安全重试流程 1. 请求超时或连接断开时,不要立即生成新订单号。 2. 使用相同 body、商户订单号和 Header 幂等键重试。 3. 刷新 `X-Timestamp` 并重新签名。 4. 若结果仍不确定,通过 Query API 按资源 ID 或商户订单号确认。 ## 冲突处理 同一 Header 幂等键在网关响应缓存有效期内配合不同 body 会返回 `422 VALIDATION_ERROR`;业务层的订单号或幂等键冲突可能返回 `409 CONFLICT`。两种情况都应停止自动重试,核对原请求。创建 Invoice 的重试首先依据账户内的 `merchant_order_id`,不能仅凭更换 Header 键创建新订单。 | 状态 | 是否自动重试 | 建议 | |---|---:|---| | 网络超时 / 连接重置 | 是 | 相同幂等键,指数退避并加入 jitter | | `429 Too Many Requests` | 是 | 遵循 `Retry-After` | | `500` / `502` / `503` / `504` | 有限重试 | 相同幂等键,最终转 Query 确认 | | `400` / `401` / `403` | 否 | 修复请求或凭证配置 | | `409` | 否 | 检查幂等键与原始 payload | | `422` | 否 | 检查幂等键是否配合了不同 body,或支付 Rail 是否可用 | --- # Webhook Webhook 用于及时通知状态变化,但它不是唯一事实来源。PSP 会向你配置的 HTTPS 回调地址发送 `POST` 请求;你的系统必须能处理**重复、乱序、延迟和暂时丢失**的事件,并用 Query API 主动对账。 > **自动重试窗口很短** > 当前每个事件最多自动重试 5 次,退避间隔依次为 1、2、4、8、16 秒——也就是说,接收端持续不可用超过约半分钟,事件就会停止自动重试。你必须有定时对账任务,并在后台监控投递失败。 ## 接入步骤 1. **准备接收端地址。** 公网可访问的 HTTPS 地址,要求见下文[回调地址要求](#回调地址要求)。 2. **配置回调地址。** 通过商户后台(Merchant Portal)为账户设置回调 URL,首次配置会自动生成签名密钥,见[配置与运维接口](#配置与运维接口)。 3. **保存 PSP 公钥。** 读取账户 Webhook 配置,保存返回的 `key_id` 与 `public_key`;接收端按 `X-Webhook-Key-ID` 选择公钥。 4. **实现接收端。** 读取原始 body → 验签 → 按事件 ID 去重 → 持久化并入队 → 返回 `2xx`。可直接参考[接收端示例](#接收端示例)。 5. **用测试向量自检验签。** 目前没有“发送测试事件”的接口,联调时请在测试环境创建订单,由真实事件触发。 6. **上线前。** 完成[上线检查清单](https://docs.potabank.com/guide/go-live.md)中的 Webhook 与对账项。 ## 请求格式 每次投递都是 `POST`,`Content-Type: application/json`,`User-Agent: PSP-Webhook/1.0`,并携带以下 Header: | Header | 说明 | |---|---| | `X-Timestamp` | 本次发送时的 Unix 秒级时间戳。**每次重试都会变** | | `X-Signature` | Ed25519 签名,标准 Base64。**每次重试都会变** | | `X-Signature-Algorithm` | 固定为 `ed25519` | | `X-Webhook-Key-ID` | 签名所用密钥的 ID(形如 `whk_…`),用它选择对应的 PSP 公钥 | | `X-Webhook-Version` | Header 契约版本,当前固定为 `2026-06-11` | | `X-Webhook-ID` | 投递 ID(UUID)。首次投递、自动重试和商户手动重发都不变 | | `X-Webhook-Event` | 事件类型,如 `payment.intent.completed` | | `X-Webhook-Event-ID` | 事件 ID(UUID)。**推荐的去重键**:自动重试、手动重发和运营重投都保持不变 | | `X-Merchant-Account-ID` | 事件所属的商户账户 ID | 你可能还会看到上表之外的 `X-Webhook-*` Header(例如单次尝试标识),它们不属于接口契约,请不要依赖。 请求体是统一的信封: ```json { "event": "payment.intent.completed", "timestamp": "2026-01-01T00:00:00Z", "data": { } } ``` `data` 的内容因事件而异,完整清单与载荷示例见 [Webhook 事件目录](https://docs.potabank.com/guide/webhook-events.md);机器可读的信封契约见 [API 参考](https://docs.potabank.com/reference/merchant-webhook.md)。 ## 推荐处理顺序 1. 读取原始请求 body。 2. 校验 PSP 签名和时间戳;不要对解析后重新序列化的 JSON 验签。 3. 使用 `X-Webhook-Event-ID` 做持久化去重。 4. 在本地事务内记录事件并提交待处理任务。 5. 尽快返回 `2xx`,把耗时业务处理放到异步消费者。 6. 状态跳跃或数据冲突时调用 Query API 校准。 ## 验签算法 PSP 在每次投递中携带以下 Header: | Header | 说明 | |---|---| | `X-Timestamp` | Unix 秒级时间戳 | | `X-Signature` | Ed25519 签名,标准 Base64 | | `X-Signature-Algorithm` | 当前为 `ed25519` | | `X-Webhook-Key-ID` | 签名所用密钥的 ID,用它选择对应的 PSP 公钥 | 验签步骤: 1. 取原始请求 body 字节,**不要**解析后重新序列化。 2. 待验证消息 = `X-Timestamp` 的字符串 + 一个 `.` + 原始 body 字节,例如 `1767225600.{"event":...}`。 3. 对 `X-Signature` 做标准 Base64(带 `=` 填充)解码,结果应为 64 字节,用 `X-Webhook-Key-ID` 对应的 PSP 公钥做 Ed25519 验证。 4. 检查 `X-Timestamp` 与当前时间的偏差;建议与请求侧一致,使用 ±60 秒窗口,并用事件 ID 去重以抵御重放。 PSP 公钥与 `key_id` 通过账户 Webhook 配置获取(字段 `key_id`、`public_key`);PSP 轮换签名密钥后 `X-Webhook-Key-ID` 会随之变化,请按 Key ID 选择公钥,而不是写死一把。生产环境的回调地址应使用 HTTPS。 ### 测试向量 使用[认证与签名](https://docs.potabank.com/guide/authentication.md)中同一组测试密钥(公钥 `A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=`): - `X-Timestamp`:`1767225600` - `X-Webhook-Key-ID`:`whk_test_vector`(示例值,仅用于接收端示例) - 原始 body(一行,无尾部换行): ```json {"event":"payment.intent.expired","timestamp":"2026-01-01T00:00:00Z","data":{"account_id":"019ba648-1d00-7000-8000-000000000003","classification":{"classification_status":"resolved","payment_category":"bank_transfer","payment_method":"pix","rail_variant":"pix_qr"},"classification_schema_version":1,"expired_at":"2026-01-01T00:00:00Z","intent_id":"019ba648-1d00-7000-8000-000000000001","merchant_id":"019ba648-1d00-7000-8000-000000000002"}} ``` - 待验证消息为 `1767225600.` 后紧接上面的 body(中间无空格、无换行)。 - 期望的 `X-Signature`: ```text loq3l3e4MFbVYT+TOsjlCFltloDnqL/kEpW0Kl3Eo9Eu3Vo9TPJWRYHBjX+jeE+WJVwM/vXL9GOi9UTih5S7DA== ``` ## 接收端示例 两个示例都只用标准库,演示“验签 → 去重 → 快速返回”。示例中的去重集合是内存实现,生产环境请改为数据库唯一约束。构建本站时会用文档里的测试向量实际运行这些代码。 ```js title="Node.js 接收端" import http from 'node:http'; import crypto from 'node:crypto'; const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex'); // key_id -> 32 字节原始公钥的 Base64(来自账户 Webhook 配置;轮换期间请同时保留新旧两把) const PUBLIC_KEYS = new Map([[process.env.PSP_WEBHOOK_KEY_ID, process.env.PSP_WEBHOOK_PUBLIC_KEY]]); const seen = new Set(); export function verify({ keyId, timestamp, signature, rawBody }) { const publicKey = PUBLIC_KEYS.get(keyId); if (!publicKey || !timestamp || !signature) return false; const key = crypto.createPublicKey({ key: Buffer.concat([SPKI_PREFIX, Buffer.from(publicKey, 'base64')]), format: 'der', type: 'spki', }); const message = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]); return crypto.verify(null, message, key, Buffer.from(signature, 'base64')); } export async function webhookHandler(req, res) { const chunks = []; for await (const chunk of req) chunks.push(chunk); const rawBody = Buffer.concat(chunks); // 必须使用原始字节,不要先 JSON.parse 再序列化 const h = req.headers; const fresh = Math.abs(Date.now() / 1000 - Number(h['x-timestamp'])) <= 60; if (!fresh || !verify({ keyId: h['x-webhook-key-id'], timestamp: h['x-timestamp'], signature: h['x-signature'], rawBody })) { res.writeHead(401).end(); // 验签失败:4xx 不会被 PSP 自动重试 return; } const eventId = h['x-webhook-event-id']; if (!seen.has(eventId)) { seen.add(eventId); // 在这里把 rawBody 持久化并投递到异步队列,不要在请求线程里做耗时业务 } res.writeHead(200).end(); // 重复事件同样返回成功 } http.createServer(webhookHandler).listen(8080); ``` ```go title="Go 验签函数" package webhook import ( "crypto/ed25519" "encoding/base64" ) // VerifyWebhook 校验 PSP 的 Webhook 签名。 // publicKeyB64 是账户 Webhook 配置返回的 public_key(32 字节原始公钥的 Base64), // timestamp 与 signatureB64 分别取自 X-Timestamp 与 X-Signature,rawBody 是未经任何处理的请求体字节。 func VerifyWebhook(publicKeyB64, timestamp, signatureB64 string, rawBody []byte) bool { publicKey, err := base64.StdEncoding.DecodeString(publicKeyB64) if err != nil || len(publicKey) != ed25519.PublicKeySize { return false } signature, err := base64.StdEncoding.DecodeString(signatureB64) if err != nil || len(signature) != ed25519.SignatureSize { return false } message := append([]byte(timestamp+"."), rawBody...) return ed25519.Verify(ed25519.PublicKey(publicKey), message, signature) } ``` ## 投递与重试 **成功判定。** 只有 `2xx` 响应算投递成功。请在 **10 秒内返回响应头**,整体不要超过 30 秒。不要用重定向(`3xx`):PSP 虽会跟随,但每一跳都要重新通过地址校验,且 `POST` 可能被改写为 `GET`。 **自动重试。** 失败后当前最多自动重试 5 次(含首次共 6 次尝试),第 n 次失败后等待 `min(1s × 2^(n-1), 60s)`,即 1、2、4、8、16 秒。响应带有 `Retry-After` 时(秒数或 HTTP 日期)改用该值,上限 60 秒。 | 情况 | 是否自动重试 | |---|---| | 网络错误、超时、连接失败 | 是 | | `408`、`425`、`429`、所有 `5xx` | 是 | | 其他 `4xx`、所有 `3xx` | **否** | | 地址校验失败、回调配置已停用或删除 | 否 | 重试时请求体与 `X-Webhook-ID`、`X-Webhook-Event-ID`、`X-Webhook-Key-ID` 不变,`X-Timestamp` 与 `X-Signature` 每次重新生成。 **重试耗尽之后。** 投递状态变为 `failed`,不会再自动发送,也没有面向商户的告警。你可以: - 在商户后台或通过[接口](#配置与运维接口)**手动重发**该次投递(`X-Webhook-ID` 与 `X-Webhook-Event-ID` 保持不变); - 或由你的对账任务通过 Query API 补齐状态。 **投递保证。** 至少一次,**可能重复,不保证顺序**。重复的来源包括:你的响应超时但实际已处理、手动重发、运营重投。请以 `X-Webhook-Event-ID` 做幂等,并以 payload 中的状态与时间、或回查订单为准处理乱序。 不要用 `X-Timestamp` 或 `X-Signature` 去重,它们每次尝试都不同。 ## 回调地址要求 - 必须是 **HTTPS**,端口只能是 **443**。 - 主机必须是域名:不能是 `localhost` 或任何 IP 地址,URL 中不能带用户名密码。 - 域名解析出的**所有** IP 必须是公网地址;解析到内网、回环、链路本地、云元数据等地址会被拒绝,且每次发送前都会重新校验。 - 证书必须是受信任机构签发的有效证书(TLS 1.2 及以上)。 - URL 存储列长为 500 个字符,更长的地址无法保存;保存时会去掉末尾的 `/`。 - 可设置 `allowed_domains` 限制回调主机(精确域名、`*.example.com` 仅子域、`.example.com` 含根域和子域);设置后回调地址及每一跳重定向都必须命中。 - 可配置自定义 Header(例如网关鉴权),但不能覆盖 `Content-Type`、`User-Agent`、签名相关和 `X-Webhook-*`、`X-Merchant-Account-ID`。 - 事件产生时,回调 URL 就已固定到该次投递上:之后修改 URL,**已产生的投递重试仍发往旧地址**,新事件才使用新地址。 ## 配置与运维接口 以下接口属于商户后台(Merchant Portal)API,使用后台登录凭证和相应权限调用,**不是** Gateway 的 API Key + Ed25519 签名。路径前缀为 `/api/v1`。 | 操作 | 方法与路径 | 说明 | |---|---|---| | 设置回调 | `PUT /accounts/{account_id}/webhook` | 请求体:`url`(必填,HTTPS)、`allowed_domains`、`headers`、`enabled`、`event_type`(留空或 `*` 表示默认配置)。首次配置自动生成签名密钥 | | 查询配置 | `GET /accounts/{account_id}/webhook` | 返回 `url`、`enabled`,以及 `key_id`、`public_key`、`public_key_fingerprint`、`signature_algorithm` | | 列出配置 | `GET /accounts/{account_id}/webhooks` | 按事件列出配置 | | 启用 / 停用 | `POST /accounts/{account_id}/webhook/enable`、`/disable` | 停用后不再产生新投递,在途的重试会终止 | | 删除配置 | `DELETE /accounts/{account_id}/webhook` | 返回 `204` | | 投递历史 | `GET /accounts/{account_id}/webhook/deliveries` | 分页:`limit`(1–100,默认 20)、`offset` | | 手动重发 | `POST /accounts/{account_id}/webhook/deliveries/{delivery_id}/retry` | 重发同一投递;已成功的投递也可再次发送 | | 准备新密钥 | `POST /accounts/{account_id}/webhook/signing-keys/prepare` | 生成下一把密钥,只返回公钥信息 | | 启用新密钥 | `POST /accounts/{account_id}/webhook/signing-keys/activate` | 未先 prepare 时返回 `409 WEBHOOK_SIGNING_KEY_NOT_PREPARED` | 私钥由 PSP 生成并加密保存,任何接口都不会返回私钥。`public_key` 是 32 字节原始 Ed25519 公钥的标准 Base64。 ## 密钥轮换 1. 调用 `signing-keys/prepare`,得到新的 `key_id` 与 `public_key`。 2. 把新公钥部署到接收端,**与旧公钥并存**。 3. 调用 `signing-keys/activate`。此后新事件使用新密钥签名。 4. 旧密钥在启用新密钥后仍保留 24 小时,仍在重试中的旧投递继续用旧 `X-Webhook-Key-ID` 签名。接收端应保留新旧两把公钥至少 24 小时,再移除旧公钥。 ## 常见问题 | 现象 | 常见原因 | |---|---| | 验签总是失败 | 对解析后重新序列化的 JSON 验签;消息没有以 `X-Timestamp` 加 `.` 开头;没有按 `X-Webhook-Key-ID` 选公钥(例如轮换后只留了旧公钥);Base64 解码错误 | | 收到重复事件 | 属于正常的至少一次投递;用 `X-Webhook-Event-ID` 去重并返回 `2xx` | | 事件顺序和业务顺序不一致 | 不保证顺序;以事件内状态为准,必要时回查订单 | | 长时间没收到某个事件 | 重试窗口已耗尽:查看投递历史并手动重发,或由对账任务补齐 | | 回调配置保存失败 | 地址不是 HTTPS/443、使用了 IP 或内网域名、URL 超过 500 个字符(存储列长度限制) | ## 状态机原则 - 不依赖事件到达顺序。 - 终态不可被较早的非终态事件回退。 - 同一事件重复投递应返回成功,而不是再次执行业务副作用。 - 金额、币种、商户订单号和账户必须与本地订单匹配后才能入账。 ## 支付分类 代收、代付及信用卡生命周期事件的 `data.classification` 只包含创建时固化的 `classification_status`、`payment_category`、`payment_method` 和 `rail_variant`。 它不包含 runtime taxonomy version;目录 tuple 后续变为 `inactive` 只阻止新订单, 不会改写、取消或中断已接受订单。`classification_schema_version` 是 Webhook payload 结构版本,仍用于事件合同治理,与 taxonomy 无关。 ## 响应时间 验签、持久化并入队后立即返回成功。不要在 Webhook 请求线程中调用下游发货、短信、邮件或长事务。 ## 失败恢复 持续失败时应通过告警发现,并由定时对账任务使用 Query API 补齐状态。完整的重试规则见上文[投递与重试](#投递与重试),信封契约见[商户 Webhook API 参考](https://docs.potabank.com/reference/merchant-webhook.md),各事件的载荷见 [Webhook 事件目录](https://docs.potabank.com/guide/webhook-events.md)。 --- # Webhook 事件目录 本页列出商户会收到的全部 Webhook 事件:触发条件、`data` 字段和载荷示例。接收端的实现、验签、重试与回调地址要求见 [Webhook 对接指南](https://docs.potabank.com/guide/webhooks.md);信封与请求头的机器可读契约见 [API 参考](https://docs.potabank.com/reference/merchant-webhook.md)。 ## 事件一览 | `event` | 触发条件 | 带分类字段 | |---|---|---| | [`payment.intent.completed`](#代收事件) | 代收订单收款完成(含多笔付款累计到全额) | 是 | | [`payment.intent.partially_paid`](#代收事件) | 代收订单收到部分付款、尚未付满 | 是 | | [`payment.intent.expired`](#代收事件) | 代收订单超时过期 | 是 | | [`payment.intent.canceled`](#代收事件) | 商户或系统取消代收订单 | 是 | | [`payment.transfer.completed`](#代付事件) | 代付成功 | 是 | | [`payment.transfer.failed`](#代付事件) | 代付失败(含风控拒绝) | 是 | | [`payment.transfer.returned`](#代付事件) | 已成功的代付被银行退回 | 是 | | [`creditcard_invoice.captured`](#信用卡事件) | 信用卡账单扣款成功 | 是 | | [`creditcard_invoice.3ds_timeout`](#信用卡事件) | 3DS 验证超时未完成 | 是 | | [`payment.refund.completed`](#退款完成) | 代收订单的退款完成 | 否 | | [`payment.transfer_refund.completed`](#退款完成) | 代付退回(transfer refund)完成 | 否 | | [`account.frozen`](#账户冻结与解冻) / `account.unfrozen` | 账户被冻结 / 解冻 | 否 | | [`merchant_settlement.completed.v1`](#商户结算完成) | 商户结算项结算完成 | 不适用(信封不同) | | [`withdrawal.completed`](#提现与旧版结算状态事件) / `withdrawal.failed` | 提现完成 / 失败 | 不适用(扁平体) | | [`settlement.completed`](#提现与旧版结算状态事件) / `settlement.failed` / `settlement.rejected` / `settlement.canceled` | 旧版结算流程的状态变更 | 不适用(扁平体) | > **并非所有事件在所有环境开启** > `withdrawal.*`、`settlement.*` 只在对应的提现、旧版结算流程启用时才会发送。`account.frozen`、`account.unfrozen` 和 `merchant_settlement.completed.v1` 依赖 PSP 的“通知源”兜底通道,是否在你的环境启用,请向接入团队确认后再依赖。你收到的事件以 `X-Webhook-Event` 为准;**遇到本页未列出的 `event`,请记录并返回 `2xx`,不要报错**,PSP 后续可能新增事件类型。 ## 通用约定 **信封。** 支付、代付、退款、信用卡与账户事件的请求体都是: ```json { "event": "…", "timestamp": "2026-01-01T00:00:00Z", "data": { } } ``` **例外:以下事件不使用上面的信封。** 事件类型都在 `event_type`(与请求头 `X-Webhook-Event` 相同),没有 `event` 字段;事件时间的字段名因类型而异(见下): - 新版结算 [`merchant_settlement.completed.v1`](#商户结算完成):有自己的顶层字段(`schema_version`、`event_id`、`event_type`、`occurred_at` 等),事件时间是 `occurred_at`,没有 `timestamp`;**业务内容仍在顶层 `data` 对象里**。 - [提现与旧版结算状态事件](#提现与旧版结算状态事件)(`withdrawal.*`、`settlement.*`):完全扁平,**没有 `data`**,业务字段都在顶层,**有顶层 `timestamp`**(状态变更时间)。 接收端应先读取请求头 `X-Webhook-Event`,再按事件选择解析方式,不要假定所有请求体都有 `event` 与 `data`。 `timestamp` 是事件发生时间(RFC3339,可能带小数秒),与 `X-Timestamp`(发送时间)不是一回事。 **金额与币种。** 金额字段都是 **JSON 整数,单位为最小货币单位**(`100000` 且 `currency` 为 `BRL` 表示 `R$ 1,000.00`)。`currency` 是 ISO 4217 三位大写字母。**以下金额是十进制字符串,不是整数**:新版结算 `merchant_settlement.completed.v1` 的 `gross_amount`/`merchant_fee_amount`/`net_amount`,旧版结算 `settlement.*` 的 `net_amount`(最小货币单位的整数值,可能为负),以及提现 `withdrawal.*` 的可选字段 `amount`。 **空值。** 字符串缺失时输出 `""`,不是 `null`;对象缺失时输出 `null`。 **字段别名。** 为兼容旧接入,部分字段有同值别名,二选一使用即可:`intent_number` 与 `invoice_number`、`provider_transaction_id` 与 `provider_tx_id`、`merchant_account_id` 与 `account_id`;信用卡事件的 `captured_amount` 与 `paid_amount`、`captured_at` 与 `completed_at`。有些事件只带其中一个名字,以各事件表格为准。 **支付分类。** 标注“带分类字段”的事件,`data` 中固定包含: | 字段 | 说明 | |---|---| | `classification_schema_version` | 整数,当前固定为 `1` | | `classification.classification_status` | 固定为 `resolved` | | `classification.payment_category` | 支付大类,小写下划线,如 `bank_transfer` | | `classification.payment_method` | 支付方式,如 `pix`、`spei` | | `classification.rail_variant` | 通道变体,如 `pix_qr`、`standard` | 分类在订单创建时固化,之后不会改变。 **重放与重发。** 运营侧“重发”会原样重放已存储的载荷,`X-Webhook-Event-ID` 不变;“刷新回调”会按订单当前状态重新生成载荷,字段与首次投递一致(含 `classification`)。无法如实还原的事件(例如分多笔付款的订单的“代收完成”、SPEI 订单的“代收完成”、代收“部分支付”)不会被重新生成,而是直接拒绝。在此之前生成并存储的旧载荷可能缺少个别字段(如退款的 `original_intent_type`、`fee_amount`),请按可选字段处理;金额与状态始终以 Query API 回查结果为准。 ## 代收事件 ### payment.intent.completed 代收订单收款完成时发送;多笔付款累计到全额时,在最后一笔到账后发送。 | 字段 | 类型 | 说明 | |---|---|---| | `intent_id` | string(uuid) | 代收订单 ID | | `intent_number` / `invoice_number` | string | 订单号(别名,值相同) | | `merchant_id` | string(uuid) | 商户 ID | | `merchant_account_id` / `account_id` | string(uuid) | 商户账户 ID(别名,值相同) | | `amount` | integer | 订单金额 | | `paid_amount` | integer | 累计已付金额 | | `fee_amount` | integer | 商户手续费 | | `net_amount` | integer | 净额:允许不同金额时为 `paid_amount - fee_amount`,否则为 `amount - fee_amount` | | `current_payment_amount` | integer | 本次到账金额 | | `remaining_amount` | integer | 剩余应付金额,完成时为 `0` | | `is_partial_payment` / `is_final_payment` | boolean | 恒为 `false` / `true` | | `payment_policy` | object | `allow_different_amount`、`allow_multiple_payments`(boolean) | | `currency` | string | 币种 | | `description` | string | 订单描述,可为 `""` | | `merchant_order_id` | string | 你的订单号,可为 `""` | | `provider_transaction_id` / `provider_tx_id` | string | 渠道流水号(别名),可为 `""` | | `payer_info` | object 或 null | 付款人信息:`cpf_cnpj`、`name`、`email`、`phone`、`account`;无付款人时为 `null` | | `payer_name` / `payer_tax_id` | string | 付款人姓名 / 证件号,可为 `""` | | `completed_at` | string(date-time) | 完成时间 | | `classification_*` | — | 支付分类,见上文 | ```json { "event": "payment.intent.completed", "timestamp": "2026-01-01T00:00:00Z", "data": { "intent_id": "019ba648-1d00-7000-8000-000000000001", "intent_number": "PI_20260101_001", "invoice_number": "PI_20260101_001", "merchant_id": "019ba648-1d00-7000-8000-000000000002", "merchant_account_id": "019ba648-1d00-7000-8000-000000000003", "account_id": "019ba648-1d00-7000-8000-000000000003", "amount": 100000, "paid_amount": 100000, "fee_amount": 500, "net_amount": 99500, "current_payment_amount": 100000, "remaining_amount": 0, "is_partial_payment": false, "is_final_payment": true, "payment_policy": { "allow_different_amount": false, "allow_multiple_payments": false }, "currency": "BRL", "description": "Test invoice", "merchant_order_id": "ORDER_20260101_001", "provider_transaction_id": "pix-tx-001", "provider_tx_id": "pix-tx-001", "payer_info": { "cpf_cnpj": "00000000000", "name": "Test Payer", "email": "payer@example.com", "phone": "", "account": "" }, "payer_name": "Test Payer", "payer_tax_id": "00000000000", "completed_at": "2026-01-01T00:00:00Z", "classification_schema_version": 1, "classification": { "classification_status": "resolved", "payment_category": "bank_transfer", "payment_method": "pix", "rail_variant": "pix_qr" } } } ``` ### payment.intent.partially_paid 允许分多笔付款的订单收到一笔付款但尚未付满时发送。与 `completed` 相比,没有 `merchant_account_id`、`fee_amount`、`net_amount`、`description`、`merchant_order_id`、`provider_transaction_id`、`payer_info` 和 `completed_at`。 | 字段 | 类型 | 说明 | |---|---|---| | `intent_id` | string(uuid) | 代收订单 ID | | `intent_number` / `invoice_number` | string | 订单号(别名) | | `merchant_id` / `account_id` | string(uuid) | 商户 ID / 账户 ID | | `amount` | integer | 订单金额 | | `paid_amount` | integer | 累计已付金额 | | `current_payment_amount` | integer | 本次到账金额 | | `remaining_amount` | integer | 剩余应付金额 | | `is_partial_payment` / `is_final_payment` | boolean | 恒为 `true` / `false` | | `payment_policy` | object | 同 `completed` | | `currency` | string | 币种 | | `provider_tx_id` | string | 渠道流水号 | | `payer_name` / `payer_tax_id` | string | 付款人姓名 / 证件号 | | `partially_paid_at` | string(date-time) | 本次部分付款时间 | | `classification_*` | — | 支付分类 | ```json { "event": "payment.intent.partially_paid", "timestamp": "2026-01-01T00:00:00Z", "data": { "intent_id": "019ba648-1d00-7000-8000-000000000001", "intent_number": "PI_20260101_002", "invoice_number": "PI_20260101_002", "merchant_id": "019ba648-1d00-7000-8000-000000000002", "account_id": "019ba648-1d00-7000-8000-000000000003", "amount": 100000, "paid_amount": 70000, "current_payment_amount": 30000, "remaining_amount": 30000, "is_partial_payment": true, "is_final_payment": false, "payment_policy": { "allow_different_amount": true, "allow_multiple_payments": true }, "currency": "BRL", "provider_tx_id": "pix-tx-002", "payer_name": "Test Payer", "payer_tax_id": "00000000000", "partially_paid_at": "2026-01-01T00:00:00Z", "classification_schema_version": 1, "classification": { "classification_status": "resolved", "payment_category": "bank_transfer", "payment_method": "pix", "rail_variant": "pix_qr" } } } ``` ### payment.intent.expired 代收订单在有效期内未付款而过期时发送。**不含订单号和 `merchant_order_id`**,请用 `intent_id` 关联。 | 字段 | 类型 | 说明 | |---|---|---| | `intent_id` | string(uuid) | 代收订单 ID | | `merchant_id` / `account_id` | string(uuid) | 商户 ID / 账户 ID | | `expired_at` | string(date-time) | 过期时间 | | `classification_*` | — | 支付分类 | ```json { "event": "payment.intent.expired", "timestamp": "2026-01-01T00:00:00Z", "data": { "intent_id": "019ba648-1d00-7000-8000-000000000001", "merchant_id": "019ba648-1d00-7000-8000-000000000002", "account_id": "019ba648-1d00-7000-8000-000000000003", "expired_at": "2026-01-01T00:00:00Z", "classification_schema_version": 1, "classification": { "classification_status": "resolved", "payment_category": "bank_transfer", "payment_method": "pix", "rail_variant": "pix_qr" } } } ``` ### payment.intent.canceled 商户或系统取消代收订单时发送。订单引用来自原订单:没有提供时为 `""`;历史事件中部分引用可能缺失。请在账户范围内优先用 `intent_id` 或 `merchant_order_id` 关联,不要要求一定存在渠道流水号。 | 字段 | 类型 | 说明 | |---|---|---| | `intent_id` | string(uuid) | 代收订单 ID | | `intent_number` / `invoice_number` | string | 订单号(别名);历史事件可能为空 | | `merchant_order_id` | string | 你的订单号,可为 `""` | | `provider_transaction_id` / `provider_tx_id` | string | 渠道流水号(别名),提交渠道前为 `""` | | `merchant_id` / `account_id` | string(uuid) | 商户 ID / 账户 ID | | `cancel_reason` | string | 取消原因 | | `canceled_at` | string(date-time) | 取消时间 | | `classification_*` | — | 支付分类 | ```json { "event": "payment.intent.canceled", "timestamp": "2026-01-01T00:00:00Z", "data": { "intent_id": "019ba648-1d00-7000-8000-000000000001", "intent_number": "PI_20260101_003", "invoice_number": "PI_20260101_003", "merchant_order_id": "ORDER_20260101_003", "provider_transaction_id": "", "provider_tx_id": "", "merchant_id": "019ba648-1d00-7000-8000-000000000002", "account_id": "019ba648-1d00-7000-8000-000000000003", "cancel_reason": "customer_request", "canceled_at": "2026-01-01T00:00:00Z", "classification_schema_version": 1, "classification": { "classification_status": "resolved", "payment_category": "bank_transfer", "payment_method": "pix", "rail_variant": "pix_qr" } } } ``` ## 代付事件 ### payment.transfer.completed 代付成功时发送。 | 字段 | 类型 | 说明 | |---|---|---| | `transfer_id` / `transfer_number` | string | 代付订单 ID / 订单号 | | `merchant_id` | string(uuid) | 商户 ID | | `merchant_account_id` / `account_id` | string(uuid) | 商户账户 ID(别名) | | `amount` | integer | 代付本金 | | `transferred_amount` | integer | 实际转出金额 | | `fee_amount` | integer | 商户手续费 | | `total_amount` | integer | `amount + fee_amount` | | `currency` | string | 币种 | | `description` | string | 描述,可为 `""` | | `merchant_order_id` | string | 你的订单号 | | `provider_transaction_id` / `provider_tx_id` | string | 渠道流水号(别名) | | `beneficiary_info` | object 或 null | 收款人:`key_type`(恒为 `""`)、`key_value`、`name`、`document`、`email`、`phone`、`bank_code`、`branch_code` | | `e2e_id` | string | 端到端标识,**仅在有值时出现** | | `settlement_id` | string | 结算 ID,**仅在有值时出现** | | `completed_at` | string(date-time) | 完成时间 | | `classification_*` | — | 支付分类 | ```json { "event": "payment.transfer.completed", "timestamp": "2026-01-01T00:00:00Z", "data": { "transfer_id": "019ba648-1d00-7000-8000-000000000001", "transfer_number": "TI_20260101_001", "merchant_id": "019ba648-1d00-7000-8000-000000000002", "merchant_account_id": "019ba648-1d00-7000-8000-000000000003", "account_id": "019ba648-1d00-7000-8000-000000000003", "amount": 50000, "transferred_amount": 50000, "fee_amount": 250, "total_amount": 50250, "currency": "BRL", "description": "Test transfer", "merchant_order_id": "PAYOUT_20260101_001", "provider_transaction_id": "pix-transfer-001", "provider_tx_id": "pix-transfer-001", "beneficiary_info": { "key_type": "", "key_value": "recipient@example.com", "name": "Maria Santos", "document": "00000000000", "email": "recipient@example.com", "phone": "", "bank_code": "001", "branch_code": "0001" }, "e2e_id": "E00000000202601010000abcdefghijk", "completed_at": "2026-01-01T00:00:00Z", "classification_schema_version": 1, "classification": { "classification_status": "resolved", "payment_category": "bank_transfer", "payment_method": "pix", "rail_variant": "pix_qr" } } } ``` ### payment.transfer.failed 代付失败时发送;风控拒绝时 `failure_code` 为 `RISK_REJECTED`。 | 字段 | 类型 | 说明 | |---|---|---| | `transfer_id` / `transfer_number` | string | 代付订单 ID / 订单号 | | `merchant_id` | string(uuid) | 商户 ID | | `merchant_account_id` / `account_id` | string(uuid) | 商户账户 ID | | `amount` | integer | 代付金额 | | `currency` | string | 币种 | | `merchant_order_id` | string | 你的订单号 | | `failure_reason` | string | 失败原因 | | `failure_code` | string | 失败代码 | | `failed_at` | string(date-time) | 失败时间 | | `classification_*` | — | 支付分类 | ```json { "event": "payment.transfer.failed", "timestamp": "2026-01-01T00:00:00Z", "data": { "transfer_id": "019ba648-1d00-7000-8000-000000000001", "merchant_id": "019ba648-1d00-7000-8000-000000000002", "merchant_account_id": "019ba648-1d00-7000-8000-000000000003", "account_id": "019ba648-1d00-7000-8000-000000000003", "transfer_number": "TI_20260101_002", "amount": 50000, "currency": "BRL", "merchant_order_id": "PAYOUT_20260101_002", "failure_reason": "Insufficient funds", "failure_code": "INSUFFICIENT_FUNDS", "failed_at": "2026-01-01T00:00:00Z", "classification_schema_version": 1, "classification": { "classification_status": "resolved", "payment_category": "bank_transfer", "payment_method": "pix", "rail_variant": "pix_qr" } } } ``` ### payment.transfer.returned 已成功的代付被银行退回时发送。`returned_amount` 是**累计**退回金额,多次部分退回会多次推送。 | 字段 | 类型 | 说明 | |---|---|---| | `transfer_id` / `transfer_number` | string | 代付订单 ID / 订单号 | | `merchant_id` | string(uuid) | 商户 ID | | `merchant_account_id` / `account_id` | string(uuid) | 商户账户 ID | | `amount` | integer | 原代付金额 | | `currency` | string | 币种 | | `merchant_order_id` | string | 你的订单号 | | `returned_amount` | integer | 累计退回金额 | | `return_reason` | string | 退回原因 | | `returned_at` | string(date-time) | 退回时间 | | `classification_*` | — | 支付分类 | ```json { "event": "payment.transfer.returned", "timestamp": "2026-01-01T00:00:00Z", "data": { "transfer_id": "019ba648-1d00-7000-8000-000000000001", "merchant_id": "019ba648-1d00-7000-8000-000000000002", "merchant_account_id": "019ba648-1d00-7000-8000-000000000003", "account_id": "019ba648-1d00-7000-8000-000000000003", "transfer_number": "TI_20260101_003", "amount": 50000, "currency": "BRL", "merchant_order_id": "PAYOUT_20260101_003", "returned_amount": 50000, "return_reason": "Account closed", "returned_at": "2026-01-01T00:00:00Z", "classification_schema_version": 1, "classification": { "classification_status": "resolved", "payment_category": "bank_transfer", "payment_method": "pix", "rail_variant": "pix_qr" } } } ``` ## 信用卡事件 ### creditcard_invoice.captured 信用卡账单扣款成功时发送。 | 字段 | 类型 | 说明 | |---|---|---| | `invoice_id` / `invoice_number` | string | 账单 ID / 账单号 | | `merchant_id` | string(uuid) | 商户 ID | | `merchant_account_id` / `account_id` | string(uuid) | 商户账户 ID | | `channel_id` | string | 通道 ID | | `amount` | integer | 账单金额 | | `captured_amount` / `paid_amount` | integer | 扣款金额(别名) | | `fee_amount` | integer | 商户手续费 | | `channel_fee` | integer | 通道费用 | | `net_amount` | integer | 净额 | | `currency` | string | 币种 | | `description` | string | 描述 | | `merchant_order_id` | string | 你的订单号 | | `provider_transaction_id` / `provider_tx_id` | string | 渠道流水号(别名) | | `captured_at` / `completed_at` | string(date-time) | 扣款时间(别名) | | `classification_*` | — | 支付分类 | ### creditcard_invoice.3ds_timeout 3DS 验证在规定时间内未完成时发送。 | 字段 | 类型 | 说明 | |---|---|---| | `invoice_id` | string | 账单 ID | | `merchant_id` | string(uuid) | 商户 ID | | `merchant_account_id` / `account_id` | string(uuid) | 商户账户 ID | | `three_ds_transaction_id` | string | 3DS 交易 ID | | `timeout_seconds` | integer | 超时时长(秒) | | `initiated_at` | string(date-time) | 3DS 发起时间 | | `timeout_at` | string(date-time) | 超时时间 | | `classification_*` | — | 支付分类 | ## 退款完成 `payment.refund.completed`(代收退款)与 `payment.transfer_refund.completed`(代付退回)使用同一结构,**不带支付分类字段**。`data` 中没有账户 ID,所属账户请读取请求头 `X-Merchant-Account-ID`。代付退回完成时,除本事件外还会发送 `payment.transfer.returned`。 | 字段 | 类型 | 说明 | |---|---|---| | `refund_id` | string(uuid) | 退款 ID | | `merchant_id` | string(uuid) | 商户 ID | | `original_intent_id` | string(uuid) | 原订单 ID | | `original_intent_type` | string | `payment_intent`(代收)或 `transfer_intent`(代付) | | `refunded_amount` | integer | 退款金额 | | `fee_amount` | integer | 手续费,可为 `0` | | `currency` | string | 币种 | | `provider_refund_id` | string | 渠道退款流水号,可为 `""` | | `completed_at` | string(date-time) | 完成时间 | ```json { "event": "payment.refund.completed", "timestamp": "2026-01-01T00:00:00Z", "data": { "refund_id": "019ba648-1d00-7000-8000-000000000001", "merchant_id": "019ba648-1d00-7000-8000-000000000002", "original_intent_id": "019ba648-1d00-7000-8000-000000000004", "original_intent_type": "payment_intent", "refunded_amount": 9800, "fee_amount": 0, "currency": "BRL", "provider_refund_id": "refund-001", "completed_at": "2026-01-01T00:00:00Z" } } ``` 退款失败不会产生首次 Webhook 通知(`payment.refund.failed` 只可能在运营重放既有记录时出现,载荷为 `refund_id`、`failure_reason`、`failure_code`、`failed_at`)。请通过 Query API 查询退款状态。 ## 账户冻结与解冻 账户被冻结或解冻时发送,不带支付分类字段。 | 字段 | 类型 | 说明 | |---|---|---| | `account_id` | string(uuid) | 商户账户 ID | | `reason` | string | 冻结原因(仅 `account.frozen`) | | `frozen_at` | string(date-time) | 冻结时间(仅 `account.frozen`) | | `unfrozen_at` | string(date-time) | 解冻时间(仅 `account.unfrozen`) | ```json { "event": "account.frozen", "timestamp": "2026-01-01T00:00:00Z", "data": { "account_id": "019ba648-1d00-7000-8000-000000000003", "reason": "compliance_review", "frozen_at": "2026-01-01T00:00:00Z" } } ``` ## 商户结算完成 `merchant_settlement.completed.v1` 的**信封与其他事件不同**:顶层没有 `event` 和 `timestamp`,事件类型在 `event_type`(同时也在请求头 `X-Webhook-Event`),`X-Webhook-Event-ID` 等于 `event_id`。金额是**十进制字符串**,时间为 UTC 微秒精度。 特别注意投递规则:这类事件收到**任何非 `2xx`** 响应都会重试,并且忽略 `Retry-After`。 | 字段 | 类型 | 说明 | |---|---|---| | `schema_version` | string | 当前为 `1.0.0` | | `event_id` | string(uuid) | 事件 ID,去重键 | | `event_type` | string | `merchant_settlement.completed.v1` | | `occurred_at` | string(date-time) | 事件发生时间(UTC,微秒精度) | | `aggregate_type` / `aggregate_id` / `aggregate_version` | string / string(uuid) / integer | 结算项聚合标识与版本 | | `data.merchant_item_id` | string(uuid) | 结算项 ID | | `data.merchant_account_id` | string(uuid) | 商户账户 ID | | `data.source_type` | string | 结算来源,如 `invoice` | | `data.status` | string | 结算状态,如 `settled` | | `data.gross_amount` / `merchant_fee_amount` / `net_amount` | string | 总额 / 手续费 / 净额,满足 `gross = fee + net`,十进制字符串 | | `data.currency` | string | 币种 | | `data.due_at` / `data.settled_at` | string(date-time) | 应结算时间 / 实际结算时间 | | `data.parent_item_id` / `data.relation_type` | string 或 null | 关联的父结算项及关系,无则为 `null` | ```json { "schema_version": "1.0.0", "event_id": "019ba648-1d00-7000-8000-0000000000a1", "event_type": "merchant_settlement.completed.v1", "occurred_at": "2026-01-01T04:00:00.123456Z", "aggregate_type": "merchant_settlement_item", "aggregate_id": "019ba648-1d00-7000-8000-0000000000a2", "aggregate_version": 3, "data": { "merchant_item_id": "019ba648-1d00-7000-8000-0000000000a2", "merchant_account_id": "019ba648-1d00-7000-8000-000000000003", "source_type": "invoice", "status": "settled", "gross_amount": "100000", "merchant_fee_amount": "500", "net_amount": "99500", "currency": "BRL", "due_at": "2026-01-01T03:00:00Z", "settled_at": "2026-01-01T04:00:00.123456Z", "parent_item_id": null, "relation_type": null } } ``` ## 提现与旧版结算状态事件 以下事件的请求体是**扁平结构**,没有 `event` / `data` 信封;事件类型在 `event_type`(与请求头 `X-Webhook-Event` 相同),状态名等于事件类型的后缀。它们只在对应的提现、旧版结算流程启用时发送,请向接入团队确认你的环境是否会收到。旧版结算的 `settlement.*` 与新版 `merchant_settlement.completed.v1` 是两套不同的事件,不要混淆。 ### withdrawal.completed / withdrawal.failed | 字段 | 类型 | 说明 | |---|---|---| | `event_type` | string | `withdrawal.completed` 或 `withdrawal.failed` | | `withdrawal_id` | string(uuid) | 提现 ID | | `status` | string | `completed` 或 `failed`(终态) | | `timestamp` | string(date-time) | 状态变更时间,UTC,秒级精度 | | `amount`、`currency`、`channel_ref`、`e2e_id`、`error_code`、`error_message` | string | 发送器支持,但当前提现流程**不会填充**,缺失时不出现,请按可选字段处理 | ```json { "event_type": "withdrawal.completed", "withdrawal_id": "019ba648-1d00-7000-8000-0000000000b1", "status": "completed", "timestamp": "2026-01-01T00:00:00Z" } ``` ### settlement.completed / settlement.failed / settlement.rejected / settlement.canceled | 字段 | 类型 | 说明 | |---|---|---| | `event_type` | string | 上述四个事件之一 | | `settlement_id` | string(uuid) | 结算 ID | | `status` | string | `completed`、`failed`、`rejected` 或 `canceled` | | `timestamp` | string(date-time) | 状态变更时间,UTC,秒级精度 | | `net_amount` | string | 结算金额,最小货币单位的整数,**十进制字符串**,可能为负数 | | `currency` | string | 币种;结算没有币种时不出现 | ```json { "event_type": "settlement.completed", "settlement_id": "019ba648-1d00-7000-8000-0000000000c1", "status": "completed", "timestamp": "2026-01-01T00:00:00Z", "net_amount": "99500", "currency": "BRL" } ``` ## 处理建议 - 先按 `X-Webhook-Event` 分发,再按事件选择解析方式,共三类: 1. 普通 `event`/`timestamp`/`data` 信封:解析 `data`。 2. `merchant_settlement.*.v1`:先读顶层的 `event_id`、`event_type` 等字段,业务内容再解析顶层的 `data`。 3. `withdrawal.*`、`settlement.*`:完全扁平,直接读取顶层字段,没有 `data`。 未知事件记录后返回 `2xx`。 - 同一订单可能先后收到多个事件(例如 `partially_paid` 之后是 `completed`,`transfer.completed` 之后又出现 `transfer.returned`),不保证顺序,请按[状态机原则](https://docs.potabank.com/guide/webhooks.md#状态机原则)处理。 - 对支付、代付等带 `data` 的事件,入账前核对 `amount`、`currency`、`merchant_order_id` 与账户是否和本地订单一致。 --- # 错误处理 API 使用 HTTP 状态码区分错误类型。调用方应同时记录状态码、错误 code、可公开的 message、请求时间和本地关联 ID;不要记录凭证或完整敏感 body。 | HTTP | 含义 | 处理建议 | |---:|---|---| | 400 | 参数不合法、缺少 `X-API-ID` 或两个幂等 Header 不一致 | 修正请求,不自动重试 | | 401 | 缺少 `X-API-Key`、凭证无效、签名、时间戳、Host 或 credential kind 错误 | 检查签名材料和服务路由 | | 403 | 已认证但 Scope 不足 | 申请最小必要权限 | | 404 | 资源不存在或对当前账户不可见 | 核对 ID 与账户边界 | | 409 | 幂等冲突或状态冲突 | 查询原请求结果,停止盲重试 | | 422 | 同一幂等键配合不同 body,或支付 Rail 已停用等语义错误 | 核对原请求与当前可用 Rail,不自动重试 | | 429 | 触发限流 | 遵循 `Retry-After`,指数退避 | | 5xx | PSP 或上游暂时异常 | 有限重试,并通过 Query 确认最终状态 | 若新订单选择的支付分类已停用,PSP 会在订单被接受前返回 `422 PAYMENT_RAIL_INACTIVE`。 调用方应刷新可用支付方式并重新选择;不要据此停止处理已接受订单,历史订单和在途订单继续按创建时冻结的分类与执行配置推进。 ## 可观测性建议 - 为每次业务意图生成本地 correlation ID。 - 记录 PSP 返回的资源 ID 与商户订单号映射。 - 将重试次数、最终 HTTP 状态和耗时做成指标。 - 对签名失败、幂等冲突、Webhook 堆积和状态长时间未收敛配置告警。 --- # 上线检查清单 ## 凭证与安全 - [ ] Invoice、Transfer、Query 客户端使用独立 Host 配置。 - [ ] 私钥存放在 KMS、HSM 或受控 Secret Manager。 - [ ] 生产环境只申请最小必要 Scope。 - [ ] 服务器时间通过 NTP 同步,偏差稳定小于 60 秒。 - [ ] 日志不包含 API Key、私钥、签名、完整卡号或敏感付款信息。 ## 请求可靠性 - [ ] 所有写请求都有稳定的 `X-Idempotency-Key` 或 `Idempotency-Key`;同时发送时两者一致。 - [ ] 超时重试沿用原始幂等键和相同业务 payload。 - [ ] 已覆盖 `400`、`401`、`403`、`409`、`422`、`429` 与 `5xx` 的处理策略。 - [ ] 不确定结果能够通过 Query API 收敛。 ## Webhook - [ ] 回调地址是 HTTPS(443 端口)域名,已通过商户后台配置;公钥按 `X-Webhook-Key-ID` 保存,轮换时同时保留新旧两把。 - [ ] 使用原始 body 验签,并检查时间戳窗口。 - [ ] `X-Webhook-Event-ID` 已持久化去重(不要用 `X-Timestamp` 或 `X-Signature`)。 - [ ] 可以处理乱序、重复与延迟事件;未知的 `event` 记录后返回 `2xx`。 - [ ] 接收端在 10 秒内返回响应头,业务处理已异步化。 - [ ] 定时对账可以补偿漏收事件,覆盖自动重试耗尽(当前约 30 秒)之后丢失的事件。 - [ ] 已配置投递失败的监控,知道如何在商户后台手动重发。 ## 业务验收 - [ ] 已在测试环境完成创建、查询、取消和退款的正反向场景。 - [ ] 已演练请求超时但 PSP 实际成功的场景。 - [ ] 已验证金额、币种、账户和商户订单号的对账规则。 - [ ] 已配置技术联系人、告警升级路径和回滚方案。 完成清单后,将测试证据、目标商户账户、所需 Scope 与回调域名提交给接入团队进行生产开通审核。 --- # API 参考 每份 OpenAPI 文档对应一个独立服务边界。请先按业务动作选择服务,再下载对应的规范生成客户端。 不同规范拥有独立 Host、认证模型和路由集合。生成 SDK 时应保留 `invoice`、`transfer` 与 `query` 三个客户端。所有商户可用的 OpenAPI 源文件可从各参考页下载。Provider 回调入口属于内部契约,不在本站发布。 - [Invoice API](https://docs.potabank.com/reference/invoice.md):代收、退款、卡支付与 Tokenization - [Transfer API](https://docs.potabank.com/reference/transfer.md):代付创建、取消与订单同步 - [Query API](https://docs.potabank.com/reference/query.md):订单、退款、账户与余额查询 - [商户 Webhook](https://docs.potabank.com/reference/merchant-webhook.md):PSP 向商户推送签名事件 - [Hosted Checkout](https://docs.potabank.com/reference/proof.md):托管支付页面与消费者支付动作 --- # Invoice API - 服务地址(示例,真实 Host 由接入团队提供):`https://invoice-api.psp.example.com` - 认证:Ed25519 签名 - 版本:1.0.0 - OpenAPI 规范:https://docs.potabank.com/openapi/invoice.openapi.yaml Invoice service surface for merchant invoice write flows. This service only accepts invoice account secrets and rejects transfer/query/webhook/proof traffic. A missing X-API-ID header returns 400 INVALID_REQUEST. A missing X-API-Key header or invalid credentials return 401 UNAUTHORIZED. A credential store failure returns 500 INTERNAL_ERROR before the request handler runs. ## Invoice Payment Intents ### Create a invoice payment intent **POST** `/api/v1/payments/intents` Requires a invoice account secret and `invoices:write` scope. Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. Unlike other write operations, this one is not deduplicated by the idempotency key: the gateway keeps no response cache for it, and the key does not decide retries. The invoice service identifies a retry by `merchant_order_id` within the merchant account; reusing a `merchant_order_id` with a different request is rejected with 409 CONFLICT. **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `payment_method` | `enum` | 必填 | 可选值:`pix`、`spei`、`upi`、`credit_card`、`apple_pay`、`google_pay`、`cash_app`、`paypal`、`zelle`、`chime`、`venmo`、`emt`、`boleto` | | `rail_variant` | `string` | 必填 | Canonical rail variant paired with payment_method; cash_app accepts standard/ecashapp, paypal accepts standard/epaypal, and zelle/chime/venmo/emt use standard. | | `amount` | `integer` | 必填 | ≥ 1 | | `currency` | `string` | 必填 | 最短 3 · 最长 3 | | `instruction` | `object` | 可选 | — | | `items` | `object[]` | 可选 | — | | `items[].name` | `string` | 必填 | 最长 160 | | `items[].description` | `string` | 可选 | 最长 500 | | `items[].sku` | `string` | 可选 | 最长 80 | | `items[].quantity` | `integer` | 必填 | ≥ 1 · ≤ 999999 | | `items[].unit_amount` | `integer` | 必填 | ≥ 0 | | `items[].currency` | `string` | 可选 | 最短 3 · 最长 3 | | `items[].image_url` | `string` | 可选 | 最长 2048 | | `payer_info` | `object` | 可选 | — | | `payer_info.name` | `string` | 可选 | 最长 200 | | `payer_info.tax_id` | `string` | 可选 | 付款人证件号。账户 payin_payer_validation_mode 为 optional 或 required 且提交了本字段时,须符合账户市场格式:巴西 CPF(11 位数字含校验位)或 CNPJ (14 位,数字或字母数字,含官方校验位,例如 00.000.000/E08G-12);墨西哥 RFC (12 或 13 位,日期段月份 01–12、日期 01–31)。格式错误返回 INVALID_REQUEST。 required 档未提交亦返回 INVALID_REQUEST。 · 最长 50 | | `payer_info.email` | `string` | 可选 | 最长 200 | | `payer_info.phone` | `string` | 可选 | 最长 50 | | `payer_info.account` | `string` | 可选 | 最长 200 | | `description` | `string` | 可选 | 最长 500 | | `merchant_order_id` | `string` | 必填 | 最长 100 | | `return_url` | `string` | 可选 | HTTPS return URL for provider-hosted checkout or 3DS completion. · 最长 500 | | `webhook_url` | `string` | 可选 | 最长 500 | | `expires_in_minutes` | `integer` | 可选 | ≥ 1 · ≤ 1440 | | `allow_different_amount` | `boolean` | 可选 | — | | `allow_multiple_payments` | `boolean` | 可选 | — | | `metadata` | `object` | 可选 | — | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/payments/intents' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "payment_method": "pix", "rail_variant": "standard", "amount": 1, "currency": "string", "instruction": {}, "items": [ { "name": "string", "description": "string", "sku": "string", "quantity": 1, "unit_amount": 0, "currency": "string", "image_url": "https://example.com" } ], "payer_info": { "name": "string", "tax_id": "string", "email": "user@example.com", "phone": "string", "account": "string" }, "description": "string", "merchant_order_id": "string", "return_url": "https://example.com", "webhook_url": "https://example.com", "expires_in_minutes": 1, "allow_different_amount": false, "allow_multiple_payments": false, "metadata": {} }' ``` **响应** #### 响应 201 Created Payment intent created | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `id` | `string` | 必填 | — | | `invoice_number` | `string` | 必填 | — | | `payment_method` | `string` | 必填 | — | | `classification` | `object` | 可选 | — | | `amount` | `integer` | 必填 | — | | `currency` | `string` | 必填 | — | | `paid_amount` | `integer` | 可选 | — | | `refunded_amount` | `integer` | 可选 | — | | `status` | `string` | 必填 | — | | `state` | `string` | 可选 | — | | `merchant_order_id` | `string` | 可选 | — | | `payment_url` | `string` | 可选 | — | | `psp_checkout_url` | `string` | 可选 | — | | `provider_checkout_url` | `string` | 可选 | — | | `payment_url_source` | `string` | 可选 | — | | `instruction` | `object` | 可选 | — | | `expected_payer_info` | `object` | 可选 | — | | `expected_payer_info.name` | `string` | 可选 | 最长 200 | | `expected_payer_info.tax_id` | `string` | 可选 | 付款人证件号。账户 payin_payer_validation_mode 为 optional 或 required 且提交了本字段时,须符合账户市场格式:巴西 CPF(11 位数字含校验位)或 CNPJ (14 位,数字或字母数字,含官方校验位,例如 00.000.000/E08G-12);墨西哥 RFC (12 或 13 位,日期段月份 01–12、日期 01–31)。格式错误返回 INVALID_REQUEST。 required 档未提交亦返回 INVALID_REQUEST。 · 最长 50 | | `expected_payer_info.email` | `string` | 可选 | 最长 200 | | `expected_payer_info.phone` | `string` | 可选 | 最长 50 | | `expected_payer_info.account` | `string` | 可选 | 最长 200 | | `payer_info` | `object` | 可选 | — | | `payer_info.name` | `string` | 可选 | 最长 200 | | `payer_info.tax_id` | `string` | 可选 | 付款人证件号。账户 payin_payer_validation_mode 为 optional 或 required 且提交了本字段时,须符合账户市场格式:巴西 CPF(11 位数字含校验位)或 CNPJ (14 位,数字或字母数字,含官方校验位,例如 00.000.000/E08G-12);墨西哥 RFC (12 或 13 位,日期段月份 01–12、日期 01–31)。格式错误返回 INVALID_REQUEST。 required 档未提交亦返回 INVALID_REQUEST。 · 最长 50 | | `payer_info.email` | `string` | 可选 | 最长 200 | | `payer_info.phone` | `string` | 可选 | 最长 50 | | `payer_info.account` | `string` | 可选 | 最长 200 | | `items` | `object[]` | 可选 | — | | `items[].name` | `string` | 必填 | 最长 160 | | `items[].description` | `string` | 可选 | 最长 500 | | `items[].sku` | `string` | 可选 | 最长 80 | | `items[].quantity` | `integer` | 必填 | ≥ 1 · ≤ 999999 | | `items[].unit_amount` | `integer` | 必填 | ≥ 0 | | `items[].currency` | `string` | 可选 | 最短 3 · 最长 3 | | `items[].image_url` | `string` | 可选 | 最长 2048 | | `allow_different_amount` | `boolean` | 可选 | — | | `allow_multiple_payments` | `boolean` | 可选 | — | | `expires_at` | `string` | 可选 | — | | `created_at` | `string` | 必填 | — | | `updated_at` | `string` | 必填 | — | | `metadata` | `object` | 可选 | — | ```json { "id": "00000000-0000-7000-8000-000000000000", "invoice_number": "string", "payment_method": "string", "classification": {}, "amount": 0, "currency": "string", "paid_amount": 0, "refunded_amount": 0, "status": "string", "state": "string", "merchant_order_id": "string", "payment_url": "string", "psp_checkout_url": "string", "provider_checkout_url": "string", "payment_url_source": "string", "instruction": {}, "expected_payer_info": { "name": "string", "tax_id": "string", "email": "user@example.com", "phone": "string", "account": "string" }, "payer_info": { "name": "string", "tax_id": "string", "email": "user@example.com", "phone": "string", "account": "string" }, "items": [ { "name": "string", "description": "string", "sku": "string", "quantity": 1, "unit_amount": 0, "currency": "string", "image_url": "https://example.com" } ], "allow_different_amount": false, "allow_multiple_payments": false, "expires_at": "2026-01-01T00:00:00Z", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "metadata": {} } ``` #### 响应 400 Bad Request Invalid request. When payin_payer_validation_mode is optional or required and payer_info.tax_id is present but not a valid market document, or when required mode is missing payer name/tax_id, the gateway returns INVALID_REQUEST. #### 响应 401 Unauthorized The credential kind or host does not match the invoice service. #### 响应 403 Forbidden The account secret lacks the required invoice scope. #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. #### 响应 503 Unavailable Risk evaluation unavailable; no invoice or provider order is created. Retry the same merchant order and idempotency key after the prerequisite is restored. ### Cancel a invoice payment intent **POST** `/api/v1/payments/intents/{invoice_number}/cancel` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoice_number` | `string` | 必填 | — | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `reason` | `string` | 可选 | 最长 500 | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/payments/intents/{invoice_number}/cancel' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "reason": "string" }' ``` **响应** #### 响应 200 OK Payment intent canceled | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `status` | `enum` | 必填 | 可选值:`canceled` | ```json { "status": "canceled" } ``` #### 响应 401 Unauthorized The credential kind or host does not match the invoice service. #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Create a invoice refund **POST** `/api/v1/payments/intents/{invoice_number}/refund` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. The same header key is also the refund's business idempotency key within the merchant: a retry with the same key returns the existing refund, and reusing the key for a different refund is rejected with 409 CONFLICT. New requests evaluate the original account's published refund rules before creating the refund or freezing funds. A block returns 403 RISK_REJECTED; risk dependency failure returns 503 SERVICE_UNAVAILABLE. The refund amount is evaluated against the original frozen method/rail; no new invoice or transfer limit reservation is created. **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoice_number` | `string` | 必填 | — | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `amount` | `integer` | 必填 | ≥ 1 | | `reason` | `string` | 可选 | 最长 500 | | `metadata` | `object` | 可选 | — | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/payments/intents/{invoice_number}/refund' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "amount": 1, "reason": "string", "metadata": {} }' ``` **响应** #### 响应 201 Created Invoice refund created | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `id` | `string` | 必填 | — | | `invoice_number` | `string` | 必填 | — | | `refund_number` | `string` | 必填 | — | | `original_intent_id` | `string` | 可选 | — | | `amount` | `integer` | 必填 | — | | `currency` | `string` | 必填 | — | | `status` | `string` | 必填 | — | | `reason` | `string` | 可选 | — | | `created_at` | `string` | 可选 | — | | `updated_at` | `string` | 可选 | — | ```json { "id": "00000000-0000-7000-8000-000000000000", "invoice_number": "string", "refund_number": "string", "original_intent_id": "00000000-0000-7000-8000-000000000000", "amount": 0, "currency": "string", "status": "string", "reason": "string", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ``` #### 响应 401 Unauthorized The credential kind or host does not match the invoice service. #### 响应 403 Forbidden RISK_REJECTED or RISK_REVIEW; no refund created #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. #### 响应 503 Unavailable SERVICE_UNAVAILABLE; refund risk evaluation failed closed ## Invoice Credit Cards ### Create a credit card authorization **POST** `/api/v1/cards/authorizations` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. This operation has two idempotency layers. The header key is covered by the signature and deduplicates the request at the gateway. The required JSON field `idempotency_key` in the request body is the business idempotency key: the card service uses it to recognize a retried authorization within the merchant account. The gateway does not check that the two values are equal. Send the same value in both places and keep it unchanged when retrying. **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoice_number` | `string` | 必填 | — | | `idempotency_key` | `string` | 必填 | — | | `channel_id` | `string` | 必填 | — | | `amount` | `integer` | 必填 | ≥ 1 | | `currency` | `string` | 必填 | 最短 3 · 最长 3 | | `card_token` | `string` | 可选 | — | | `card_cvv` | `string` | 可选 | — | | `card_info` | `object` | 可选 | — | | `card_info.card_number` | `string` | 必填 | — | | `card_info.cvv` | `string` | 必填 | — | | `card_info.expiry_month` | `integer` | 必填 | ≥ 1 · ≤ 12 | | `card_info.expiry_year` | `integer` | 必填 | — | | `card_info.cardholder_name` | `string` | 必填 | — | | `payer_info` | `object` | 必填 | — | | `payer_info.name` | `string` | 必填 | — | | `payer_info.email` | `string` | 必填 | — | | `payer_info.phone` | `string` | 可选 | — | | `description` | `string` | 可选 | — | | `merchant_order_id` | `string` | 可选 | — | | `require_3ds` | `boolean` | 可选 | — | | `pre_auth` | `boolean` | 可选 | — | | `metadata` | `object` | 可选 | — | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/cards/authorizations' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "invoice_number": "string", "idempotency_key": "string", "channel_id": "00000000-0000-7000-8000-000000000000", "amount": 1, "currency": "string", "card_token": "string", "card_cvv": "string", "card_info": { "card_number": "string", "cvv": "string", "expiry_month": 1, "expiry_year": 0, "cardholder_name": "string" }, "payer_info": { "name": "string", "email": "user@example.com", "phone": "string" }, "description": "string", "merchant_order_id": "string", "require_3ds": false, "pre_auth": false, "metadata": {} }' ``` **响应** #### 响应 201 Created Authorization created #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Complete 3DS for an authorization **POST** `/api/v1/cards/authorizations/{authorization_id}/complete-3ds` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. The header key only signs and deduplicates the request; the request body has no `idempotency_key`. A replayed completion is rejected by the one-time `cres_nonce` with 409 CONFLICT. **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `authorization_id` | `string` | 必填 | — | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoice_id` | `string` | 必填 | — | | `transaction_id` | `string` | 必填 | — | | `auth_value` | `string` | 必填 | — | | `eci` | `string` | 必填 | — | | `cres_nonce` | `string` | 必填 | — | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/cards/authorizations/{authorization_id}/complete-3ds' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "invoice_id": "00000000-0000-7000-8000-000000000000", "transaction_id": "string", "auth_value": "string", "eci": "string", "cres_nonce": "string" }' ``` **响应** #### 响应 200 OK Authorization updated #### 响应 410 Gone Authorization has expired (INVOICE_EXPIRED). #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Capture a credit card authorization **POST** `/api/v1/cards/captures` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. This operation has two idempotency layers. The header key is covered by the signature and deduplicates the request at the gateway. The required JSON field `idempotency_key` in the request body is the business idempotency key: the card service uses it to recognize a retried capture. The gateway does not check that the two values are equal. Send the same value in both places and keep it unchanged when retrying. **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoice_id` | `string` | 必填 | — | | `capture_amount` | `integer` | 必填 | ≥ 1 | | `idempotency_key` | `string` | 必填 | — | | `reason` | `string` | 可选 | — | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/cards/captures' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "invoice_id": "00000000-0000-7000-8000-000000000000", "capture_amount": 1, "idempotency_key": "string", "reason": "string" }' ``` **响应** #### 响应 200 OK Capture completed | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoice_id` | `string` | 可选 | — | | `captured_amount` | `integer` | 可选 | — | | `total_captured` | `integer` | 可选 | — | | `status` | `string` | 可选 | — | | `captured_at` | `string` | 可选 | — | ```json { "invoice_id": "00000000-0000-7000-8000-000000000000", "captured_amount": 0, "total_captured": 0, "status": "string", "captured_at": "2026-01-01T00:00:00Z" } ``` #### 响应 410 Gone Authorization has expired (INVOICE_EXPIRED). #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Create a credit card refund **POST** `/api/v1/cards/refunds` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. This operation has two idempotency layers. The header key is covered by the signature and deduplicates the request at the gateway. The required JSON field `idempotency_key` in the request body is the business idempotency key: within the merchant account, a retry with the same `idempotency_key` and the same refund returns the first refund, and reusing it for a different refund is rejected with 409 DUPLICATE_IDEMPOTENCY_KEY. The gateway does not check that the two values are equal. Send the same value in both places and keep it unchanged when retrying. **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoice_id` | `string` | 必填 | — | | `idempotency_key` | `string` | 必填 | — | | `merchant_refund_id` | `string` | 可选 | — | | `refund_amount` | `integer` | 必填 | ≥ 1 | | `reason` | `string` | 可选 | — | | `notes` | `string` | 可选 | — | | `metadata` | `object` | 可选 | — | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/cards/refunds' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "invoice_id": "00000000-0000-7000-8000-000000000000", "idempotency_key": "string", "merchant_refund_id": "string", "refund_amount": 1, "reason": "string", "notes": "string", "metadata": {} }' ``` **响应** #### 响应 201 Created Card refund created #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ## Invoice Tokenization ### Tokenize a card **POST** `/api/v1/tokenization/cards` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `customer_id` | `string` | 必填 | — | | `card_number` | `string` | 必填 | 最短 16 · 最长 16 | | `expiry_month` | `string` | 必填 | 最短 2 · 最长 2 | | `expiry_year` | `string` | 必填 | 最短 4 · 最长 4 | | `cardholder_name` | `string` | 必填 | — | | `cvv` | `string` | 可选 | 最短 3 · 最长 4 | | `scope` | `enum` | 可选 | 可选值:`single_use`、`multi_use`、`permanent` | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/tokenization/cards' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "customer_id": "string", "card_number": "string", "expiry_month": "string", "expiry_year": "string", "cardholder_name": "string", "cvv": "string", "scope": "single_use" }' ``` **响应** #### 响应 201 Created Token created | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `token_id` | `string` | 必填 | — | | `customer_id` | `string` | 必填 | — | | `token_type` | `enum` | 必填 | 可选值:`card`、`bank_account` | | `scope` | `string` | 可选 | — | | `created_at` | `string` | 必填 | — | | `expires_at` | `string` | 可选 | — | | `masked_info` | `object` | 必填 | — | ```json { "token_id": "string", "customer_id": "string", "token_type": "card", "scope": "string", "created_at": "2026-01-01T00:00:00Z", "expires_at": "2026-01-01T00:00:00Z", "masked_info": {} } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Tokenize a bank account **POST** `/api/v1/tokenization/bank-accounts` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `customer_id` | `string` | 必填 | — | | `account_number` | `string` | 必填 | — | | `bank_code` | `string` | 必填 | — | | `account_type` | `enum` | 可选 | 可选值:`checking`、`savings` | | `account_holder_name` | `string` | 必填 | — | | `branch_code` | `string` | 可选 | — | | `scope` | `enum` | 可选 | 可选值:`single_use`、`multi_use`、`permanent` | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/tokenization/bank-accounts' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "customer_id": "string", "account_number": "string", "bank_code": "string", "account_type": "checking", "account_holder_name": "string", "branch_code": "string", "scope": "single_use" }' ``` **响应** #### 响应 201 Created Token created | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `token_id` | `string` | 必填 | — | | `customer_id` | `string` | 必填 | — | | `token_type` | `enum` | 必填 | 可选值:`card`、`bank_account` | | `scope` | `string` | 可选 | — | | `created_at` | `string` | 必填 | — | | `expires_at` | `string` | 可选 | — | | `masked_info` | `object` | 必填 | — | ```json { "token_id": "string", "customer_id": "string", "token_type": "card", "scope": "string", "created_at": "2026-01-01T00:00:00Z", "expires_at": "2026-01-01T00:00:00Z", "masked_info": {} } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Detokenize a token **POST** `/api/v1/tokenization/tokens/{token_id}/detokenize` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `token_id` | `string` | 必填 | — | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/tokenization/tokens/{token_id}/detokenize' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' ``` **响应** #### 响应 200 OK Sensitive token data returned #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Extend a token **POST** `/api/v1/tokenization/tokens/{token_id}/extend` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `token_id` | `string` | 必填 | — | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `extension_days` | `integer` | 可选 | ≥ 1 · ≤ 365 · 默认 `365` | **请求示例** ```bash curl -X POST 'https://invoice-api.psp.example.com/api/v1/tokenization/tokens/{token_id}/extend' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'Content-Type: application/json' \ -d '{ "extension_days": 365 }' ``` **响应** #### 响应 200 OK Token extended #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Revoke a token **DELETE** `/api/v1/tokenization/tokens/{token_id}` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `token_id` | `string` | 必填 | — | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求示例** ```bash curl -X DELETE 'https://invoice-api.psp.example.com/api/v1/tokenization/tokens/{token_id}' \ -H 'X-API-ID: ' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' ``` **响应** #### 响应 204 No Content Token revoked #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. --- # Transfer API - 服务地址(示例,真实 Host 由接入团队提供):`https://transfer-api.psp.example.com` - 认证:Ed25519 签名 - 版本:1.0.0 - OpenAPI 规范:https://docs.potabank.com/openapi/transfer.openapi.yaml Transfer service surface for merchant disbursement write flows. This service only accepts transfer account secrets and rejects invoice/query/webhook/proof traffic. A missing X-API-ID header returns 400 INVALID_REQUEST. A missing X-API-Key header or invalid credentials return 401 UNAUTHORIZED. A credential store failure returns 500 INTERNAL_ERROR before the request handler runs. ## Transfers ### Create a transfer intent **POST** `/api/v1/payments/transfers` Requires a transfer account secret and `transfers:write` scope. Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. The same header key is also the transfer's business idempotency key: reusing it for a different transfer request is rejected with 409 CONFLICT. The request body has no separate idempotency field. **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-API-Key` | `string` | 必填 | — | | `X-Timestamp` | `string` | 必填 | — | | `X-Signature` | `string` | 必填 | — | | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `payment_method` | `enum` | 必填 | 可选值:`pix`、`spei`、`upi`、`america_ach`、`paypal`、`chime`、`zelle`、`cash_app`、`venmo`、`emt`、`apple_pay` | | `rail_variant` | `string` | 必填 | Canonical rail variant paired with payment_method. | | `amount` | `integer` | 必填 | ≥ 1 | | `currency` | `string` | 必填 | 最短 3 · 最长 3 | | `beneficiary` | `object` | 可选 | — | | `beneficiary.name` | `string` | 可选 | 最长 200 | | `beneficiary.tax_id` | `string` | 可选 | 收款人证件号。账户 transfer_beneficiary_validation_mode 为 optional 或 required 且提交了本字段时,须符合账户市场格式:巴西 CPF(11 位数字含校验位)或 CNPJ (14 位,数字或字母数字,含官方校验位,例如 00.000.000/E08G-12);墨西哥 RFC (12 或 13 位,日期段月份 01–12、日期 01–31)。格式错误返回 INVALID_REQUEST。 required 档未提交亦返回 INVALID_REQUEST。 · 最长 50 | | `beneficiary.email` | `string` | 可选 | 最长 200 | | `beneficiary.phone` | `string` | 可选 | 最长 50 | | `beneficiary.account` | `string` | 可选 | 最长 200 | | `beneficiary.bank_code` | `string` | 可选 | 最长 20 | | `beneficiary.branch_code` | `string` | 可选 | 最长 20 | | `instruction` | `object` | 可选 | PIX QR may omit beneficiary; other transfer modes require beneficiary name and account. PIX and SPEI consume `instruction.tax_id`. It must be a string; a number or object returns INVALID_REQUEST. UPI, ACH, and hosted methods ignore this field and do not persist it as beneficiary.tax_id. When transfer_beneficiary_validation_mode is optional or required and the value is non-empty, it must match the account market format (Brazil CPF, numeric or alphanumeric CNPJ, or Mexico RFC) and must match `beneficiary.tax_id` when that field is also set. Off mode does not send the value upstream. PIX keeps an instruction-only tax_id off beneficiary.tax_id so older Gateway versions can replay the same request. SPEI likewise keeps an explicit RFC and an instruction-only alias off beneficiary.tax_id. Off mode does not backfill the channel RFC from beneficiary.tax_id or instruction.tax_id; an explicit instruction.beneficiary_rfc is still submitted unchanged. Optional and required modes persist the RFC decision for the channel and worker. SPEI `beneficiary_rfc` must be a string. When transfer_beneficiary_validation_mode is optional or required and this field is non-empty (including when beneficiary.tax_id is omitted), it must be a Mexican RFC (12 or 13 characters, month 01–12, day 01–31); otherwise the request returns INVALID_REQUEST. | | `description` | `string` | 可选 | 最长 500 | | `merchant_order_id` | `string` | 可选 | 最长 100 | | `webhook_url` | `string` | 可选 | 最长 500 | | `metadata` | `object` | 可选 | — | **请求示例** ```bash curl -X POST 'https://transfer-api.psp.example.com/api/v1/payments/transfers' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'X-API-ID: ' \ -H 'Content-Type: application/json' \ -d '{ "payment_method": "pix", "rail_variant": "pix_key", "amount": 1, "currency": "string", "beneficiary": { "name": "string", "tax_id": "string", "email": "user@example.com", "phone": "string", "account": "string", "bank_code": "string", "branch_code": "string" }, "instruction": {}, "description": "string", "merchant_order_id": "string", "webhook_url": "https://example.com", "metadata": {} }' ``` **响应** #### 响应 201 Created Transfer intent created | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `id` | `string` | 必填 | — | | `transfer_number` | `string` | 必填 | — | | `payment_method` | `string` | 必填 | — | | `classification` | `object` | 必填 | — | | `classification.classification_status` | `enum` | 必填 | 可选值:`resolved` | | `classification.payment_category` | `string` | 必填 | — | | `classification.payment_method` | `string` | 必填 | — | | `classification.rail_variant` | `string` | 必填 | — | | `amount` | `integer` | 必填 | — | | `currency` | `string` | 必填 | — | | `transferred_amount` | `integer` | 可选 | — | | `returned_amount` | `integer` | 可选 | — | | `status` | `enum` | 必填 | 可选值:`pending`、`processing`、`completed`、`failed`、`returned`、`canceled` | | `state` | `string` | 可选 | — | | `description` | `string` | 可选 | — | | `merchant_order_id` | `string` | 可选 | — | | `provider_transaction_id` | `string` | 可选 | — | | `beneficiary` | `object` | 可选 | The beneficiary the merchant submitted and PSP sent upstream. Echoed back unchanged; a provider callback never overwrites it. | | `beneficiary.name` | `string` | 可选 | 最长 200 | | `beneficiary.tax_id` | `string` | 可选 | 收款人证件号。账户 transfer_beneficiary_validation_mode 为 optional 或 required 且提交了本字段时,须符合账户市场格式:巴西 CPF(11 位数字含校验位)或 CNPJ (14 位,数字或字母数字,含官方校验位,例如 00.000.000/E08G-12);墨西哥 RFC (12 或 13 位,日期段月份 01–12、日期 01–31)。格式错误返回 INVALID_REQUEST。 required 档未提交亦返回 INVALID_REQUEST。 · 最长 50 | | `beneficiary.email` | `string` | 可选 | 最长 200 | | `beneficiary.phone` | `string` | 可选 | 最长 50 | | `beneficiary.account` | `string` | 可选 | 最长 200 | | `beneficiary.bank_code` | `string` | 可选 | 最长 20 | | `beneficiary.branch_code` | `string` | 可选 | 最长 20 | | `actual_beneficiary` | `object` | 可选 | The beneficiary the provider reported back after settling the transfer. Absent until a callback carries one, and absent for good on providers that do not report it. Compare it against `beneficiary` to tell whether the money reached the requested account. | | `actual_beneficiary.name` | `string` | 可选 | 最长 200 | | `actual_beneficiary.tax_id` | `string` | 可选 | 收款人证件号。账户 transfer_beneficiary_validation_mode 为 optional 或 required 且提交了本字段时,须符合账户市场格式:巴西 CPF(11 位数字含校验位)或 CNPJ (14 位,数字或字母数字,含官方校验位,例如 00.000.000/E08G-12);墨西哥 RFC (12 或 13 位,日期段月份 01–12、日期 01–31)。格式错误返回 INVALID_REQUEST。 required 档未提交亦返回 INVALID_REQUEST。 · 最长 50 | | `actual_beneficiary.email` | `string` | 可选 | 最长 200 | | `actual_beneficiary.phone` | `string` | 可选 | 最长 50 | | `actual_beneficiary.account` | `string` | 可选 | 最长 200 | | `actual_beneficiary.bank_code` | `string` | 可选 | 最长 20 | | `actual_beneficiary.branch_code` | `string` | 可选 | 最长 20 | | `failure_reason` | `string` | 可选 | — | | `failure_code` | `string` | 可选 | — | | `created_at` | `string` | 必填 | — | | `updated_at` | `string` | 必填 | — | | `submitted_at` | `string` | 可选 | — | | `completed_at` | `string` | 可选 | — | | `failed_at` | `string` | 可选 | — | | `returned_at` | `string` | 可选 | — | | `authorization_url` | `string` | 可选 | Short-lived payer authorization URL, available after asynchronous channel submission. | | `authorization_url_expires_at` | `string` | 可选 | — | | `metadata` | `object` | 可选 | — | ```json { "id": "00000000-0000-7000-8000-000000000000", "transfer_number": "string", "payment_method": "string", "classification": { "classification_status": "resolved", "payment_category": "string", "payment_method": "string", "rail_variant": "string" }, "amount": 0, "currency": "string", "transferred_amount": 0, "returned_amount": 0, "status": "pending", "state": "string", "description": "string", "merchant_order_id": "string", "provider_transaction_id": "string", "beneficiary": { "name": "string", "tax_id": "string", "email": "user@example.com", "phone": "string", "account": "string", "bank_code": "string", "branch_code": "string" }, "actual_beneficiary": { "name": "string", "tax_id": "string", "email": "user@example.com", "phone": "string", "account": "string", "bank_code": "string", "branch_code": "string" }, "failure_reason": "string", "failure_code": "string", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "submitted_at": "2026-01-01T00:00:00Z", "completed_at": "2026-01-01T00:00:00Z", "failed_at": "2026-01-01T00:00:00Z", "returned_at": "2026-01-01T00:00:00Z", "authorization_url": "https://example.com", "authorization_url_expires_at": "2026-01-01T00:00:00Z", "metadata": {} } ``` #### 响应 400 Bad Request The request body or path parameters are invalid. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `code` | `string` | 必填 | — | | `message` | `string` | 必填 | — | ```json { "code": "string", "message": "string" } ``` #### 响应 401 Unauthorized The credential kind or host does not match the transfer service. #### 响应 403 Forbidden The account secret lacks the required transfer scope. #### 响应 404 Not Found ACCOUNT_FEE_NOT_FOUND. No canonical transfer pricing policy matches this account, rail, currency and operation. The transfer is not created. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `code` | `string` | 必填 | — | | `message` | `string` | 必填 | — | ```json { "code": "string", "message": "string" } ``` #### 响应 409 Conflict The idempotency key or transfer state conflicts with the request. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `code` | `string` | 必填 | — | | `message` | `string` | 必填 | — | ```json { "code": "string", "message": "string" } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Cancel a transfer intent **POST** `/api/v1/payments/transfers/{intent_id}/cancel` Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `intent_id` | `string` | 必填 | — | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-API-Key` | `string` | 必填 | — | | `X-Timestamp` | `string` | 必填 | — | | `X-Signature` | `string` | 必填 | — | | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求体** (application/json) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `reason` | `string` | 可选 | 最长 500 | **请求示例** ```bash curl -X POST 'https://transfer-api.psp.example.com/api/v1/payments/transfers/{intent_id}/cancel' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'X-API-ID: ' \ -H 'Content-Type: application/json' \ -d '{ "reason": "string" }' ``` **响应** #### 响应 200 OK Transfer intent canceled | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `status` | `enum` | 必填 | 可选值:`canceled` | ```json { "status": "canceled" } ``` #### 响应 400 Bad Request The request body or path parameters are invalid. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `code` | `string` | 必填 | — | | `message` | `string` | 必填 | — | ```json { "code": "string", "message": "string" } ``` #### 响应 401 Unauthorized The credential kind or host does not match the transfer service. #### 响应 403 Forbidden The account secret lacks the required transfer scope. #### 响应 404 Not Found The transfer does not exist for this account. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `code` | `string` | 必填 | — | | `message` | `string` | 必填 | — | ```json { "code": "string", "message": "string" } ``` #### 响应 409 Conflict The idempotency key or transfer state conflicts with the request. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `code` | `string` | 必填 | — | | `message` | `string` | 必填 | — | ```json { "code": "string", "message": "string" } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Request a transfer order sync **POST** `/api/v1/orders/{order_id}/sync` Requires a transfer account secret and `transfers:write` scope. A non-terminal transfer is never advanced by this request: the gateway delivers one workflow command derived from the idempotency key and returns `action=update_transfer_workflow` with `synced=false` once the Transfer workflow accepts it. The workflow observes the provider and alone decides the terminal state and funds; read the outcome from webhooks or the query service. Retrying with the same idempotency key maps to the same workflow command. A transfer that is not yet submitted or whose submission is unconfirmed (state `submission_unknown`, even when a provider ID was saved) returns `action=none`. A completed transfer only refreshes its channel fee (`action=query_channel_status`). If the transfer's workflow run has failed, the request answers 503 until operations recover the workflow; merchant retries, with the same or a new idempotency key, never restart it. Send the idempotency key in `X-Idempotency-Key` or `Idempotency-Key`; at least one is required. When both are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. The Ed25519 signature covers `X-Idempotency-Key` when it is sent, otherwise `Idempotency-Key`. **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `order_id` | `string` | 必填 | — | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-API-Key` | `string` | 必填 | — | | `X-Timestamp` | `string` | 必填 | — | | `X-Signature` | `string` | 必填 | — | | `X-Idempotency-Key` | `string` | 可选 | Idempotency key. Send it in this header or in Idempotency-Key; at least one of the two is required. When this header is sent, the Ed25519 signature covers its value. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | | `Idempotency-Key` | `string` | 可选 | Idempotency key, alternative to X-Idempotency-Key; at least one of the two is required. The Ed25519 signature covers this header only when X-Idempotency-Key is not sent. If both headers are sent they must be equal, otherwise the request is rejected with 400 INVALID_REQUEST. · 最短 1 | **请求示例** ```bash curl -X POST 'https://transfer-api.psp.example.com/api/v1/orders/{order_id}/sync' \ -H 'X-API-Key: ' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Sync request handled; see `action` for what was done. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `order_id` | `string` | 必填 | — | | `order_type` | `enum` | 必填 | 可选值:`transfer_intent`、`transfer_refund` | | `action` | `enum` | 可选 | `update_transfer_workflow` means the Transfer workflow accepted the sync command; the gateway did not query the provider or write state. · 可选值:`none`、`query_channel_status`、`update_transfer_workflow` | | `previous_state` | `string` | 必填 | — | | `current_state` | `string` | 必填 | Local state when the response was built; workflow results arrive asynchronously. | | `synced` | `boolean` | 必填 | Always false for `update_transfer_workflow`. | | `synced_at` | `string` | 必填 | — | | `message` | `string` | 可选 | — | ```json { "order_id": "00000000-0000-7000-8000-000000000000", "order_type": "transfer_intent", "action": "none", "previous_state": "string", "current_state": "string", "synced": false, "synced_at": "2026-01-01T00:00:00Z", "message": "string" } ``` #### 响应 400 Bad Request The request body or path parameters are invalid. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `code` | `string` | 必填 | — | | `message` | `string` | 必填 | — | ```json { "code": "string", "message": "string" } ``` #### 响应 401 Unauthorized The credential kind or host does not match the transfer service. #### 响应 403 Forbidden The account secret lacks the required transfer scope. #### 响应 404 Not Found The transfer does not exist for this account. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `code` | `string` | 必填 | — | | `message` | `string` | 必填 | — | ```json { "code": "string", "message": "string" } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. #### 响应 503 Unavailable The Transfer workflow or provider cannot accept the request now; retry with the same idempotency key. | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `code` | `string` | 必填 | — | | `message` | `string` | 必填 | — | ```json { "code": "string", "message": "string" } ``` --- # Query API - 服务地址(示例,真实 Host 由接入团队提供):`https://query-api.psp.example.com` - 认证:Ed25519 签名 - 版本:1.0.0 - OpenAPI 规范:https://docs.potabank.com/openapi/query.openapi.yaml Query service surface for merchant read operations. Query accepts invoice or transfer account secrets for reads, but it does not expose write routes. A missing X-API-ID header returns 400 INVALID_REQUEST. A missing X-API-Key header or invalid credentials return 401 UNAUTHORIZED. A credential store failure returns 500 INTERNAL_ERROR before the request handler runs. ## Query Payments ### List invoice payment intents **GET** `/api/v1/payments/intents` **查询参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `payment_method` | `string` | 可选 | — | | `merchant_order_id` | `string` | 可选 | — | | `status` | `string` | 可选 | — | | `state` | `string` | 可选 | — | | `created_after` | `string` | 可选 | — | | `created_before` | `string` | 可选 | — | | `limit` | `integer` | 可选 | ≥ 1 · ≤ 100 | | `offset` | `integer` | 可选 | ≥ 0 | | `cursor` | `string` | 可选 | Presence selects cursor pagination, including an empty value for the first page. Response contains items, next_cursor, has_more and page_size instead of total. | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/payments/intents' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Payment intent page | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `(InvoiceOffsetPage)` | `object` | 可选 | — | | `items` | `object[]` | 必填 | — | | `items[].id` | `string` | 必填 | — | | `items[].invoice_number` | `string` | 必填 | — | | `items[].payment_method` | `string` | 必填 | — | | `items[].amount` | `integer` | 必填 | — | | `items[].currency` | `string` | 必填 | — | | `items[].status` | `string` | 必填 | — | | `items[].state` | `string` | 可选 | — | | `items[].merchant_order_id` | `string` | 可选 | — | | `items[].paid_amount` | `integer` | 可选 | — | | `items[].refunded_amount` | `integer` | 可选 | — | | `items[].payment_url` | `string` | 可选 | — | | `items[].psp_checkout_url` | `string` | 可选 | — | | `items[].provider_checkout_url` | `string` | 可选 | — | | `items[].instruction` | `object` | 可选 | — | | `items[].expected_payer_info` | `object` | 可选 | — | | `items[].payer_info` | `object` | 可选 | — | | `items[].created_at` | `string` | 可选 | — | | `items[].updated_at` | `string` | 可选 | — | | `items[].metadata` | `object` | 可选 | — | | `total` | `integer` | 必填 | — | | `limit` | `integer` | 必填 | — | | `offset` | `integer` | 必填 | — | | `(InvoiceCursorPage)` | `object` | 可选 | — | | `next_cursor` | `string` | 必填 | — | | `has_more` | `boolean` | 必填 | — | | `page_size` | `integer` | 必填 | — | ```json { "items": [ { "id": "00000000-0000-7000-8000-000000000000", "invoice_number": "string", "payment_method": "string", "amount": 0, "currency": "string", "status": "string", "state": "string", "merchant_order_id": "string", "paid_amount": 0, "refunded_amount": 0, "payment_url": "string", "psp_checkout_url": "string", "provider_checkout_url": "string", "instruction": {}, "expected_payer_info": {}, "payer_info": {}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "metadata": {} } ], "total": 0, "limit": 0, "offset": 0 } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Get a invoice payment intent **GET** `/api/v1/payments/intents/{invoice_number}` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoice_number` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/payments/intents/{invoice_number}' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Payment intent detail | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `id` | `string` | 必填 | — | | `invoice_number` | `string` | 必填 | — | | `payment_method` | `string` | 必填 | — | | `amount` | `integer` | 必填 | — | | `currency` | `string` | 必填 | — | | `status` | `string` | 必填 | — | | `state` | `string` | 可选 | — | | `merchant_order_id` | `string` | 可选 | — | | `paid_amount` | `integer` | 可选 | — | | `refunded_amount` | `integer` | 可选 | — | | `payment_url` | `string` | 可选 | — | | `psp_checkout_url` | `string` | 可选 | — | | `provider_checkout_url` | `string` | 可选 | — | | `instruction` | `object` | 可选 | — | | `expected_payer_info` | `object` | 可选 | — | | `payer_info` | `object` | 可选 | — | | `created_at` | `string` | 可选 | — | | `updated_at` | `string` | 可选 | — | | `metadata` | `object` | 可选 | — | ```json { "id": "00000000-0000-7000-8000-000000000000", "invoice_number": "string", "payment_method": "string", "amount": 0, "currency": "string", "status": "string", "state": "string", "merchant_order_id": "string", "paid_amount": 0, "refunded_amount": 0, "payment_url": "string", "psp_checkout_url": "string", "provider_checkout_url": "string", "instruction": {}, "expected_payer_info": {}, "payer_info": {}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "metadata": {} } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### List transfer intents **GET** `/api/v1/payments/transfers` **查询参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `payment_method` | `string` | 可选 | — | | `merchant_order_id` | `string` | 可选 | — | | `status` | `string` | 可选 | — | | `state` | `string` | 可选 | — | | `created_after` | `string` | 可选 | — | | `created_before` | `string` | 可选 | — | | `limit` | `integer` | 可选 | ≥ 1 · ≤ 100 | | `offset` | `integer` | 可选 | ≥ 0 | | `cursor` | `string` | 可选 | Presence selects cursor pagination, including an empty value for the first page. Response contains items, next_cursor, has_more and page_size instead of total. | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/payments/transfers' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Transfer intent page | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `(TransferOffsetPage)` | `object` | 可选 | — | | `items` | `object[]` | 必填 | — | | `items[].id` | `string` | 必填 | — | | `items[].transfer_number` | `string` | 必填 | — | | `items[].payment_method` | `string` | 必填 | — | | `items[].amount` | `integer` | 必填 | — | | `items[].currency` | `string` | 必填 | — | | `items[].status` | `string` | 必填 | — | | `items[].state` | `string` | 可选 | — | | `items[].merchant_order_id` | `string` | 可选 | — | | `items[].beneficiary` | `object` | 可选 | — | | `items[].actual_beneficiary` | `object` | 可选 | — | | `items[].transferred_amount` | `integer` | 可选 | — | | `items[].returned_amount` | `integer` | 可选 | — | | `items[].created_at` | `string` | 可选 | — | | `items[].updated_at` | `string` | 可选 | — | | `items[].metadata` | `object` | 可选 | — | | `total` | `integer` | 必填 | — | | `limit` | `integer` | 必填 | — | | `offset` | `integer` | 必填 | — | | `(TransferCursorPage)` | `object` | 可选 | — | | `next_cursor` | `string` | 必填 | — | | `has_more` | `boolean` | 必填 | — | | `page_size` | `integer` | 必填 | — | ```json { "items": [ { "id": "00000000-0000-7000-8000-000000000000", "transfer_number": "string", "payment_method": "string", "amount": 0, "currency": "string", "status": "string", "state": "string", "merchant_order_id": "string", "beneficiary": {}, "actual_beneficiary": {}, "transferred_amount": 0, "returned_amount": 0, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "metadata": {} } ], "total": 0, "limit": 0, "offset": 0 } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Get a transfer intent **GET** `/api/v1/payments/transfers/{intent_id}` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `intent_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/payments/transfers/{intent_id}' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Transfer intent detail | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `id` | `string` | 必填 | — | | `transfer_number` | `string` | 必填 | — | | `payment_method` | `string` | 必填 | — | | `amount` | `integer` | 必填 | — | | `currency` | `string` | 必填 | — | | `status` | `string` | 必填 | — | | `state` | `string` | 可选 | — | | `merchant_order_id` | `string` | 可选 | — | | `beneficiary` | `object` | 可选 | — | | `actual_beneficiary` | `object` | 可选 | — | | `transferred_amount` | `integer` | 可选 | — | | `returned_amount` | `integer` | 可选 | — | | `created_at` | `string` | 可选 | — | | `updated_at` | `string` | 可选 | — | | `metadata` | `object` | 可选 | — | ```json { "id": "00000000-0000-7000-8000-000000000000", "transfer_number": "string", "payment_method": "string", "amount": 0, "currency": "string", "status": "string", "state": "string", "merchant_order_id": "string", "beneficiary": {}, "actual_beneficiary": {}, "transferred_amount": 0, "returned_amount": 0, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z", "metadata": {} } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### List refunds **GET** `/api/v1/payments/refunds` **查询参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `original_intent_id` | `string` | 可选 | — | | `payment_method` | `string` | 可选 | — | | `status` | `string` | 可选 | — | | `state` | `string` | 可选 | — | | `created_after` | `string` | 可选 | — | | `created_before` | `string` | 可选 | — | | `limit` | `integer` | 可选 | ≥ 1 · ≤ 100 | | `offset` | `integer` | 可选 | ≥ 0 | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/payments/refunds' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Refund page | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `items` | `object[]` | 必填 | — | | `items[].id` | `string` | 必填 | — | | `items[].refund_number` | `string` | 必填 | — | | `items[].original_intent_id` | `string` | 必填 | — | | `items[].invoice_number` | `string` | 可选 | — | | `items[].amount` | `integer` | 必填 | — | | `items[].currency` | `string` | 必填 | — | | `items[].status` | `string` | 必填 | — | | `items[].state` | `string` | 可选 | — | | `items[].reason` | `string` | 可选 | — | | `items[].created_at` | `string` | 可选 | — | | `items[].updated_at` | `string` | 可选 | — | | `total` | `integer` | 必填 | — | | `limit` | `integer` | 必填 | — | | `offset` | `integer` | 必填 | — | ```json { "items": [ { "id": "00000000-0000-7000-8000-000000000000", "refund_number": "string", "original_intent_id": "00000000-0000-7000-8000-000000000000", "invoice_number": "string", "amount": 0, "currency": "string", "status": "string", "state": "string", "reason": "string", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ], "total": 0, "limit": 0, "offset": 0 } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Get a refund **GET** `/api/v1/payments/refunds/{refund_id}` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `refund_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/payments/refunds/{refund_id}' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Refund detail | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `id` | `string` | 必填 | — | | `refund_number` | `string` | 必填 | — | | `original_intent_id` | `string` | 必填 | — | | `invoice_number` | `string` | 可选 | — | | `amount` | `integer` | 必填 | — | | `currency` | `string` | 必填 | — | | `status` | `string` | 必填 | — | | `state` | `string` | 可选 | — | | `reason` | `string` | 可选 | — | | `created_at` | `string` | 可选 | — | | `updated_at` | `string` | 可选 | — | ```json { "id": "00000000-0000-7000-8000-000000000000", "refund_number": "string", "original_intent_id": "00000000-0000-7000-8000-000000000000", "invoice_number": "string", "amount": 0, "currency": "string", "status": "string", "state": "string", "reason": "string", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z" } ``` #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### List order events **GET** `/api/v1/orders/{order_id}/events` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `order_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/orders/{order_id}/events' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Order event list ### List order notifications **GET** `/api/v1/orders/{order_id}/notifications` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `order_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/orders/{order_id}/notifications' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Order notification list ## Query Accounts ### List merchant accounts **GET** `/api/v1/merchant-accounts` **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/merchant-accounts' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Merchant account page #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Get a merchant account **GET** `/api/v1/merchant-accounts/{account_id}` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `account_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/merchant-accounts/{account_id}' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Merchant account detail #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Get merchant account balance **GET** `/api/v1/merchant-accounts/{account_id}/balance` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `account_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/merchant-accounts/{account_id}/balance' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Balance detail #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### List account transactions **GET** `/api/v1/merchant-accounts/{account_id}/transactions` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `account_id` | `string` | 必填 | — | **查询参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `limit` | `integer` | 可选 | ≥ 1 · ≤ 100 | | `offset` | `integer` | 可选 | ≥ 0 | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/merchant-accounts/{account_id}/transactions' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Account transaction page ### Get aggregate merchant balance **GET** `/api/v1/merchants/balance` **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/merchants/balance' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Merchant balance list #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ## Query Cards ### Get credit card authorization **GET** `/api/v1/cards/authorizations/{authorization_id}` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `authorization_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/cards/authorizations/{authorization_id}' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Authorization detail #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Get credit card capture **GET** `/api/v1/cards/captures/{invoice_id}` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `invoice_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/cards/captures/{invoice_id}' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Capture detail #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### List credit card refunds **GET** `/api/v1/cards/refunds` **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/cards/refunds' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Card refund page #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### Get credit card refund **GET** `/api/v1/cards/refunds/{refund_id}` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `refund_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/cards/refunds/{refund_id}' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Card refund detail #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ## Query Tokenization ### Get token metadata **GET** `/api/v1/tokenization/tokens/{token_id}` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `token_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/tokenization/tokens/{token_id}' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Token detail #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. ### List customer tokens **GET** `/api/v1/tokenization/customers/{customer_id}/tokens` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `customer_id` | `string` | 必填 | — | **请求示例** ```bash curl -X GET 'https://query-api.psp.example.com/api/v1/tokenization/customers/{customer_id}/tokens' \ -H 'X-API-ID: ' ``` **响应** #### 响应 200 OK Customer token page #### 响应 500 Server Error Credential lookup failed (INTERNAL_ERROR). The request handler was not executed. --- # 商户 Webhook - 服务地址(示例,真实 Host 由接入团队提供):`https://{merchantWebhookHost}` - 认证:Ed25519 签名 - 版本:1.1.0 - OpenAPI 规范:https://docs.potabank.com/openapi/merchant-webhook.openapi.yaml > 本页是 PSP 推送事件的**接口契约**。要实现接收端,请先阅读 [Webhook 对接指南](/guide/webhooks)(验签、重试、幂等、回调地址要求)和 [Webhook 事件目录](/guide/webhook-events)(每个事件的触发条件与载荷示例)。 Canonical outbound contract for PSP merchant events delivered to merchant-owned HTTPS endpoints. The request body is one of five envelopes; route on the `X-Webhook-Event` header first. - `ClassifiedMerchantWebhookEnvelope`: payment intent, transfer and credit card lifecycle events. `data` always carries classification schema version 1 and the resolved public payment classification. - `UnclassifiedMerchantWebhookEnvelope`: refund and account freeze lifecycle events. `data` cannot carry classification fields. - `MerchantSettlementWebhookEnvelope`: `merchant_settlement.*.v1` events for settlement items. Different top-level fields; amounts are decimal strings. - `WithdrawalStatusWebhookEnvelope` and `SettlementStatusWebhookEnvelope`: flat status bodies sent by the withdrawal and settlement workflows. Amounts are integers in the currency's minor unit unless a schema states otherwise. Fields documented as aliases always carry the same value. Only the `data` objects of `ClassifiedMerchantWebhookEnvelope` and `UnclassifiedMerchantWebhookEnvelope` stay open (`additionalProperties: true`) so PSP can add fields without a new contract version. `MerchantSettlementWebhookEnvelope`, its `MerchantSettlementEventData`, both flat status envelopes, the classified and unclassified envelopes themselves, `InvoicePaymentPolicy` and `ResolvedPaymentClassification` reject undeclared fields. Retries and operator redeliveries resend the stored body unchanged, so fields introduced later may be absent from older events. A callback regenerated by an operator refresh is stored and resent the same way; bodies stored before a refresh rebuilt the complete event can carry only a subset of the fields documented as required for first deliveries (for example `classification` on payment events), and may omit fields that are optional for first deliveries as well (for example the refund `fee_amount` or `original_intent_type`). Stable taxonomy IDs, direction, provenance, aliases, defaults, and legacy flat classification fields are intentionally not exposed. ## 事件 ### Deliver a merchant transaction event **EVENT** `merchantEvent` PSP signs the exact request body as `.` with the account-level Ed25519 signing key identified by `X-Webhook-Key-ID`. Verify the signature and timestamp window before parsing the body, and deduplicate by `X-Webhook-Event-ID`. **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Timestamp` | `string` | 必填 | Unix time in seconds (UTC) at which PSP signed this HTTP attempt. It is regenerated on every attempt (automatic retry, merchant manual retry and operator redelivery), so it is neither the event time nor a deduplication key. Reject requests whose value differs from the receiver's clock by more than 300 seconds. · 格式 `^[0-9]+$` | | `X-Signature` | `string` | 必填 | Standard Base64 encoding of the 64-byte Ed25519 signature over the ASCII bytes `.`. It is recomputed on every attempt because `X-Timestamp` changes. Verify against the raw body bytes, never a re-serialized JSON document. · 最短 1 | | `X-Signature-Algorithm` | `string` | 必填 | Signature algorithm; always `ed25519`. · 固定值 `ed25519` | | `X-Webhook-Key-ID` | `string` | 必填 | Identifier of the account-level signing key bound to this delivery. All attempts of a delivery, including operator redeliveries of it, are signed with the originally bound key. Select the public key by this ID and keep retired keys available during rotation. · 最短 1 | | `X-Webhook-Version` | `string` | 必填 | Version of the delivery header and signing contract, fixed to `2026-06-11`. It does not version the body; merchant settlement bodies carry their own `schema_version`. · 固定值 `2026-06-11` | | `X-Webhook-ID` | `string` | 必填 | Delivery ID. It stays the same across automatic retries and merchant manual retries of one delivery. An operator redelivery creates a new delivery with a new `X-Webhook-ID` while keeping the same `X-Webhook-Event-ID` and body. Use it to correlate attempts with PSP delivery records, not for business deduplication. | | `X-Webhook-Event` | `string` | 必填 | Event type of the body: the body `event` for event/data envelopes, or the body `event_type` for merchant settlement, withdrawal and settlement status bodies. Route on this header before parsing the body; record and acknowledge event types you do not handle. · 最短 1 | | `X-Webhook-Event-ID` | `string` | 必填 | Event ID. It never changes across attempts, manual retries or operator redeliveries of the same event and is the recommended deduplication key. For merchant settlement events it equals the body `event_id`; for other events it is a stable PSP-derived ID that is not repeated in the body. | | `X-Merchant-Account-ID` | `string` | 必填 | Merchant account the delivery belongs to; identical on every attempt. Use it to scope events whose `data` has no account ID, such as refund completion events. | **请求体** (application/json · 必填) *变体 ClassifiedMerchantWebhookEnvelope* | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `event` | `enum` | 必填 | 可选值:`payment.intent.completed`、`payment.intent.partially_paid`、`payment.intent.expired`、`payment.intent.canceled`、`payment.transfer.completed`、`payment.transfer.failed`、`payment.transfer.returned`、`creditcard_invoice.captured`、`creditcard_invoice.3ds_timeout` | | `timestamp` | `string` | 必填 | Time the event occurred (RFC 3339, may include fractional seconds and an offset). | | `data` | `object` | 必填 | — | | `data.classification_schema_version` | `integer` | 必填 | 固定值 `1` | | `data.classification` | `object` | 必填 | — | | `data.classification.classification_status` | `string` | 必填 | 固定值 `resolved` | | `data.classification.payment_category` | `string` | 必填 | 格式 `^[a-z0-9]+(?:_[a-z0-9]+)*$` | | `data.classification.payment_method` | `string` | 必填 | 格式 `^[a-z0-9]+(?:_[a-z0-9]+)*$` | | `data.classification.rail_variant` | `string` | 必填 | 格式 `^[a-z0-9]+(?:_[a-z0-9]+)*$` | *变体 UnclassifiedMerchantWebhookEnvelope* | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `event` | `enum` | 必填 | 可选值:`payment.refund.completed`、`payment.transfer_refund.completed`、`payment.refund.failed`、`account.frozen`、`account.unfrozen` | | `timestamp` | `string` | 必填 | Time the event occurred (RFC 3339, may include fractional seconds and an offset). | | `data` | `object` | 必填 | Merchant event data for event types that do not carry payment classification. | | `data.classification_schema_version` | `—` | 可选 | 该事件不会包含此字段 | | `data.classification` | `—` | 可选 | 该事件不会包含此字段 | *变体 MerchantSettlementWebhookEnvelope* | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `schema_version` | `string` | 必填 | Body schema version. · 固定值 `1.0.0` | | `event_id` | `string` | 必填 | Original settlement event ID (UUIDv7); the deduplication key. | | `event_type` | `enum` | 必填 | Settlement event type. · 可选值:`merchant_settlement.completed.v1`、`merchant_settlement.adjusted.v1`、`merchant_settlement.reversed.v1` | | `occurred_at` | `string` | 必填 | Event time in UTC. · 格式 `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?Z$` | | `aggregate_type` | `string` | 必填 | Aggregate type; always merchant_settlement_item. · 固定值 `merchant_settlement_item` | | `aggregate_id` | `string` | 必填 | Settlement item ID; equals data.merchant_item_id. | | `aggregate_version` | `integer` | 必填 | Settlement item version that produced the event. · ≥ 1 | | `data` | `object` | 必填 | Public settlement item projection. Amounts are non-negative decimal strings in minor units, never JSON numbers, and satisfy gross_amount = merchant_fee_amount + net_amount. | | `data.merchant_item_id` | `string` | 必填 | Settlement item ID. | | `data.merchant_account_id` | `string` | 必填 | Merchant account ID; equals X-Merchant-Account-ID. | | `data.source_type` | `enum` | 必填 | Source of the settlement item. merchant_settlement.completed.v1 is only delivered for invoice items; adjusted and reversed events use adjustment and reversal respectively. · 可选值:`invoice`、`transfer`、`refund`、`chargeback`、`adjustment`、`reversal` | | `data.status` | `enum` | 必填 | Item status; settled, adjusted or reversed for the respective event type. · 可选值:`settled`、`adjusted`、`reversed` | | `data.gross_amount` | `string` | 必填 | Gross amount as a decimal string in minor units. · 格式 `^(0\|[1-9][0-9]*)$` | | `data.merchant_fee_amount` | `string` | 必填 | Merchant fee as a decimal string in minor units. · 格式 `^(0\|[1-9][0-9]*)$` | | `data.net_amount` | `string` | 必填 | Net amount as a decimal string in minor units. · 格式 `^(0\|[1-9][0-9]*)$` | | `data.currency` | `string` | 必填 | ISO 4217 currency code. · 格式 `^[A-Z]{3}$` | | `data.due_at` | `string` | 必填 | Time the item became due for settlement, in UTC. · 格式 `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?Z$` | | `data.settled_at` | `string` | 必填 | Time the item was settled, in UTC. · 格式 `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?Z$` | | `data.parent_item_id` | `string,null` | 必填 | Parent settlement item for adjustments and reversals; null otherwise. | | `data.relation_type` | `enum` | 必填 | Relation to parent_item_id; null when there is no parent. · 可选值:`adjustment`、`reversal`、`null` | *变体 WithdrawalStatusWebhookEnvelope* | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `event_type` | `enum` | 必填 | Withdrawal event type; equals X-Webhook-Event. · 可选值:`withdrawal.completed`、`withdrawal.failed` | | `withdrawal_id` | `string` | 必填 | PSP withdrawal ID. | | `status` | `enum` | 必填 | Terminal withdrawal status. · 可选值:`completed`、`failed` | | `timestamp` | `string` | 必填 | Workflow time of the status change, UTC with second precision. | | `amount` | `string` | 可选 | Withdrawal amount as a decimal string; present only when populated. | | `currency` | `string` | 可选 | ISO 4217 currency code; present only when populated. | | `channel_ref` | `string` | 可选 | Channel reference; present only when populated. | | `e2e_id` | `string` | 可选 | End-to-end payment identifier; present only when populated. | | `error_code` | `string` | 可选 | Failure code; present only when populated. | | `error_message` | `string` | 可选 | Failure message; present only when populated. | *变体 SettlementStatusWebhookEnvelope* | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `event_type` | `enum` | 必填 | Settlement event type; equals X-Webhook-Event. · 可选值:`settlement.completed`、`settlement.failed`、`settlement.rejected`、`settlement.canceled` | | `settlement_id` | `string` | 必填 | PSP settlement ID. | | `status` | `enum` | 必填 | Settlement status. · 可选值:`completed`、`failed`、`rejected`、`canceled` | | `timestamp` | `string` | 必填 | Workflow time of the status change, UTC with second precision. | | `net_amount` | `string` | 必填 | Settlement amount as a decimal string of integer minor units. · 格式 `^-?(0\|[1-9][0-9]*)$` | | `currency` | `string` | 可选 | ISO 4217 currency code; omitted when the settlement has no currency. | **请求示例** ```bash curl -X POST 'https:///' \ -H 'X-Timestamp: ' \ -H 'X-Signature: ' \ -H 'X-Signature-Algorithm: ' \ -H 'X-Webhook-Key-ID: ' \ -H 'X-Webhook-Version: ' \ -H 'X-Webhook-ID: ' \ -H 'X-Webhook-Event: ' \ -H 'X-Webhook-Event-ID: ' \ -H 'X-Merchant-Account-ID: ' \ -H 'Content-Type: application/json' \ -d '{ "event": "payment.intent.completed", "timestamp": "2026-01-01T00:00:00Z", "data": { "classification_schema_version": 0, "classification": { "classification_status": "string", "payment_category": "string", "payment_method": "string", "rail_variant": "string" } } }' ``` **响应** #### 响应 200 OK Event accepted. #### 响应 204 No Content Event accepted without a response body. --- # Hosted Checkout - 服务地址(示例,真实 Host 由接入团队提供):`https://checkout.psp.example.com` - 认证:无需凭证 - 版本:1.0.0 - OpenAPI 规范:https://docs.potabank.com/openapi/proof.openapi.yaml Consumer checkout for an existing Gateway PaymentIntent. Use the psp_checkout_url returned by the Invoice API. Checkout access requires a token bound to the payment and tenant in X-Checkout-Session-Token or ?t=. Merchant Gateway credentials do not apply. ## Hosted Checkout ### Read hosted payment and saved instructions **GET** `/checkout/hosted/payments/{payment_id}` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `payment_id` | `string` | 必填 | — | **查询参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `t` | `string` | 可选 | Checkout token; provide this parameter or X-Checkout-Session-Token. | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Checkout-Session-Token` | `string` | 可选 | Checkout token; provide this header or t. | **请求示例** ```bash curl -X GET 'https://checkout.psp.example.com/checkout/hosted/payments/{payment_id}' ``` **响应** #### 响应 200 OK Hosted payment detail and instructions ### Read payment status **GET** `/checkout/hosted/payments/{payment_id}/status` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `payment_id` | `string` | 必填 | — | **查询参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `t` | `string` | 可选 | Checkout token; provide this parameter or X-Checkout-Session-Token. | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Checkout-Session-Token` | `string` | 可选 | Checkout token; provide this header or t. | **请求示例** ```bash curl -X GET 'https://checkout.psp.example.com/checkout/hosted/payments/{payment_id}/status' ``` **响应** #### 响应 200 OK Payment status ### Read the validated return redirect **GET** `/checkout/hosted/payments/{payment_id}/success-redirect` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `payment_id` | `string` | 必填 | — | **查询参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `t` | `string` | 可选 | Checkout token; provide this parameter or X-Checkout-Session-Token. | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Checkout-Session-Token` | `string` | 可选 | Checkout token; provide this header or t. | **请求示例** ```bash curl -X GET 'https://checkout.psp.example.com/checkout/hosted/payments/{payment_id}/success-redirect' ``` **响应** #### 响应 200 OK Redirect decision ### Submit hosted credit card payment **POST** `/checkout/creditcard/payment-intents/{payment_id}/pay` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `payment_id` | `string` | 必填 | — | **查询参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `t` | `string` | 可选 | Checkout token; provide this parameter or X-Checkout-Session-Token. | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Checkout-Session-Token` | `string` | 可选 | Checkout token; provide this header or t. | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `card_token` | `string` | 可选 | — | | `card_cvv` | `string` | 可选 | — | | `card_info` | `object` | 可选 | — | | `card_info.card_number` | `string` | 必填 | — | | `card_info.cvv` | `string` | 必填 | — | | `card_info.expiry_month` | `integer` | 必填 | ≥ 1 · ≤ 12 | | `card_info.expiry_year` | `integer` | 必填 | — | | `card_info.cardholder_name` | `string` | 必填 | — | | `billing_address` | `object` | 可选 | — | | `payer_info` | `object` | 可选 | — | | `payer_info.name` | `string` | 可选 | — | | `payer_info.email` | `string` | 可选 | — | | `payer_info.phone` | `string` | 可选 | — | | `require_3ds` | `boolean` | 可选 | — | **请求示例** ```bash curl -X POST 'https://checkout.psp.example.com/checkout/creditcard/payment-intents/{payment_id}/pay' \ -H 'Content-Type: application/json' \ -d '{ "card_token": "string", "card_cvv": "string", "card_info": { "card_number": "string", "cvv": "string", "expiry_month": 1, "expiry_year": 0, "cardholder_name": "string" }, "billing_address": {}, "payer_info": { "name": "string", "email": "user@example.com", "phone": "string" }, "require_3ds": false }' ``` **响应** #### 响应 200 OK Payment submitted ### Complete card 3DS authentication **POST** `/checkout/creditcard/payment-intents/{payment_id}/complete-3ds` **路径参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `payment_id` | `string` | 必填 | — | **查询参数** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `t` | `string` | 可选 | Checkout token; provide this parameter or X-Checkout-Session-Token. | **请求头** | 名称 | 类型 | 必填 | 说明 | |---|---|---|---| | `X-Checkout-Session-Token` | `string` | 可选 | Checkout token; provide this header or t. | **请求体** (application/json · 必填) | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `authorization_id` | `string` | 必填 | — | | `transaction_id` | `string` | 必填 | — | | `auth_value` | `string` | 必填 | — | | `eci` | `string` | 必填 | — | | `cres_nonce` | `string` | 必填 | — | **请求示例** ```bash curl -X POST 'https://checkout.psp.example.com/checkout/creditcard/payment-intents/{payment_id}/complete-3ds' \ -H 'Content-Type: application/json' \ -d '{ "authorization_id": "00000000-0000-7000-8000-000000000000", "transaction_id": "string", "auth_value": "string", "eci": "string", "cres_nonce": "string" }' ``` **响应** #### 响应 200 OK 3DS completed --- # 更新日志 本文档站遵循语义化版本思路记录面向商户的契约变化。OpenAPI 文件是 API 参考的机器可读事实来源。 ## 2026-10-30 - 与 PSP 生产发布 `v1.0.0-rc.20261028.3` 对齐 Invoice、Transfer、Query 和商户 Webhook 契约。 - 修正 PIX 收款示例、收款系统单号、代付退款说明及幂等 Header 和冲突处理。 - 补充商户可用的请求、查询和响应结构,更新 Checkout 路由及令牌说明。 ## 2026-09-03 - Pre-GA 破坏性移除 Transfer 与 Merchant Webhook 中的 runtime taxonomy version。 - 支付分类改为稳定 identity 快照;taxonomy tuple 的 `inactive` 只阻止新订单,不改变已接受订单。 - 保留 OpenAPI、Webhook header 和 `classification_schema_version` 等独立合同版本。 ## 2026-08-27 - 首次发布商户 Gateway API 文档站。 - 按 Invoice、Transfer、Query、Merchant Webhook、Proof / Checkout 拆分参考文档。 - 增加 Ed25519 认证、幂等重试、Webhook 和上线检查指南。 - API Reference 使用独立全屏布局,移除重复的 VitePress 页面框架和非商户工具栏。