HTTP 端 API - 订阅支付#
订阅模型概览#
订阅支付基于链上订阅合约(Permit2 AllowanceTransfer 授权模式)实现"一次签名授权、按周期自动拉款":
-
Buyer(付款人) 链下对两个对象签名:
SubscriptionTerms(订阅条款)+ Permit2PermitSingle(额度授权),不需要为每期付款重复签名; -
Seller(商户)后端 收集 Buyer 双签后调用 Broker 接口创建订阅,并在每个计费周期到期后发起扣款(Seller-Driven);
-
Broker(facilitator) 校验签名与条款后代为提交链上交易,
terms.facilitator必须等于/supported下发的 facilitator 地址。
| 维度 | 说明 |
|---|---|
| scheme | period |
| 资产转移 | Permit2 AllowanceTransfer → 订阅合约周期拉款 |
| 代币兼容性 | 所有 ERC-20(需先对 Permit2 canonical 合约 approve) |
| 计费周期 | 固定秒数(periodMode=0)或自然月(periodMode=1) |
| 结算时序 | 异步为主;写接口支持 syncSettle=true 阻塞等待链上终态 |
| 金额语义 | 每期扣 amountPerPeriod,跳期不补扣(只扣当前周期) |
| 套餐管理 | 升级立即生效并扣新档首期;预约降级(到周期末生效) |
-
Base URL:
https://web3.okx.com -
路径前缀:
/api/v6/pay/x402 -
Network:X Layer(chainId
196,CAIP-2 标识eip155:196)
认证#
接口分两档鉴权:
-
API Key 鉴权:全部写接口(创建 / 扣款 / 升降级 / 取消 / 取消降级 / 清理过期)以及
charges、pending两个商户视角查询,请求头携带OK-ACCESS-*字段。API Key 对应的商户身份同时用作接口鉴权基准:创建订阅时落库,之后的扣款 / 升降级 / 商户侧取消必须由同一商户发起。 -
公开只读:
/supported、subscriptions/detail、buyers/{buyer}/*无需 API Key(返回的均为链上可推导数据),Buyer 可直接调用;仍受限频约束。
| Header | 必传 | 描述 |
|---|---|---|
OK-ACCESS-KEY | 是 | API Key |
OK-ACCESS-SIGN | 是 | 请求签名 |
OK-ACCESS-PASSPHRASE | 是 | API 密码短语 |
OK-ACCESS-TIMESTAMP | 是 | ISO 8601 时间戳 |
Content-Type | 是 | POST 请求需设为 application/json |
所有响应统一使用业务包络:
{
"code": "0",
"msg": "",
"data": { /* 业务字段 */ }
}
业务错误时 code 为非 "0",data 为 null,msg 携带机器可读的错误标识(如 period_not_due),错误码集中见文末错误码章节。
通用约定#
syncSettle(写接口通用)#
写接口 body 均支持 syncSettle 字段:
-
true:阻塞轮询链上终态或超时(默认 5000ms),返回时data反映落库后的最新状态; -
false/ 不传:提交后立即返回(状态多为pending),后续用查询接口轮询。
字段表示#
-
金额(
amountPerPeriod/initialChargeAmount/amount):原子单位十进制字符串; -
时间(
periodSec/startAt/termsDeadline/expiration/deadline):Unix 秒; -
subId/salt/nonce/permitHash/changeFromSubId:0x+ 64 hex 的 bytes32; -
地址:
0x+ 40 hex,小写。
枚举码#
| 枚举 | 取值 |
|---|---|
订阅状态 state | 0 pending / 1 active / 2 completed / 3 canceled / 4 changed / 99 failed(链下提交失败本地态) |
扣款流水状态 charge.state | 0 pending / 1 success / 2 failed |
扣款类型 chargeType | 1 首期 / 2 周期扣款 / 3 降级后首期 / 4 过期清理标记 |
待生效变更状态 pendingChange.state | 0 pending / 1 activated / 2 canceled / 3 expired |
变更生效方式 changeEffectiveAt | 0 none(创建)/ 1 immediate(升级)/ 2 period_end(降级) |
取消发起方 cancelAuth.initiator | 0 payer / 1 merchant |
周期模式 periodMode | 0 fixed_seconds(固定秒数)/ 1 calendar_month(自然月) |
周期模式(periodMode)#
| 模式 | periodSec 要求 | 周期边界 |
|---|---|---|
0 固定秒数 | 必须 > 0 | startAt + n × periodSec |
1 自然月 | 必须 = 0 | addMonths(billingAnchorAt, n)(月底截断,保留时分秒) |
自然月关键语义:
-
锚点不漂移:每个边界都从原始锚点
billingAnchorAt加 n 个月,1/31 12:00 → 2/28 → 3/31 → 4/30,不会链式漂移成 28 号节奏; -
边界精确时刻属于下一期;
-
billingAnchorAt:普通新订阅 =startAt;升级 / 降级激活继承旧订阅锚点(月底节奏延续);startAt=0场景由链上回填; -
时间全部为 UTC 时间戳;
-
跳期不补扣:漏掉的中间周期释放预留额度,扣款只扣当前周期(两种模式同语义)。
1. /api/v6/pay/x402/supported#
/api/v6/pay/x402/supported
查询 Broker 支持的 scheme、network 及签名者列表(无需 API Key)。订阅接入前必须调用此接口,获取订阅合约地址与 facilitator 地址。
订阅能力以 kinds 数组中 scheme="period" 的条目广播(按灰度开关与链上合约配置动态输出):
kinds[].extra 子字段 | 描述 |
|---|---|
facilitatorAddress | facilitator EOA 地址。Buyer 必须原样填进 terms.facilitator —— 合约要求提交交易的地址与其一致 |
subscriptionContract | 订阅合约地址,Permit2 PermitSingle.spender 必须等于它 |
permit2Contract | Permit2 canonical 合约地址,Buyer 第一层 ERC-20 approve 的目标 |
signers[network] 列出该链全部可用 facilitator EOA,供 Seller 校验某地址确属本 facilitator。
请求示例#
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/supported'
响应示例#
{
"code": "0",
"msg": "",
"data": {
"kinds": [
{ "x402Version": 2, "scheme": "exact", "network": "eip155:196" },
{ "x402Version": 2, "scheme": "aggr_deferred", "network": "eip155:196" },
{
"x402Version": 2,
"scheme": "period",
"network": "eip155:196",
"extra": {
"facilitatorAddress": "0xFacilitatorEOA...",
"subscriptionContract": "0xSubscriptionContract...",
"permit2Contract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
}
}
],
"extensions": [],
"signers": {
"eip155:196": [
"0xFacilitatorEOA...",
"0xFacilitatorEOA2..."
]
}
}
}
2. /api/v6/pay/x402/subscriptions#
/api/v6/pay/x402/subscriptions
创建订阅。Buyer 双签(SubscriptionTerms + Permit2 PermitSingle)由 Seller 后端转发提交,合约创建订阅并按首期参数扣首期。
请求参数#
| 参数 | 类型 | 必传 | 描述 |
|---|---|---|---|
chainIndex | Long | 是 | 链索引,如 196 |
terms | Object | 是 | 订阅条款,详见公共数据结构 SubscriptionTerms |
permit | Object | 是 | Permit2 授权,详见公共数据结构 PermitSingle |
termsSig | String | 是 | terms 的 EIP-712 签名(65 字节 hex),签名者 = terms.payer |
permitSig | String | 是 | permit 的 EIP-712 签名(65 字节 hex),签名者 = terms.payer |
syncSettle | Boolean | 否 | 见通用约定 syncSettle |
约束:
-
创建时
terms.changeFromSubId必须为全 0,terms.changeEffectiveAt必须为0; -
首期规则:
initialChargePeriods > 0时要求initialChargeAmount ≤ initialChargePeriods × amountPerPeriod;initialChargePeriods = 0 且 initialChargeAmount > 0为 pre-start 预收费(要求≤ amountPerPeriod且startAt > now); -
permit 额度须覆盖订阅全额承诺,
permit.details.expiration不得早于订阅服务窗口结束; -
创建会产生资金转移,对
payer与merchant执行合规筛查,命中黑名单拒绝。
响应参数#
| 参数 | 类型 | 描述 |
|---|---|---|
subId | String | 订阅 ID(bytes32,= terms 的 EIP-712 摘要) |
txHash | String | 创建交易哈希 |
state | Integer | 订阅状态,见枚举码 |
请求示例(自然月月付)#
curl --location --request POST 'https://web3.okx.com/api/v6/pay/x402/subscriptions' \
--header 'Content-Type: application/json' \
--header 'OK-ACCESS-KEY: 37c541a1-****-****-****-10fe7a038418' \
--header 'OK-ACCESS-SIGN: leaV********3uw=' \
--header 'OK-ACCESS-PASSPHRASE: 1****6' \
--header 'OK-ACCESS-TIMESTAMP: 2026-04-01T12:21:41.274Z' \
--data '{
"chainIndex": 196,
"terms": {
"payer": "0x1111111111111111111111111111111111111111",
"merchant": "0x2222222222222222222222222222222222222222",
"facilitator": "0xFacilitatorEOA...",
"token": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
"amountPerPeriod": "5000000",
"periodSec": 0,
"maxPeriods": 12,
"startAt": 0,
"initialChargePeriods": 1,
"initialChargeAmount": "5000000",
"termsDeadline": 1781000000,
"permitHash": "0xab12...permitStructHash...cd34",
"salt": "0x7f3a...random32bytes...9e01",
"planId": "0x0000...keccak(pro_monthly)...0000",
"planTier": 2,
"changeFromSubId": "0x0000000000000000000000000000000000000000000000000000000000000000",
"changeEffectiveAt": 0,
"periodMode": 1
},
"permit": {
"details": {
"token": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
"amount": "60000000",
"expiration": 1812600000,
"nonce": 5
},
"spender": "0xSubscriptionContract...",
"sigDeadline": "1781000600"
},
"termsSig": "0x<65字节 hex>",
"permitSig": "0x<65字节 hex>",
"syncSettle": true
}'
固定秒数模式:
periodMode=0、periodSec填正数(如月付2592000)。
响应示例#
{
"code": "0",
"msg": "",
"data": {
"subId": "0x9a8b...termsDigest...c7d6",
"txHash": "0xabc...create...def",
"state": 1
}
}
3. /api/v6/pay/x402/subscriptions/charge#
/api/v6/pay/x402/subscriptions/charge
周期扣款。计费周期到期后由 Seller 后端发起,无需 Buyer 再次签名。仅创建该订阅的商户(API Key)可调用。
请求参数#
| 参数 | 类型 | 必传 | 描述 |
|---|---|---|---|
subId | String | 是 | 订阅 ID |
syncSettle | Boolean | 否 | 见通用约定 syncSettle |
校验链:订阅必须 active;未扣完全部周期(否则 all_periods_charged);当前周期已到期(否则 period_not_due);到期时对 payer 与 merchant 执行合规筛查。
响应参数#
| 参数 | 类型 | 描述 |
|---|---|---|
subId | String | 原订阅 ID(回显请求) |
period | Long | 本次扣款的周期号(= 当前周期;跳期不补扣) |
txHash | String | 扣款交易哈希 |
state | Integer | 扣款流水状态:0 pending / 1 success / 2 failed |
planChangeTriggered | Boolean | 本周期是否触发了已排程的降级切换 |
newSubId | String | planChangeTriggered=true 时为降级后新订阅 ID;否则 null |
请求示例#
curl --location --request POST 'https://web3.okx.com/api/v6/pay/x402/subscriptions/charge' \
--header 'Content-Type: application/json' \
--header 'OK-ACCESS-KEY: 37c541a1-****-****-****-10fe7a038418' \
--header 'OK-ACCESS-SIGN: leaV********3uw=' \
--header 'OK-ACCESS-PASSPHRASE: 1****6' \
--header 'OK-ACCESS-TIMESTAMP: 2026-04-01T12:21:41.274Z' \
--data '{ "subId": "0x9a8b...c7d6", "syncSettle": true }'
响应示例 — 普通扣款#
{
"code": "0",
"msg": "",
"data": {
"subId": "0x9a8b...c7d6",
"period": 4,
"txHash": "0xabc...charge...def",
"state": 1,
"planChangeTriggered": false,
"newSubId": null
}
}
响应示例 — 本周期触发降级切换#
{
"code": "0",
"msg": "",
"data": {
"subId": "0x9a8b...c7d6",
"period": 4,
"txHash": "0xabc...activate...def",
"state": 1,
"planChangeTriggered": true,
"newSubId": "0x2c1d...newDowngradeSubId...8f0a"
}
}
4. /api/v6/pay/x402/subscriptions/change#
/api/v6/pay/x402/subscriptions/change
套餐升降级。升级(changeEffectiveAt=1)立即生效并扣新档首期;降级(changeEffectiveAt=2)排程到当前周期末,由下一次 charge 触发切换。仅创建该订阅的商户可调用。
请求参数#
| 参数 | 类型 | 必传 | 描述 |
|---|---|---|---|
chainIndex | Long | 是 | 链索引 |
oldSubId | String | 是 | 旧订阅 ID(信息字段;服务端以 newTerms.changeFromSubId 为准) |
newTerms | Object | 是 | 新条款:changeFromSubId 必须 = 旧 subId,见公共数据结构 SubscriptionTerms |
permit | Object | 是 | 新 Permit2 PermitSingle(覆盖新档全额承诺) |
termsSig / permitSig | String | 是 | EIP-712 签名,签名者 = newTerms.payer |
syncSettle | Boolean | 否 | 见通用约定 syncSettle |
方向判定与约束:
-
newTerms.planTier > 旧 planTier必须changeEffectiveAt=1(升级);<必须changeEffectiveAt=2(降级);相等拒绝(tier_same); -
跨新旧订阅必须一致:
payer/merchant/facilitator/token/periodSec/periodMode(周期模式不可切换); -
旧订阅必须
active且没有已排程未生效的降级(否则pending_change_exists); -
降级的
newTerms.initialChargeAmount必须 = 0; -
newTerms.startAt规则:旧订阅未生效(pre-start)时须 = 旧订阅startAt;降级与固定模式已生效升级须 = 0;自然月已生效升级可 = 旧订阅当前周期起点(对齐升级,继承旧锚点、第 1 期回溯覆盖旧当期并全额扣款)或 = 0(从交易时间重新建锚)。
响应参数#
| 参数 | 类型 | 描述 |
|---|---|---|
newSubId | String | 新订阅 ID(= newTerms 摘要)。升级即刻生效;降级为待生效的新 subId |
txHash | String | 交易哈希 |
state | Integer | 升级:新订阅状态;降级:旧订阅状态(仍为 1 active) |
请求示例 — 升级(立即生效)#
curl --location --request POST 'https://web3.okx.com/api/v6/pay/x402/subscriptions/change' \
--header 'Content-Type: application/json' \
--header 'OK-ACCESS-KEY: 37c541a1-****-****-****-10fe7a038418' \
--header 'OK-ACCESS-SIGN: leaV********3uw=' \
--header 'OK-ACCESS-PASSPHRASE: 1****6' \
--header 'OK-ACCESS-TIMESTAMP: 2026-04-01T12:21:41.274Z' \
--data '{
"chainIndex": 196,
"oldSubId": "0x9a8b...c7d6",
"newTerms": {
"payer": "0x1111111111111111111111111111111111111111",
"merchant": "0x2222222222222222222222222222222222222222",
"facilitator": "0xFacilitatorEOA...",
"token": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
"amountPerPeriod": "20000000",
"periodSec": 0,
"maxPeriods": 12,
"startAt": 0,
"initialChargePeriods": 0,
"initialChargeAmount": "0",
"termsDeadline": 1781200000,
"permitHash": "0x<新 permit struct hash>",
"salt": "0x<新 random32bytes>",
"planId": "0x<keccak(enterprise_monthly)>",
"planTier": 3,
"changeFromSubId": "0x9a8b...c7d6",
"changeEffectiveAt": 1,
"periodMode": 1
},
"permit": {
"details": { "token": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8", "amount": "240000000", "expiration": 1812600000, "nonce": 6 },
"spender": "0xSubscriptionContract...",
"sigDeadline": "1781200600"
},
"termsSig": "0x<65字节 hex>",
"permitSig": "0x<65字节 hex>",
"syncSettle": true
}'
响应示例 — 升级#
{
"code": "0",
"msg": "",
"data": { "newSubId": "0x6b5c...upgradedSubId...a1f2", "txHash": "0xabc...upgrade...def", "state": 1 }
}
响应示例 — 降级(排程到周期末)#
请求体差异:newTerms.changeEffectiveAt=2、planTier 更低、startAt=0、initialChargeAmount="0"。
{
"code": "0",
"msg": "",
"data": { "newSubId": "0x2c1d...pendingNewSubId...8f0a", "txHash": "0xabc...schedule...def", "state": 1 }
}
降级响应的
state是旧订阅状态(仍1active);newSubId为待生效的新 subId,到下一次charge触发激活后才生效。
5. /api/v6/pay/x402/subscriptions/cancel#
/api/v6/pay/x402/subscriptions/cancel
取消订阅。必须携带 CancelAuth 链下签名(payer 或 merchant 任一方可发起),取消后停止后续扣款并释放预留额度。
请求参数#
| 参数 | 类型 | 必传 | 描述 |
|---|---|---|---|
subId | String | 是 | 订阅 ID(与 cancelAuth.subId 交叉校验) |
cancelAuth | Object | 是 | 取消授权,详见公共数据结构 CancelAuth |
syncSettle | Boolean | 否 | 见通用约定 syncSettle |
校验:订阅必须 active;cancelAuth.subId = body subId;deadline > now;签名恢复地址 = payer(initiator=0)或 merchant(initiator=1);initiator=1 时调用方 API Key 还必须是该订阅的创建商户。
响应参数#
| 参数 | 类型 | 描述 |
|---|---|---|
subId | String | 订阅 ID |
txHash | String | 预留字段,当前返回 null(取消交易可通过订阅详情 / 扣款流水侧观测) |
state | Integer | 订阅状态,成功后为 3 canceled |
请求示例#
curl --location --request POST 'https://web3.okx.com/api/v6/pay/x402/subscriptions/cancel' \
--header 'Content-Type: application/json' \
--header 'OK-ACCESS-KEY: 37c541a1-****-****-****-10fe7a038418' \
--header 'OK-ACCESS-SIGN: leaV********3uw=' \
--header 'OK-ACCESS-PASSPHRASE: 1****6' \
--header 'OK-ACCESS-TIMESTAMP: 2026-04-01T12:21:41.274Z' \
--data '{
"subId": "0x9a8b...c7d6",
"cancelAuth": {
"action": 0,
"initiator": 0,
"subId": "0x9a8b...c7d6",
"nonce": "0x<random32bytes>",
"deadline": 1781300000,
"signature": "0x<65字节 hex,payer 签>"
},
"syncSettle": true
}'
响应示例#
{
"code": "0",
"msg": "",
"data": { "subId": "0x9a8b...c7d6", "txHash": null, "state": 3 }
}
6. /api/v6/pay/x402/subscriptions/cancel-pending-change#
/api/v6/pay/x402/subscriptions/cancel-pending-change
取消一条待生效降级。仅 payer 可签署授权。
请求参数#
| 参数 | 类型 | 必传 | 描述 |
|---|---|---|---|
subId | String | 是 | 订阅 ID |
cancelAuth | Object | 是 | 见公共数据结构 PendingChangeCancelAuth,须含目标 newSubId |
syncSettle | Boolean | 否 | 见通用约定 syncSettle |
校验:存在状态为 pending 的降级排程(否则 no_pending_change_or_not_pending);cancelAuth.subId = 排程的 subId;cancelAuth.newSubId = 排程的 newSubId(否则 pending_cancel_target_mismatch);deadline > now;签名恢复地址 = 订阅 payer。
newSubId可从订阅详情接口响应的pendingPlanChange.newSubId获取。
响应参数#
| 参数 | 类型 | 描述 |
|---|---|---|
subId | String | 订阅 ID |
txHash | String | 待生效降级记录关联的交易哈希 |
state | Integer | 待生效变更状态(非订阅状态):成功后为 2 canceled |
请求示例#
curl --location --request POST 'https://web3.okx.com/api/v6/pay/x402/subscriptions/cancel-pending-change' \
--header 'Content-Type: application/json' \
--header 'OK-ACCESS-KEY: 37c541a1-****-****-****-10fe7a038418' \
--header 'OK-ACCESS-SIGN: leaV********3uw=' \
--header 'OK-ACCESS-PASSPHRASE: 1****6' \
--header 'OK-ACCESS-TIMESTAMP: 2026-04-01T12:21:41.274Z' \
--data '{
"subId": "0x9a8b...c7d6",
"cancelAuth": {
"subId": "0x9a8b...c7d6",
"newSubId": "0x2c1d...pendingNewSubId...8f0a",
"nonce": "0x<random32bytes>",
"deadline": 1781300000,
"signature": "0x<65字节 hex,仅 payer 签>"
},
"syncSettle": true
}'
响应示例#
{
"code": "0",
"msg": "",
"data": { "subId": "0x9a8b...c7d6", "txHash": "0xabc...schedule...def", "state": 2 }
}
7. /api/v6/pay/x402/subscriptions/finalize-expired#
/api/v6/pay/x402/subscriptions/finalize-expired
清理已过服务窗口但未终结的订阅,释放其在订阅合约中的预留额度,使 Buyer 可将额度用于新订阅。
请求参数#
| 参数 | 类型 | 必传 | 描述 |
|---|---|---|---|
subId | String | 是 | 订阅 ID |
校验:订阅 active 且服务窗口已结束(固定模式 now ≥ startAt + maxPeriods × periodSec;自然月 now ≥ addMonths(锚点, maxPeriods);否则 not_ended)。
响应参数#
| 参数 | 类型 | 描述 |
|---|---|---|
subId | String | 订阅 ID |
txHash | String | 预留字段,当前返回 null |
state | Integer | 预留字段,当前返回 null(终态可通过订阅详情查询,完成后为 2 completed) |
请求示例#
curl --location --request POST 'https://web3.okx.com/api/v6/pay/x402/subscriptions/finalize-expired' \
--header 'Content-Type: application/json' \
--header 'OK-ACCESS-KEY: 37c541a1-****-****-****-10fe7a038418' \
--header 'OK-ACCESS-SIGN: leaV********3uw=' \
--header 'OK-ACCESS-PASSPHRASE: 1****6' \
--header 'OK-ACCESS-TIMESTAMP: 2026-04-01T12:21:41.274Z' \
--data '{ "subId": "0x9a8b...c7d6" }'
响应示例#
{
"code": "0",
"msg": "",
"data": { "subId": "0x9a8b...c7d6", "txHash": null, "state": null }
}
8. /api/v6/pay/x402/subscriptions/detail#
/api/v6/pay/x402/subscriptions/detail
查询订阅详情(公开只读,无需 API Key)。Buyer 可直接调用,例如读取 pendingPlanChange.newSubId 用于签署取消降级授权。
请求参数#
| 参数 | 位置 | 类型 | 必传 | 描述 |
|---|---|---|---|---|
subId | query | String | 是 | 订阅 ID |
响应参数#
| 参数 | 类型 | 描述 |
|---|---|---|
subId / state / payer / merchant / token | 基础字段 | |
amountPerPeriod / periodSec / maxPeriods / startAt | 订阅条款(自然月 periodSec=0) | |
periodMode | Integer | 0 固定秒数 / 1 自然月 |
billingAnchorAt | Long | 自然月账单锚点(Unix 秒);0 = 待链上回填;固定模式忽略 |
lastChargedPeriod | Long | 已扣到的周期号 |
totalPulled | String | 累计拉款(原子单位) |
planId / planTier | String / Integer | 套餐标识 / 档位 |
changedToSubId | String | 升降级切换后的新 subId(无则 null) |
isActive | Boolean | state=1 且未过服务窗口 |
serviceEnded | Boolean | state=1 但已过服务窗口(未清理) |
currentPeriod | Long | 按时钟推导的当前周期号(封顶到 maxPeriods)。不要用它判断过期,过期看 serviceEnded / isActive |
elapsedPeriods | Long | 真实流逝周期号(不封顶,展示用);> maxPeriods 即服务窗口已结束 |
nextChargeableAt | Long | 下一个可扣款时间(Unix 秒);全部扣完则 null |
pendingPlanChange | Object | 内嵌的待生效降级(无则 null):subId / newSubId / effectiveFromPeriod / state |
请求示例#
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/subscriptions/detail?subId=0x9a8b...c7d6'
响应示例#
{
"code": "0",
"msg": "",
"data": {
"subId": "0x9a8b...c7d6",
"state": 1,
"payer": "0x1111111111111111111111111111111111111111",
"merchant": "0x2222222222222222222222222222222222222222",
"token": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
"amountPerPeriod": "20000000",
"periodSec": 0,
"periodMode": 1,
"maxPeriods": 12,
"startAt": 1781001000,
"billingAnchorAt": 1781001000,
"lastChargedPeriod": 3,
"totalPulled": "60000000",
"planId": "0x<keccak(pro_monthly)>",
"planTier": 2,
"changedToSubId": null,
"isActive": true,
"serviceEnded": false,
"currentPeriod": 4,
"elapsedPeriods": 4,
"nextChargeableAt": 1788777000,
"pendingPlanChange": {
"subId": "0x9a8b...c7d6",
"newSubId": "0x2c1d...pendingNewSubId...8f0a",
"effectiveFromPeriod": 5,
"state": 0
}
}
}
9. /api/v6/pay/x402/subscriptions/charges#
/api/v6/pay/x402/subscriptions/charges
查询订阅扣款流水(商户接口,需 API Key)。
请求参数#
| 参数 | 位置 | 类型 | 必传 | 默认 | 描述 |
|---|---|---|---|---|---|
subId | query | String | 是 | 订阅 ID | |
limit | query | Integer | 否 | 50 | 1 到 100,按创建时间倒序 |
offset | query | Integer | 否 | 0 | ≥ 0 |
响应参数#
charges 数组,每项:
| 参数 | 类型 | 描述 |
|---|---|---|
subId | String | 订阅 ID |
period | Long | 周期号 |
chargeType | Integer | 1 首期 / 2 周期扣款 / 3 降级后首期 / 4 过期清理标记 |
amount | String | 扣款金额(原子单位) |
state | Integer | 0 pending / 1 success / 2 failed |
txHash | String | 交易哈希 |
planChangeTriggered | Boolean | 该笔扣款是否触发了降级切换 |
newSubId | String | 触发降级时的新订阅 ID |
请求示例#
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/subscriptions/charges?subId=0x9a8b...c7d6&limit=50&offset=0' \
--header 'OK-ACCESS-KEY: 37c541a1-****-****-****-10fe7a038418' \
--header 'OK-ACCESS-SIGN: leaV********3uw=' \
--header 'OK-ACCESS-PASSPHRASE: 1****6' \
--header 'OK-ACCESS-TIMESTAMP: 2026-04-01T12:21:41.274Z'
响应示例#
{
"code": "0",
"msg": "",
"data": {
"charges": [
{ "subId": "0x9a8b...c7d6", "period": 3, "chargeType": 2, "amount": "20000000", "state": 1, "txHash": "0x...p3...", "planChangeTriggered": false, "newSubId": null },
{ "subId": "0x9a8b...c7d6", "period": 1, "chargeType": 1, "amount": "20000000", "state": 1, "txHash": "0x...init...", "planChangeTriggered": false, "newSubId": null }
]
}
}
10. /api/v6/pay/x402/subscriptions/pending#
/api/v6/pay/x402/subscriptions/pending
查询订阅最近一条待生效降级记录(任意状态,可观测 canceled / activated / expired 终态;商户接口,需 API Key)。无记录时各字段为 null。
请求参数#
| 参数 | 位置 | 类型 | 必传 | 描述 |
|---|---|---|---|---|
subId | query | String | 是 | 订阅 ID |
响应参数#
| 参数 | 类型 | 描述 |
|---|---|---|
subId | String | 订阅 ID |
newSubId | String | 降级目标的新订阅 ID |
effectiveFromPeriod | Long | 从第几周期开始生效 |
state | Integer | 0 pending / 1 activated / 2 canceled / 3 expired |
请求示例#
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/subscriptions/pending?subId=0x9a8b...c7d6' \
--header 'OK-ACCESS-KEY: 37c541a1-****-****-****-10fe7a038418' \
--header 'OK-ACCESS-SIGN: leaV********3uw=' \
--header 'OK-ACCESS-PASSPHRASE: 1****6' \
--header 'OK-ACCESS-TIMESTAMP: 2026-04-01T12:21:41.274Z'
响应示例 — 有排程#
{
"code": "0",
"msg": "",
"data": { "subId": "0x9a8b...c7d6", "newSubId": "0x2c1d...8f0a", "effectiveFromPeriod": 5, "state": 0 }
}
响应示例 — 无排程#
{
"code": "0",
"msg": "",
"data": { "subId": null, "newSubId": null, "effectiveFromPeriod": null, "state": null }
}
11. /api/v6/pay/x402/buyers/{buyer}/allowance-status#
/api/v6/pay/x402/buyers/{buyer}/allowance-status
查询 Buyer 的两层授权状态(公开只读,无需 API Key)。Buyer SDK 用它拼 PermitSingle 并判断是否需要先做 ERC-20 approve。结果不缓存(在途交易可能改变 nonce)。
请求参数#
| 参数 | 位置 | 类型 | 必传 | 描述 |
|---|---|---|---|---|
buyer | path | String | 是 | 付款人地址 |
token | query | String | 是 | ERC-20 代币地址 |
chainIndex | query | Long | 是 | 链索引 |
响应参数#
| 参数 | 类型 | 描述 |
|---|---|---|
permit2Allowance | String | 第 1 层:ERC20.allowance(buyer, Permit2)。不足则 Buyer 必须先执行 token.approve(permit2Contract, ...) |
approvedAmount | String | 第 2 层:Permit2 内授给订阅合约的额度 |
expiration | Long | 第 2 层额度的过期时间 |
nonce | Long | Permit2 当前 nonce,下一笔 permit 直接用此值签名 |
reservedAmount | String | 订阅合约中活跃订阅已占用的预留额度 |
reservedExpiration | Long | 预留额度的过期时间,新 permit expiration 的下限 |
tokenBalance | String | Buyer 代币余额(UX 提示用) |
availableAmount | String | 派生:max(approvedAmount - reservedAmount, 0),可供新订阅的余量 |
subscriptionContract | String | 订阅合约地址(PermitSingle.spender) |
permit2Contract | String | Permit2 合约地址(第 1 层 approve 的目标) |
请求示例#
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/buyers/0x1111111111111111111111111111111111111111/allowance-status?token=0x4ae46a509f6b1d9056937ba4500cb143933d2dc8&chainIndex=196'
响应示例#
{
"code": "0",
"msg": "",
"data": {
"approvedAmount": "100000000",
"expiration": 1812600000,
"nonce": 5,
"reservedAmount": "40000000",
"reservedExpiration": 1812600000,
"tokenBalance": "523000000",
"availableAmount": "60000000",
"permit2Allowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
"subscriptionContract": "0xSubscriptionContract...",
"permit2Contract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
}
}
12. /api/v6/pay/x402/buyers/{buyer}/subscriptions#
/api/v6/pay/x402/buyers/{buyer}/subscriptions
查询 Buyer 自己的订阅列表(公开只读,无需 API Key)。不返回商户身份信息(无 merchant / facilitator / subscriptionContract)。
请求参数#
| 参数 | 位置 | 类型 | 必传 | 默认 | 描述 |
|---|---|---|---|---|---|
buyer | path | String | 是 | 付款人地址 | |
limit | query | Integer | 否 | 50 | 1 到 100,按创建时间倒序 |
offset | query | Integer | 否 | 0 | ≥ 0 |
响应参数#
subscriptions 数组,每项:
| 参数 | 类型 | 描述 |
|---|---|---|
chainIndex | Long | 链索引 |
subId / state / payer / token | 基础字段 | |
amountPerPeriod / periodSec / maxPeriods / startAt | 订阅条款 | |
periodMode / billingAnchorAt | 周期模式 / 自然月锚点 | |
initialChargePeriods / initialChargeAmount | 首期参数 | |
lastChargedPeriod / totalPulled | 扣款进度 | |
planId / planTier / changedToSubId | 套餐信息 | |
isActive / serviceEnded / currentPeriod / elapsedPeriods / nextChargeableAt | 派生字段,语义同订阅详情接口 |
请求示例#
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/buyers/0x1111111111111111111111111111111111111111/subscriptions?limit=20&offset=0'
响应示例#
{
"code": "0",
"msg": "",
"data": {
"subscriptions": [
{
"chainIndex": 196,
"subId": "0x9a8b...c7d6",
"state": 1,
"payer": "0x1111111111111111111111111111111111111111",
"token": "0x4ae46a509f6b1d9056937ba4500cb143933d2dc8",
"amountPerPeriod": "20000000",
"periodSec": 0,
"periodMode": 1,
"maxPeriods": 12,
"startAt": 1781001000,
"billingAnchorAt": 1781001000,
"initialChargePeriods": 0,
"initialChargeAmount": "0",
"lastChargedPeriod": 3,
"totalPulled": "60000000",
"planId": "0x<keccak(pro_monthly)>",
"planTier": 2,
"changedToSubId": null,
"isActive": true,
"serviceEnded": false,
"currentPeriod": 4,
"elapsedPeriods": 4,
"nextChargeableAt": 1788777000
}
]
}
}
公共数据结构#
SubscriptionTerms#
Buyer 签名的订阅条款,共 17 个字段,全部进入 EIP-712 签名(planId 除外)。subId = terms 的 EIP-712 摘要。
| 字段 | 类型 | 链上类型 | 必传 | 描述 |
|---|---|---|---|---|
payer | String | address | 是 | 付款人(签名者) |
merchant | String | address | 是 | 收款商户(链上地址) |
facilitator | String | address | 是 | facilitator EOA,必须原样取自 /supported |
token | String | address | 是 | ERC-20 代币地址 |
amountPerPeriod | String | uint160 | 是 | 每期金额(原子单位) |
periodSec | Long | uint64 | 是 | 周期秒数;自然月模式必须 = 0 |
maxPeriods | Long | uint32 | 是 | 总周期数 |
startAt | Long | uint64 | 是 | 起始时间;0 = 合约取上链时刻;非 0 不得早于当前时间 |
initialChargePeriods | Long | uint32 | 是 | 首期覆盖的周期数(0 = 无独立首期) |
initialChargeAmount | String | uint160 | 是 | 首期金额(原子单位) |
termsDeadline | Long | uint64 | 是 | terms 签名有效期 |
permitHash | String | bytes32 | 是 | = PermitSingle 的 EIP-712 struct hash(绑定 permit) |
salt | String | bytes32 | 是 | Buyer 生成的防重放随机值 |
planId | String | bytes32 | 是 | 套餐 ID(业务标识,不进链上签名) |
planTier | Integer | uint8 | 是 | 套餐档位(> 0;升降级方向比较用) |
changeFromSubId | String | bytes32 | 是 | 创建 = 全 0;升降级 = 旧 subId |
changeEffectiveAt | Integer | uint8 | 是 | 0 none / 1 immediate / 2 period_end |
periodMode | Integer | uint8 | 是 | 0 固定秒数 / 1 自然月 |
PermitSingle#
Permit2 AllowanceTransfer 授权对象。
| 字段 | 类型 | 描述 |
|---|---|---|
details.token | String | 代币地址(须 = terms.token) |
details.amount | String | 授权额度(uint160 字符串),须覆盖订阅全额承诺 |
details.expiration | Long | 额度过期时间(uint48 秒),不得早于订阅服务窗口结束 |
details.nonce | Long | Permit2 nonce(uint48),取自 allowance-status 接口 |
spender | String | 必须 = 订阅合约地址 |
sigDeadline | String | permit 签名有效期(uint256 字符串) |
CancelAuth#
取消订阅授权(payer 或 merchant 签名)。
| 字段 | 类型 | 描述 |
|---|---|---|
action | Integer | 固定 0(cancel_subscription) |
initiator | Integer | 0 payer / 1 merchant |
subId | String | bytes32,目标订阅 ID |
nonce | String | bytes32,防重放 |
deadline | Long | Unix 秒 |
signature | String | EIP-712 签名(65 字节 hex) |
PendingChangeCancelAuth#
取消降级排程授权(仅 payer 签名)。
| 字段 | 类型 | 描述 |
|---|---|---|
subId | String | bytes32,订阅 ID |
newSubId | String | bytes32,待取消降级的目标新 subId(须 = 当前排程的 newSubId) |
nonce | String | bytes32,防重放 |
deadline | Long | Unix 秒 |
signature | String | EIP-712 签名(65 字节 hex) |
EIP-712 签名定义#
域(domain separator)#
| 用途 | name | version | verifyingContract |
|---|---|---|---|
| terms / cancelAuth / pendingChangeCancelAuth | A2APaySubscription | 1 | 订阅合约 |
| PermitSingle | Permit2 | (无 version) | Permit2 合约 |
最终摘要:keccak256(0x1901 ‖ domainSeparator ‖ structHash)。
TypeString#
SubscriptionTerms(address payer,address merchant,address facilitator,address token,uint160 amountPerPeriod,uint64 periodSec,uint32 maxPeriods,uint64 startAt,uint32 initialChargePeriods,uint160 initialChargeAmount,uint64 termsDeadline,bytes32 permitHash,bytes32 salt,uint8 planTier,bytes32 changeFromSubId,uint8 changeEffectiveAt,uint8 periodMode)
CancelAuth(uint8 action,bytes32 subId,uint8 initiator,bytes32 nonce,uint64 deadline)
PendingChangeCancelAuth(bytes32 subId,bytes32 newSubId,bytes32 nonce,uint64 deadline)
PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)签名要求:
-
全部签名为 65 字节 secp256k1(
r‖s‖v),强制 EIP-2 低 s 值(s ≤ N/2,否则拒绝signature_high_s); -
签名前可通过订阅合约的
hashSubscriptionTerms(terms)/hashPermitSingle(permit)view 函数 eth_call 比对本地摘要; -
Permit2 链上前置条件:Buyer 必须先对 Permit2 canonical 合约
0x000000000022d473030f116ddee9f6b43ac78ba3执行足额approve,否则创建被拒。
支持的网络和币种#
| 网络 | Chain Index | 状态 |
|---|---|---|
| X Layer | 196 | 已支持 |
X Layer 支持的稳定币:
| 币种 | 合约地址 |
|---|---|
| USDC | 0x74b7f16337b8972027f6196a17a631ac6de26d22 |
| USDG | 0x4ae46a509f6b1d9056937ba4500cb143933d2dc8 |
| USD₮0 | 0x779ded0c9e1022225f8e0630b35a9b54be713736 |
错误码#
错误响应统一使用包络 {"code": "<code>", "msg": "<message>", "data": null}。
1. 认证错误(HTTP 401)#
| 错误码 | 描述 |
|---|---|
| 50103 | 请求头 OK-ACCESS-KEY 不能为空 |
| 50104 | 请求头 OK-ACCESS-PASSPHRASE 不能为空 |
| 50105 | 请求头 OK-ACCESS-PASSPHRASE 错误 |
| 50106 | 请求头 OK-ACCESS-SIGN 不能为空 |
| 50107 | 请求头 OK-ACCESS-TIMESTAMP 不能为空 |
| 50111 | 无效的 OK-ACCESS-KEY |
| 50112 | 无效的 OK-ACCESS-TIMESTAMP |
| 50113 | 无效的签名 |
2. 请求错误#
| 错误码 | HTTP 状态 | 描述 |
|---|---|---|
| 50011 | 429 | 用户请求频率过快,超过该接口允许的限额 |
3. 业务错误#
订阅接口的业务错误统一返回 HTTP 200,code 为业务错误码,msg 携带机器可读的错误标识(部分附 : 加人类可读补充说明):
| 错误码 | 含义 |
|---|---|
| 30001 | 参数 / 业务校验失败,具体原因见 msg 错误标识(下表) |
| 8000 | 系统内部错误 |
| 10051 | 合规拦截(payer 或 merchant 命中风险地址) |
| -1 | 未分类系统错误,请稍后重试 |
4. msg 错误标识#
常见取值(按触发接口分组):
通用
| 标识 | 描述 |
|---|---|
unsupported_chain | chainIndex 不支持订阅 |
feature_disabled | 订阅功能灰度关停(仅拦创建 / 升降级,存量扣款不受影响) |
contract_not_configured | 该链未配置订阅合约 |
facilitator_not_registered | facilitator 地址无可用签名者 |
missing_required_terms_fields / invalid_address_format / invalid_bytes32 | 字段缺失 / 格式错误 |
unauthorized_caller | API Key 商户与订阅创建商户不一致 |
subscription_not_found / sub_not_found | 订阅不存在 |
system_error | 未分类异常 |
创建 / 升降级(条款与签名校验)
| 标识 | 描述 |
|---|---|
amount_per_period_invalid / period_sec_invalid / max_periods_invalid / plan_tier_invalid | 数值非法 |
period_mode_invalid | periodMode 不是 0/1 |
period_sec_not_allowed | 自然月模式 periodSec ≠ 0 |
start_at_in_past | 非 0 startAt 早于当前时间 |
initial_charge_mismatch / initial_charge_periods_exceeds_max / initial_charge_exceeds_limit | 首期参数越界(降级必须为 0;pre-start 预收费越界) |
token_mismatch | terms.token ≠ permit.details.token |
permit_spender_mismatch | permit.spender ≠ 订阅合约 |
permit_hash_mismatch | terms.permitHash ≠ 实际 permit struct hash |
allowance_insufficient / allowance_expired | permit 额度 / 有效期不足以覆盖承诺 |
terms_deadline_expired / permit_sig_deadline_expired | 签名已过期 |
terms_signature_invalid / terms_binding_invalid / permit_signature_invalid | 签名恢复地址 ≠ payer |
signature_high_s / signature_recovery_failed | 签名格式非法 |
salt_already_used / subscription_already_exists | 防重放拒绝 |
create_must_have_zero_changeFromSubId / create_must_have_none_changeEffectiveAt | 创建必须不带变更字段 |
升降级专属
| 标识 | 描述 |
|---|---|
change_must_have_nonzero_changeFromSubId / change_must_have_non_none_effectiveAt | 变更必须带变更字段 |
changeFromSubId_mismatch / payer_mismatch / merchant_mismatch / facilitator_mismatch / period_sec_mismatch / period_mode_mismatch | 新旧订阅不变量违反 |
tier_same / change_effective_at_mismatch | 档位与生效方式不匹配 |
start_at_mismatch | startAt 违反升降级规则 |
sub_not_active_for_change / pending_change_exists | 旧订阅非 active / 已有待生效降级 |
change_in_flight | 同订阅另一变更在途 |
扣款
| 标识 | 描述 |
|---|---|
subscription_not_active | 订阅非 active |
all_periods_charged | 全部周期已扣完 |
period_not_due | 当前周期未到期 |
charge_in_flight | 同订阅另一扣款在途 |
insufficient_allowance / insufficient_balance / permit_expired | 额度 / 余额 / 授权过期不足 |
取消 / 取消降级 / 清理
| 标识 | 描述 |
|---|---|
cancel_auth_required / cancel_subId_mismatch / cancel_deadline_expired / cancel_signature_invalid | CancelAuth 校验失败 |
no_pending_change_or_not_pending | 无待生效降级 |
pending_cancel_subId_mismatch / pending_cancel_target_mismatch / pending_cancel_deadline_expired / pending_cancel_signature_invalid | 取消降级授权校验失败 |
not_ended | 服务窗口未结束(finalize-expired) |
链上
| 标识 | 描述 |
|---|---|
on_chain_simulation_failed | 预执行模拟 revert |
on_chain_tx_failed | 链上交易失败 |
intent_submit_failed | 交易提交失败 |
- 订阅模型概览认证通用约定syncSettle(写接口通用)字段表示枚举码周期模式(periodMode)1. /api/v6/pay/x402/supported请求示例响应示例2. /api/v6/pay/x402/subscriptions请求参数响应参数请求示例(自然月月付)响应示例3. /api/v6/pay/x402/subscriptions/charge请求参数响应参数请求示例响应示例 — 普通扣款响应示例 — 本周期触发降级切换4. /api/v6/pay/x402/subscriptions/change请求参数响应参数请求示例 — 升级(立即生效)响应示例 — 升级响应示例 — 降级(排程到周期末)5. /api/v6/pay/x402/subscriptions/cancel请求参数响应参数请求示例响应示例6. /api/v6/pay/x402/subscriptions/cancel\-pending\-change请求参数响应参数请求示例响应示例7. /api/v6/pay/x402/subscriptions/finalize\-expired请求参数响应参数请求示例响应示例8. /api/v6/pay/x402/subscriptions/detail请求参数响应参数请求示例响应示例9. /api/v6/pay/x402/subscriptions/charges请求参数响应参数请求示例响应示例10. /api/v6/pay/x402/subscriptions/pending请求参数响应参数请求示例响应示例 — 有排程响应示例 — 无排程11. /api/v6/pay/x402/buyers/\{buyer\}/allowance\-status请求参数响应参数请求示例响应示例12. /api/v6/pay/x402/buyers/\{buyer\}/subscriptions请求参数响应参数请求示例响应示例公共数据结构SubscriptionTermsPermitSingleCancelAuthPendingChangeCancelAuthEIP\-712 签名定义域(domain separator)TypeString支持的网络和币种错误码1. 认证错误(HTTP 401)2. 请求错误3. 业务错误4. msg 错误标识
