XD-OP-02 · DOC
接入指南
从商户进件、申请开放平台应用到拿到接入地址与调用凭证,再到事件通知与域名归属配置——按这条顺序走,不需要先写代码就能把接入前置办齐。
小豆支付开放平台是面向已有自有系统的商家的支付接入服务:通过服务器对服务器的接口,把支付宝与微信收款接进商家自己的系统,并以签名事件通知回传支付状态;资金由持牌支付公司收付结算。个人也能申请:主体类型选「个人」,身份证 + 本人银行卡即可完成商户进件。央行持牌机构清算,安全可靠的个人和企业支付接口。
本篇讲「怎么接得对」:请求怎么签、幂等怎么做、状态怎么判、收银与交付的边界在哪。字段清单见 API 参考。
1. 域名与 Base URL
- 商家 API 的 Base URL 只在登录后的门户「应用凭证」页下发(同一页还有
appId、kid与指纹);文档与示例一律写作<BASE_URL>,官方资料里不印 host,也不要在公开渠道互相转抄。取不到时请联系对接人。 - 不要在服务端代码里硬编码 host/IP/端口,也不要在前端拼接——把 Base URL 当配置项管理,环境切换时只改配置。
- 平台收银页地址永远使用下发响应里的
payUrl原值,商家不得自行拼接收银地址。
2. 域名归属验证(可选,建议完成)
平台不强制核验域名归属:申报域名由你自己填写,不做归属验证也能开通应用并收款。建议顺手完成一次核验——核验通过后该域名在平台侧标记为「已验证」,域名被他人冒用申报时归属更明确,处理争议时更有依据;核验也是免费的,两个形态二选一,整条流程在门户完成。
| 形态 | 你需要做什么 | 适用情形 |
|---|---|---|
| DNS TXT 记录(推荐) | 在域名的解析服务商处添加一条 TXT 记录 | 你能改域名解析(绝大多数情况) |
| 网站回源文件 | 在网站根目录放一个验证文件 | 改不了解析、但能改站点内容 |
2.1 DNS TXT 记录形态
门户签发的指引里会给出两个值:记录名与记录值。
- 记录名(完整):
_xiaodou-open-api.<你申报的域名>,例如申报pay.example.com时是_xiaodou-open-api.pay.example.com。 - 记录值:门户上那一串验证码(
xdch_开头),逐字符复制。
注意解析商的「主机记录」栏:国内多数解析服务商(DNSPod 等)要求这一栏只填主域名之前的部分,不是完整记录名。同一例子(申报
pay.example.com)在这些服务商里应填的是_xiaodou-open-api;若你把完整记录名填进去,实际生成的记录会变成_xiaodou-open-api.pay.example.com.pay.example.com,验证永远不通过。门户指引里会按你申报的域名直接给出这一栏该填的值,照抄即可。
2.2 网站回源文件形态
- 在站点根目录放置验证文件,使门户给出的地址(形如
https://<你申报的域名>/.well-known/xiaodou-open-api-challenge)返回的内容包含验证码即可通过(判据是「页面内容含该验证码」;原样只返回验证码是最稳妥的做法,可避免框架附加内容引入变量)。 - 该地址必须满足三条:可公网访问、走 HTTPS、不跳转(不能 302 到别处,也不能先跳登录页)。
2.3 生效时间与常见失败
| 现象 | 含义 | 你要做的 |
|---|---|---|
| 提示「暂未读取到验证记录」(唯一可重试的失败) | 平台按当前 DNS/站点状态还没读到你的配置 | 解析/部署生效通常需要几分钟,稍后重新点一次验证即可(同一条验证最多 5 次,每次只统计「没读到」) |
| 提示「已读取到配置内容,但与本页验证码不一致」 | 记录填了,但值不对(常见:复制时带了空格、换行,或填了上一次的验证码) | 该条验证就此作废(终态):重新签发 → 拿到新验证码 → 覆盖记录值 → 再验证。直接点「验证」只会拿到 409 |
| 提示「目标域名解析到非公网地址」 | 域名指向了内网/保留地址 | 同属终态:先修好解析,再重新签发验证(平台不会去访问非公网目标) |
| 提示「多次验证未通过,该验证已关闭」 | 尝试次数用尽(安全约束) | 在门户重新签发一条新的验证 |
| 提示「该验证已过期」 | 超出门户显示的有效期(24 小时) | 重新签发 |
- 上表的提示文案是大意——按失败原因分类,措辞以门户当场显示的为准,不要按字面去比对。
- 只有「暂未读取到验证记录」能原地重试;其余失败都是终态,必须重新签发(且新验证码要重新覆盖到 DNS 记录或回源文件)。每次失败门户都会在该条记录旁边给出具体原因,按上表处理。
- 申请时可以先不填域名:开通后在卖家门户的「应用配置变更」里补上你自己的域名(那里至少要填一条,但同样不需要核验);也可以申请时就填,是否核验都不影响开通与生效(域名归属不是判据)。
- 单次申报最多 10 个域名(超出会与「格式非法」共用同一句
400文案,请先自查数量)。 - 域名在本平台不是运行时访问控制:平台不会因为某个域名未验证而拒绝你发起的支付或回调。
3. 鉴权总览
| 方向 | 机制 | 关键材料 |
|---|---|---|
| 商家 → 平台 | XD-Signature-v1 六头签名(RSA-SHA256) | 你的 RSA2 私钥、appId、kid |
| 平台 → 商家(响应) | XD-Response-v1 响应头签名 | 平台公钥(固定文档页与门户双渠道发布) |
| 平台 → 商家(事件) | XD-Webhook-v1 投递头签名 | 平台公钥 |
- 六头成组提交,缺一个即
400;验签失败统一返回中性401(code 一致,但message会点出时间窗越界这类原因,排障时先看它);nonce 复用是409,不是 401。 - 时间窗与 nonce 防重放是强校验:服务器时间必须与标准时间对齐(建议常开 NTP)。
- 响应签名与事件签名的规范串与请求签名不同,不要复用同一段拼串逻辑 —— 见 安全红线 与 webhook 验签。
3.1 平台公钥与指纹(公示)
平台用同一把 RSA2 密钥对两类出站物签名:同步响应(XD-Response-v1)与事件投递(XD-Webhook-v1)。公钥 PEM 正文在本页公示;门户「应用凭证」页公示同一把钥的 kid 与指纹(与下方 kid/指纹逐字符一致,两处同源、可相互核对)。
| 项 | 值 |
|---|---|
密钥标识(kid) | xdpk-2026-09 |
| 指纹(规范化 PEM 字节的 SHA-256,十六进制小写) | c828f7e47c2eef5a958aa32009f5151bda43b585c979b40bf7c795b0b48d35f4 |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw9Z3B6q8jsaNu3KmswGF
Kl/8Rc3dWV2yFCq61VIbuPVEQPI6S1S2gbFeWpXz6KzrdAJxTDyMqfSDnSStoesn
t6FCMjgc0SBflQOjxsM2ahNDJO4JmJGj7Mca/nFCMrvVTy4tLxFkxMUxOcXhn4LU
QPCvpoIc7Y10xHBrqVizrHwMWNPxteWsOG4ZQUbXDrYddsceTcZ3a46cBhBUyqas
lNkWwzjaw9KmrdcXWOjDT3YCyMAXkcjLBh/7gr+sb11P4PUCjeQl2F6dDASIjZmO
EXFRSQJtXSRlna+WHWgVSFtrWBdyq+Gf+5pwjXMk1PrSWIG56QKhCXKCylNz8Kwn
TQIDAQAB
-----END PUBLIC KEY-----用法要点:
- 只从本页或门户取公钥;不要在未验签的响应或事件里取公钥(那等于让攻击者自己递钥匙)。
- 验签时按响应/事件头里的
key-id选中对应公钥;kid与上面不一致时先到本页确认是否有新版本。 - 指纹用于本地自检:算之前先按下面的规则规范化,再取 SHA-256(十六进制小写),与上表逐字符比对,可确认没抄错、没被中间环节替换:
- 把 PEM 文本里的 CRLF(
\r\n)与孤立 CR(\r)统一成 LF(\n); - 去掉首尾空白(含末尾换行);
- 对这段 UTF-8 文本字节求 SHA-256。 照抄上面的 PEM 块时,不要把文件末尾的换行一起算进去——那会得到另一个值,让你误以为「被换了钥匙」。
- 轮换:平台更换签名密钥时,会同时发布新的
kid与新公钥/指纹;新旧并存期内,请同时保留两把公钥并按kid选择。参考实现见 undefined(在公开仓与「下载接入包」内;不在「下载文档包」内)。
4. 幂等:三层,别混用
| 层 | 键 | 语义 |
|---|---|---|
| 请求级 | X-XD-Request-Id | 同 (appId, 方法, 路径, requestId) 绑定一次请求体摘要;重放原样返回首次响应;同 requestId 换请求体 → 409 冲突 |
| 业务级 | outTradeNo / outRefundNo | 同号同内容 → 幂等重放(200 + replayed=true);同号异内容 → 409 冲突 |
| 状态机 | 订单/退款状态 | 非法迁移或终态冲突 → 409 |
实践建议:
- 重试必须复用同一个
requestId(网络超时、5xx 重试场景)。换requestId重发等于新请求,可能造成重复下单。注意请求体也要逐字节相同:同一个requestId换了请求体(哪怕只是「不传 body」变成传{})会拿到409 OPEN_API_REQUEST_ID_CONFLICT。 - 「同号同内容」里的内容 = 业务摘要:
channel、tradeScope、amountFen、currency、subject、description、returnUrl、attach(完整清单见 API 参考 §2.7)——改动其中任何一个再重试,都是「异内容」→409。payerClientIp不在摘要内,重试时按实际情况传不会冲突。 outTradeNo由你的系统生成,不要用「时间戳+随机数」当订单号后重试——重试要用同一个号。- 遇到
409先查单,不要盲目换号重试。
5. 状态与终态可信边界
订单状态:CREATED → PAYING → SUCCEEDED,或收敛到 CLOSED / FAILED;另有 UNKNOWN(状态不确定,不判死)。退款状态:REQUESTED → PENDING → SUCCEEDED / FAILED,另有 UNKNOWN。
CLOSED / FAILED 不是终点(最容易写错的一条):支付机构随后确认到账时,平台会把订单提升为 SUCCEEDED 并推送 PAYMENT_SUCCEEDED——关单与到账竞态一律以支付机构为准。所以这两个状态不能建成吸收态,收到 PAYMENT_SUCCEEDED 就按到账处理。
可信边界(最容易踩的一条):
- ❌ 收银页被打开、收银页回跳
returnUrl、用户点了「完成」——都不是支付终态。 - ✅ 终态只认:查单接口返回的订单状态,或经验签的事件通知。
UNKNOWN 的处理纪律:UNKNOWN 表示「平台与支付机构之间的结果暂时不可知」,不是失败。遇到 UNKNOWN:不要重新下单、不要自行判失败、不要给买家提示「支付失败」;按查单/事件收敛即可(补偿策略见 退款与对账)。
6. 通道与交易范围申报
channel是你要走哪条收款通道(枚举见 API 参考);通道在下单时冻结,订单创建后不可改。tradeScope是交易形态申报:双申报的应用必填,单申报应用可省略(省略即取该申报形态)。申报与实际不符会被拒(422)。- 通道存在但链路未放行时返回
422 OPEN_API_CHANNEL_UNAVAILABLE——这是可预期的正常拒绝,不要按故障处理,也不要重试刷量。 - 放行粒度是「通道 × 交易形态」:微信 JSAPI 已开放(仅微信内打开收银页可调起;微信外手机浏览器与商家 App 内没有直接拉起形态);其他通道或交易形态可能尚未放行。接入前请与对接人确认你要用的那个组合是否已放行;平台侧没有自助查询面,也不要靠试探下单去确认。
7. 收银与交付的边界
- 平台负责:收款、支付事实、状态与事件。
- 商家负责:交付本身(卡密发放、服务开通、发货、开票、售后)。
- 收银页在买家点「返回商家」时把浏览器
303跳回你下单时冻结的returnUrl——它不是自动跳转:买家不点、刷新过页面时都不会回跳;按钮由收银页本次会话首次观察到支付成功时提供,扫码支付(PC 出码 → 手机付款)不受此限(只要收银页保持打开并轮询到支付成功,按钮照常提供并可回跳)。而且这是一次导航,不是终态通知,你的落地页必须自己再查一次单或等事件。 - 微信内(JSAPI)支付完成后微信会接管完成页:买家从微信支付记录中的「商家小票」点「返回商家」回到同一个
returnUrl(平台侧配置,无需商家开通;同样只是导航,不是终态)。 - 不要把交付内容(卡密、下载链接、账号密码)塞进
returnUrl的 query 里:它会被浏览器历史、日志、Referer 一路带走。交付只在你的服务端用订单状态驱动。
8. 商家自有 App / 宿主内的硬约束
如果你的买家在你自己的 App(WebView/内嵌浏览器)里打开收银链:
- 必须用系统浏览器打开(等价「跳转外部浏览器」语义),禁止在自有 App 的 WebView 内嵌打开收银页。
- 原因:内嵌容器的 UA 与浏览器特性会失真,平台侧按终端分派收款形态的判断会错,买家会落到一个拉不起来的页面。
- App 内的微信支付仍然走推荐路径(把链接送进微信内打开),平台侧没有「App 内直接拉起微信支付」的形态。
9. 时间与金额
- 金额:整数分,字段后缀
Fen;仅人民币。 - 时间:接口返回的时间字段是 ISO-8601 UTC;
payUrlExpireHint/ 事件occurredAt是 UTC epoch 秒。 - 判断「是否过期」以服务端返回的
expireAt为准,不要在客户端用本地时间自算窗口。
10. 联调支持边界(重要)
平台暂不提供沙箱、测试应用与回调模拟器。可用的替代路径:
- 本地 demo(undefined,只在公开仓提供;不在门户的两个下载包内):loopback + mock,全链跑通,不连生产支付机构。
- 上线检查清单(undefined):逐条自检,上线前必须全过。
- 首次生产首单人工陪跑:通过官网反馈渠道提交,平台侧协助(不承诺 SLA)。
本地 demo 与任何「mock 通过」都不能替代生产首单验证:mock 只证明你的签名与解析没问题,不证明资金链通。
open-platform/docs/02-integration-guide.md · 最后更新 2026-10-17
下载本文 .md
在 GitHub 查看