通用商户收款
外部系统已有自己的订单、订阅和交付时,可由商户服务端按金额创建 TDCloud 收款单,再让买家跳转收银台。无需预先创建 TDCloud 商品;TDCloud 确认收款和结算,商户系统自行更新业务订单与权益。
1. 准备商户服务端凭据
使用已绑定启用商户的机密客户端,申请并获批 merchant_payments.create、merchant_payments.read。现有应用不会自动增加权限,需联系平台调整。所有本页 OpenAPI 只接受 client_credentials 令牌,不接受用户授权令牌;调用方不提交买家编号或商户编号。
服务地址使用开发者中心分配的完整地址,见 接入概览。Secret 和 Access Token 仅保存在商户服务端。
curl -X POST "{TOKEN_ENDPOINT}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id={CLIENT_ID}" \
--data-urlencode "client_secret={CLIENT_SECRET}" \
--data-urlencode "scope=merchant_payments.create merchant_payments.read"使用响应的 access_token 和实际 scope,按 expires_in 管理有效期。Client Credentials 不签发用户 Refresh Token,过期后用服务端凭据重新获取令牌。公共客户端或未绑定商户的客户端不能使用此流程。
2. 查询可用支付渠道
GET {API_BASE_URL}/openapi/v1/merchant-payment-channels
权限:merchant_payments.read。HTTP 200 返回 {"items": [...]};无可用渠道时数组为空。
curl "{API_BASE_URL}/openapi/v1/merchant-payment-channels" \
-H "Authorization: Bearer {ACCESS_TOKEN}"| 字段 | 类型 | 说明 |
|---|---|---|
payment_no | string | 建单时使用的真实渠道编号,不是显示名称 |
name | string | 渠道名称 |
provider | string | 当前支持 stripe、dog-coin(TDC) |
owner | string | platform 或 merchant |
currency | string | 渠道支付币种 |
login_required | boolean | 渠道本身是否必须登录;商户禁止未登录消费时,收银台也会要求登录 |
test_mode | boolean | 是否使用管理员分配的 Stripe 测试渠道 |
可用渠道受商户归属、启用状态和调试授权约束。首期支持 TDC 与已配置签名 Webhook 的 Stripe。TDC 始终要求买家登录 TDCloud;其他渠道是否允许未登录消费由商户设置。不要将本目录套用于商品订单的渠道发现或报价。
3. 创建收款单
POST {API_BASE_URL}/openapi/v1/merchant-payments
权限:merchant_payments.create。请求头:Authorization: Bearer {ACCESS_TOKEN}、Content-Type: application/json。
| 参数 | 类型 | 必填 | 约束 |
|---|---|---|---|
external_reference | string | 是 | 商户业务订单引用,去除首尾空白后非空,最长 191 字节;创建后固定 |
idempotency_key | string | 是 | 同一次建单的重试键,最长 128 字节;同商户内唯一 |
amount_minor | integer | 是 | 正整数,按支付币种精度表示金额;最大 9007199254740991,且须能在平台内部精度内表示 |
currency | string | 是 | 已启用且渠道支持的币种,使用大写代码 |
payment_no | string | 否 | 传入时限定为该渠道;省略或为空时由买家在收银台选择可用支付方式 |
description | string | 否 | 展示给买家的付款内容,最长 500 字节,去除首尾空白;不要放入内部编号、密钥或其他敏感信息 |
expires_in | integer | 否 | 支付有效秒数,60~7200;默认 1800(30 分钟),建单后固定 |
notify_enabled | boolean | 否 | 省略时使用显式通知参数或商户默认配置;false 关闭本单通知,不能同时传通知地址/密钥;true 要求存在有效通知配置 |
notify_url | string | 否 | 服务端通知的公网 HTTPS 地址,须与 notify_secret 同时传入;见状态通知一节 |
notify_secret | string | 否 | 独立随机签名密钥,32~256 字节,不含空白 |
return_url | string | 否 | 支付确认后返回的完整 HTTPS 地址,最长 2048 字节;规则见下一节 |
金额示例
此接口使用 amount_minor,与商品订单接口的八位定点金额不同。
| 付款金额 | currency | amount_minor | 返回的 currency_decimals |
|---|---|---|---|
| 10.50 USD | USD | 1050 | 2 |
| 10.50 TDC | TDC | 1050000000 | 8 |
使用整数或十进制库按币种精度换算,不使用浮点乘法四舍五入拼金额。创建成功后核对返回的金额、币种和精度,不用实时汇率改写原订单。
curl -X POST "{API_BASE_URL}/openapi/v1/merchant-payments" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"external_reference":"merchant-order-123","idempotency_key":"merchant-order-123-attempt-1","amount_minor":1050,"currency":"USD","description":"订单说明","expires_in":1800,"return_url":"https://merchant.example/orders/merchant-order-123?source=tdcloud#payment"}'成功响应:HTTP 201,重复同参数请求也返回 201 和同一收款单。
{
"order_no": "MP_EXAMPLE",
"external_reference": "merchant-order-123",
"payment_no": "",
"amount_minor": 1050,
"currency": "USD",
"currency_decimals": 2,
"status": "pending",
"test_mode": false,
"checkout_url": "https://tdcloud.cc/checkout/merchant/MP_EXAMPLE?token=CHECKOUT_TOKEN",
"return_url": "https://merchant.example/orders/merchant-order-123?source=tdcloud#payment",
"expires_at": "2026-10-08T14:00:00+08:00"
}示例编号、地址、时间均为占位值。保存 order_no 与业务订单的映射,将实际返回的 checkout_url 交给买家浏览器;不能把它当作已支付凭证或在日志中暴露其中的令牌。expires_at 在建单时确定,发起渠道会话、刷新页面和重试均不会延长。
买家选择支付方式
默认建单不传 payment_no,TDCloud 保存当时可用的渠道及金额、汇率、手续费规则,返回一个收银台地址。买家在收银台选择付款方式;只有一种可用方式时直接显示该方式。商户希望限定单一渠道时仍可传入 payment_no,历史固定渠道订单沿用原方式。
买家只能看到当前商户可用且符合实际付款账号授权的方式。需要登录的方式会提示登录,未登录时不能使用 TDC 余额。TDC 可按建单时保存的汇率支付其他已启用币种的订单,页面显示实际应付 TDC;金额和订单币种保持不变。自有渠道手续费在建单时检查,发起付款时再次检查并预占。
首次发起渠道付款后锁定方式。再次点击或刷新继续使用同一支付尝试;渠道响应不明时不能切换方式另建会话。超时不会因选择方式或刷新而延期。尚未选择时,服务端 payment_no 为空、test_mode 为 false;选择后查单反映实际渠道和测试模式,不改变原建单参数的幂等校验。
买家收银台只展示商户名称、付款内容、金额、倒计时与支付方式。外部订单引用、TDCloud 对账单号、支付事件号、渠道交易号及钱包结算信息保留在商户服务端查单和通知接口。收银台使用本单专属的方式标识,不能通过提交任意渠道编号越权付款。
幂等与重试
同一商户、同一 idempotency_key、相同规范化参数返回同一单。金额、币种、渠道、外部引用、描述、有效期或返回地址变化返回 HTTP 409 payment_conflict。不同商户的幂等空间隔离;external_reference 本身不是去重键。
超时后以原键和原参数重试,不能仅因没有收到响应就换键再建一张可支付单。确需新支付尝试时,先核对原单和渠道结果,避免两张单均可付款。
支付超时
expires_in是从建单成功开始计算的有效秒数。省略默认 30 分钟,也可传入 60~7200 秒(1 分钟~2 小时)。显式1800与省略等价;同键改为其他有效期返回409 payment_conflict。- 到达固定
expires_at后禁止继续发起支付,收银台发起接口返回410 payment_expired。同键重试仍返回原单及原期限,不能续期。新上限用于新建订单,历史订单保留原期限。服务端查询及对账仍可用。 - 尚未发起渠道会话的单在查询、列表、收银台读取或后台巡检时转为
expired。已发起会话的单先进入review,后台核对付款并关闭尚未支付的 Stripe 会话;确认渠道已过期后才释放预占手续费和授信,且只释放一次。 - 异步付款未决、渠道查单/关单失败或会话创建响应不明继续保留
review,不能直接认定失败或重复扣费。真实付款确认后,即使回调晚到,也会在钱包及账务完成后返回paid;无法获取会话号的异常需管理员按 TDCloud 单号到 Stripe 核对。 - 后台每分钟巡检,并在渠道异常时重试。Stripe 会话自身的最短期限为 30 分钟,可能晚于收款单期限;TDCloud 会主动关单,不会用渠道期限延长收款单。不能将本地到期理解为渠道地址必定在同一瞬间关闭。Stripe 会话到期规则 、主动关闭会话 。
收银台显示倒计时,到期后移除支付入口并提示核对结果。商户遇到 review 或已付款异常时,应继续查询原单、保留稳定单号供人工对账;确认原支付尝试结束后再创建新尝试。
4. 接入支付返回地址
return_url 由商户服务端创建收款单时设置,随后固定。首尾空白会被去除;必须是完整 HTTPS URL,不允许账号密码、反斜线或未编码空白/控制字符。未传、空字符串或历史单没有此字段时,买家留在原收银台流程。
此参数与 OAuth 的 redirect_uri 不同:无需登记为 OAuth 回调,可以按业务订单设置路径、查询参数和 # 片段。浏览器自行在收银台 URL 上添加 return_url 或 status=paid 不会覆盖保存地址或确认支付。
支付流程如下:
- 买家进入 TDCloud
checkout_url,按商户策略登录或匿名付款。 - Stripe 完成或取消付款先回 TDCloud 收银台;TDC 支付也在收银台确认。
- 收银台向服务端查单。只有返回
paid后才自动进入保存的return_url,并保留“返回商户”入口。 - 异步付款未决时继续查单;取消支付、到期或退款/拒付不触发成功返回。浏览器取消不保证收款单立即变为终态。
- 商户返回页调用自己的后端,再由后端查 TDCloud 收款单,核对金额、币种和稳定
event_id,幂等更新业务订单。
原地址的路径、查询参数和片段保留;TDCloud 不自动添加 Token、单号或支付状态。商户应在原地址中携带自己的业务引用,并将回跳仅作为触发查单的入口。浏览器付款成功页、回跳参数或渠道会话创建成功都不能作为交付依据。
5. 服务端状态通知
状态通知是可选能力。默认不开启,可通过服务端查单确认结果。浏览器返回地址和服务端通知互相独立,均不必填写。
商户默认配置
在 商户中心 → 支付与结算 → 收款状态通知 配置公网 HTTPS 通知地址。保存地址后平台生成独立签名密钥,仅本次显示;将其保存到商户接收服务,完成验签后再启用默认通知。读取设置不会返回密钥,遗失时可更换。
| 建单方式 | 通知行为 |
|---|---|
| 不传通知参数 | 商户默认通知启用时使用默认地址与密钥,否则不发送 |
notify_enabled: false | 关闭本单通知,即使商户已启用默认通知;不要同时传地址或密钥 |
notify_enabled: true | 使用显式地址和密钥,否则使用已启用的商户默认配置;均不可用则拒绝建单 |
成对传 notify_url / notify_secret | 覆盖商户默认配置,使用本次提供的地址和密钥 |
生效地址与密钥在建单时固定。更改、关闭默认配置或更换密钥只影响新订单,历史订单及补发继续使用原配置;接收服务需保留旧密钥直到历史通知处理完成。相同幂等键重试不会重新读取默认配置或改变已创建订单。
单笔指定通知
建单时同时传入 notify_url 和 notify_secret,TDCloud 会向商户服务端发送 HTTPS POST。notify_url 仅允许公网 HTTPS,最长 2048 字节,不允许账号密码、片段、内网地址或重定向。notify_secret 使用商户生成的独立随机密钥,32~256 字节,不含空白;不要使用 OAuth Client Secret。地址和密钥参与建单幂等校验,创建后不能修改。
{
"external_reference": "merchant-order-123",
"idempotency_key": "merchant-order-123-attempt-1",
"amount_minor": 1050,
"currency": "USD",
"payment_no": "CHANNEL_NO",
"notify_url": "https://merchant.example/hooks/tdcloud",
"notify_secret": "MERCHANT_GENERATED_RANDOM_SECRET_AT_LEAST_32_BYTES"
}商户默认通知未启用时,两个参数均省略不发送通知;已启用默认通知时,使用 notify_enabled: false 关闭本单通知。密钥不会出现在建单、查单、收银台或投递记录响应中。
通知内容与验签
每次状态变化发送 merchant_payment.status_changed,包含创建后的 pending、支付确认后的 paid、review、expired、refunded 和 chargeback。支付和账务确认、订单状态与通知事件在同一事务中保存;投递失败不撤销已确认支付。
{
"id": "MN_NOTIFICATION_EXAMPLE",
"type": "merchant_payment.status_changed",
"api_version": 1,
"created_at": "2026-10-08T07:45:00Z",
"data": {
"order_no": "MP_EXAMPLE",
"external_reference": "merchant-order-123",
"payment_no": "CHANNEL_NO",
"amount_minor": 1050,
"currency": "USD",
"currency_decimals": 2,
"status": "paid",
"status_version": 2,
"test_mode": false,
"expires_at": "2026-10-08T08:00:00Z",
"event_id": "MPE_PAYMENT_EXAMPLE",
"provider_trade_no": "cs_EXAMPLE",
"occurred_at": "2026-10-08T07:45:00Z"
}
}data 是该状态发生时的收款单快照,不含收银台 Token、返回地址或通知密钥。id 是通知事件号;data.event_id 是确认支付后的稳定支付事件号,两者用途不同。created_at 是通知生成时间,支付时间使用 data.occurred_at。
| 请求头 | 含义 |
|---|---|
X-TDCloud-Event-ID | 与请求体 id 一致的通知事件号 |
X-TDCloud-Timestamp | 本次投递的 Unix 秒时间戳 |
X-TDCloud-Signature | sha256= 加 HMAC-SHA256 的小写十六进制结果 |
签名内容为 时间戳 + "." + 原始 HTTP 请求体字节,密钥为该单创建时生效的签名密钥(商户默认密钥或显式 notify_secret)。使用常量时间比较签名,并拒绝与当前时间相差超过 5 分钟的时间戳;服务端时钟须同步。验签前不要重新序列化 JSON。例如 Python:
import hashlib
import hmac
import time
# raw_body is the untouched HTTP request body; secret is stored server-side.
timestamp = request.headers["X-TDCloud-Timestamp"]
provided = request.headers["X-TDCloud-Signature"]
expected = "sha256=" + hmac.new(
secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
if abs(time.time() - int(timestamp)) > 300 or not hmac.compare_digest(expected, provided):
raise ValueError("Invalid TDCloud notification")验签后核对订单引用、金额、币种和 test_mode,按通知 id 去重。通知可能重复或乱序;同单 status_version 单调递增,低版本不能覆盖高版本。查单也返回 status_version。支付记账继续按 data.event_id 幂等;只有 paid 表示确认支付,review 和 expired 不能作为已支付凭证。退款/拒付保留原支付事件号。
确认、重试与补发
商户应先可靠保存通知,再返回 HTTP 2xx;响应体不参与确认。单次投递最多等待 8 秒。超时、连接失败、3xx、4xx 和 5xx 均重试,不跟随重定向。首次异步投递后,失败间隔依次为 1 分钟、5 分钟、15 分钟、30 分钟、1 小时、2 小时、4 小时、6 小时、8 小时,共最多 10 次自动尝试。工作进程中断后可恢复;通知事件号与请求体保持一致,每次重签时间戳。
商户也可在 商户中心 → 支付与结算 → 收款通知记录 输入收款单号查看投递结果和最近明细,自动重试耗尽后点击“补发通知”。页面仅查询当前商户的订单。
| 方法与路径 | 权限 | 用途 |
|---|---|---|
GET /openapi/v1/merchant-payments/{order_no}/notifications | merchant_payments.read | 本商户订单的投递记录,HTTP 200 返回 {"items": [...]} |
POST /openapi/v1/merchant-payments/{order_no}/notifications/{event_id}/retry | merchant_payments.create | 将自动重试耗尽的通知重新排队,HTTP 202 返回 {"accepted": true} |
记录按状态版本倒序返回,limit 默认 20,允许 1~100,越界回退 20。字段包括通知 event_id、status_version、event 快照、delivery_status(pending / delivered / failed)、累计 attempts、next_attempt_at、last_http_status、last_error 和最近 20 条 delivery_attempts。尝试记录没有 finished_at 表示进程中断或仍在投递。补发不生成新事件、不改订单状态;已送达或仍待投递的事件重复请求不会额外排队。重试接口按客户端每分钟最多 30 次。
通知与查单并用:未收到通知时查询原单并定期对账;通知投递耗尽不代表支付失败。
6. 服务端查单与对账
| 方法与路径 | 权限 | 用途 |
|---|---|---|
GET /openapi/v1/merchant-payments/{order_no} | merchant_payments.read | 查询本商户一单,HTTP 200 返回收款单对象 |
GET /openapi/v1/merchant-payments | merchant_payments.read | 按本商户外部引用或 TDCloud 单号过滤,HTTP 200 返回 {"items": [...]} |
列表必须提供至少一个 external_reference 或 order_no;两者同时提供时共同匹配。limit 默认 20,范围 1–100,非法或越界回退 20。按最新记录优先返回,不提供页码、总数或完整商户订单分页。
curl "{API_BASE_URL}/openapi/v1/merchant-payments/{ORDER_NO}" \
-H "Authorization: Bearer {ACCESS_TOKEN}"
curl --get "{API_BASE_URL}/openapi/v1/merchant-payments" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
--data-urlencode "external_reference=merchant-order-123" \
--data-urlencode "limit=20"单笔结果包含建单返回的字段;下列字段用于确认支付:
| 字段 | 类型 | 说明 |
|---|---|---|
status | string | 收款单状态,见下表 |
status_version | integer | 同单状态版本,防止旧通知覆盖新状态;历史未变更订单可为 0 |
event_id | string,可省略 | 确认支付后的稳定、唯一支付事件号;用于商户幂等记账 |
provider_trade_no | string,可省略 | 渠道交易号;TDC 为 TDC:{order_no} |
occurred_at | string 或 null | 确认支付时间,RFC 3339,保留响应中的时区 |
settlement_tdc | string,可省略 | 平台渠道锁定的结算 TDC,按八位定点整数表示;非支付凭证 |
return_url | string,可省略 | 已保存的商户返回地址 |
已支付响应示例(仅展示核对字段):
{
"order_no": "MP_EXAMPLE",
"external_reference": "merchant-order-123",
"payment_no": "CHANNEL_NO",
"amount_minor": 1050,
"currency": "USD",
"currency_decimals": 2,
"status": "paid",
"event_id": "MPE_EXAMPLE",
"provider_trade_no": "pi_EXAMPLE",
"occurred_at": "2026-10-08T13:45:00+08:00"
}expires_at 同样为 RFC 3339 时间,不是 Unix 秒。单号、事件号和渠道交易号按字符串保存。服务端状态通知可触发商户处理,但仍应保留有间隔、有超时上限的查单和定期对账。
| 状态 | 含义与处理 |
|---|---|
pending | 未决,继续核对;不能交付,也不能仅凭本地计时判失败 |
paid | 收款与商户账务处理已确认;核对订单引用、金额和币种后按 event_id 只记一次 |
review | 支付期限已到但渠道结果未决或核对失败;创建响应不明时需平台人工核对,不要换键重复收款 |
expired | 已核对无付款的到期状态;晚到的真实收款仍可能被核验并转为 paid,应继续异常对账 |
refunded | 已确认退款,保留原支付事件;商户按自己的退款与权益规则处理 |
chargeback | 已确认拒付,保留原支付事件;商户处理争议和业务状态 |
不要套用商品订单接口的数字 status。当前未定义通用 failed、cancelled 或公开取消接口;未来未知状态保留为待核对,不按支付成功处理。
7. 结算、手续费与调试
平台渠道按建单汇率转为 TDC 记入商户钱包;平台按商户配置 0–30 天冻结,0 天可实时使用。商户自有渠道由商户自行收款,TDCloud 不重复入账;平台手续费在建单时检查资金,在获取支付链接前再次检查并预占,优先使用可用 TDC,再使用已获批授信。无法筹足则拒绝交易。
投诉先阻止该单冻结销售款释放,核实损失后依次使用该单冻结款、担保金、可用余额及待追回余额处理。不可返还的手续费可能导致损失超出冻结款。退款、拒付和投诉处置由平台核验,不能用修改商户业务订单代替平台账务动作;本页没有商户自助退款/提现 OpenAPI。更多操作见 支付与结算。
test_mode: true 仅表示管理员指定分配的 Stripe 测试渠道。消费者不发生真实 Stripe 扣款,但 TDCloud 正常订单、账务、钱包 TDC 和冻结流程仍执行。测试权限不由管理员身份自动获得,也不代表可随意混用生产业务;测试账号、商品和交付由管理员维护。
调试配置只填写商户时,指定商户的所有买家可使用所选测试渠道;只填写付款用户时,这些用户可在所有商户下使用;同时填写时,商户与付款用户条件须同时满足。两项均为空不授予使用权,多个编号在各自列表内匹配任意一个。
服务端渠道目录和建单没有买家身份,先按商户范围检查。存在付款用户限制的测试渠道会在目录中返回 login_required: true;建单成功不代表任意买家均可付款。收银台按实际 TDCloud 登录用户检查,生成或再次获取渠道支付链接前再次检查;请求中的自报买家编号不能授予调试权限。付款用户限制要求登录,即使商户已允许未登录消费。已发出的渠道会话仍可能在撤销调试权限后成功,已验证收款继续正常入账。
8. 错误、限流与接入检查
错误至少包含字符串 error。HTTP 202 的 provider_result_pending 也不是支付成功。按 HTTP 状态和错误码分支,不依赖中文文案。
| HTTP | error | 处理 |
|---|---|---|
| 400 | invalid_notify_config | 开启单笔通知需提供地址和密钥或启用商户默认通知;关闭单笔通知不能同时传地址或密钥 |
| 400 | invalid_notify_url | 使用公网 HTTPS 通知地址,不能包含账号密码、片段或内网目标 |
| 400 | invalid_notify_secret | 通知地址须配独立随机密钥,32~256 字节,不含空白 |
| 503 | notification_unavailable | 稍后用原幂等键重试 |
| 400 | invalid_request | 核对参数、金额及列表过滤条件 |
| 400 | invalid_expires_in | 修正有效期为 60~7200 秒的整数;返回 error_description 和 field: "expires_in" |
| 400 | invalid_return_url | 修正 HTTPS 地址;同时返回 error_description、field: "return_url" |
| 401 | invalid_token | 重新获取有效 OAuth 令牌 |
| 403 | insufficient_scope、client_credentials_required | 核对批准权限及令牌主体 |
| 403 | merchant_inactive | 请平台恢复绑定商户 |
| 403 | login_required | 收银台要求买家登录 |
| 403 | payment_debug_access_denied | 核对调试渠道分配与启用状态 |
| 404 | payment_not_found | 核对单号及本商户归属,不推断其他商户资源 |
| 409 | payment_conflict | 原幂等键参数冲突;收银台支付状态冲突也可能使用此码 |
| 410 | payment_expired | 不能继续发起支付;查询原单并核对付款结果,避免重复下单 |
| 409 | provider_result_mismatch | 由平台核对渠道会话、金额和币种 |
| 422 | channel_unavailable、currency_rate_unavailable | 重新确认渠道、币种及平台汇率 |
| 422 | merchant_fee_funding_unavailable | 核对平台费率、可用钱包和授信额度 |
| 202 | provider_result_pending | 渠道结果未明,保留原单并核对 |
| 429 | rate_limited | 按 Retry-After 等待,避免持续重试 |
| 503 | rate_limit_unavailable | 限流服务暂不可用,有限退避重试 |
| 500 | server_error | 保存脱敏上下文并向平台反馈;写请求复用原幂等键 |
建单每客户端每分钟 60 次;查单、过滤列表与渠道目录共用每客户端每分钟 300 次。收银台按 IP 限速:查询 180 次、发起支付 30 次/分钟。
完成接入后,检查动态金额、支付返回页、同键重试及参数冲突、商户权限、未登录消费、支付超时和异步结果。确认业务订单只按稳定 event_id 更新一次,并按退款、拒付结果处理后续业务。Stripe Webhook 验签由 TDCloud 完成。