开发者平台
主题

HTTP 端 API - 订阅支付#

订阅模型概览#

订阅支付基于链上订阅合约(Permit2 AllowanceTransfer 授权模式)实现"一次签名授权、按周期自动拉款":

  • Buyer(付款人) 链下对两个对象签名:SubscriptionTerms(订阅条款)+ Permit2 PermitSingle(额度授权),不需要为每期付款重复签名;

  • Seller(商户)后端 收集 Buyer 双签后调用 Broker 接口创建订阅,并在每个计费周期到期后发起扣款(Seller-Driven);

  • Broker(facilitator) 校验签名与条款后代为提交链上交易,terms.facilitator 必须等于 /supported 下发的 facilitator 地址。

维度说明
schemeperiod
资产转移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 鉴权:全部写接口(创建 / 扣款 / 升降级 / 取消 / 取消降级 / 清理过期)以及 chargespending 两个商户视角查询,请求头携带 OK-ACCESS-* 字段。API Key 对应的商户身份同时用作接口鉴权基准:创建订阅时落库,之后的扣款 / 升降级 / 商户侧取消必须由同一商户发起。

  • 公开只读/supportedsubscriptions/detailbuyers/{buyer}/* 无需 API Key(返回的均为链上可推导数据),Buyer 可直接调用;仍受限频约束。

Header必传描述
OK-ACCESS-KEYAPI Key
OK-ACCESS-SIGN请求签名
OK-ACCESS-PASSPHRASEAPI 密码短语
OK-ACCESS-TIMESTAMPISO 8601 时间戳
Content-TypePOST 请求需设为 application/json

所有响应统一使用业务包络:

JSON
{
  "code": "0",
  "msg": "",
  "data": { /* 业务字段 */ }
}

业务错误时 code 为非 "0"datanullmsg 携带机器可读的错误标识(如 period_not_due),错误码集中见文末错误码章节。

通用约定#

syncSettle(写接口通用)#

写接口 body 均支持 syncSettle 字段:

  • true:阻塞轮询链上终态或超时(默认 5000ms),返回时 data 反映落库后的最新状态;

  • false / 不传:提交后立即返回(状态多为 pending),后续用查询接口轮询。

字段表示#

  • 金额(amountPerPeriod / initialChargeAmount / amount):原子单位十进制字符串;

  • 时间(periodSec / startAt / termsDeadline / expiration / deadline):Unix 秒;

  • subId / salt / nonce / permitHash / changeFromSubId0x + 64 hex 的 bytes32;

  • 地址:0x + 40 hex,小写。

枚举码#

枚举取值
订阅状态 state0 pending / 1 active / 2 completed / 3 canceled / 4 changed / 99 failed(链下提交失败本地态)
扣款流水状态 charge.state0 pending / 1 success / 2 failed
扣款类型 chargeType1 首期 / 2 周期扣款 / 3 降级后首期 / 4 过期清理标记
待生效变更状态 pendingChange.state0 pending / 1 activated / 2 canceled / 3 expired
变更生效方式 changeEffectiveAt0 none(创建)/ 1 immediate(升级)/ 2 period_end(降级)
取消发起方 cancelAuth.initiator0 payer / 1 merchant
周期模式 periodMode0 fixed_seconds(固定秒数)/ 1 calendar_month(自然月)

周期模式(periodMode)#

模式periodSec 要求周期边界
0 固定秒数必须 > 0startAt + n × periodSec
1 自然月必须 = 0addMonths(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#

GET
/api/v6/pay/x402/supported

查询 Broker 支持的 scheme、network 及签名者列表(无需 API Key)。订阅接入前必须调用此接口,获取订阅合约地址与 facilitator 地址

订阅能力以 kinds 数组中 scheme="period" 的条目广播(按灰度开关与链上合约配置动态输出):

kinds[].extra 子字段描述
facilitatorAddressfacilitator EOA 地址。Buyer 必须原样填进 terms.facilitator —— 合约要求提交交易的地址与其一致
subscriptionContract订阅合约地址,Permit2 PermitSingle.spender 必须等于它
permit2ContractPermit2 canonical 合约地址,Buyer 第一层 ERC-20 approve 的目标

signers[network] 列出该链全部可用 facilitator EOA,供 Seller 校验某地址确属本 facilitator。

请求示例#

Bash
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/supported'

响应示例#

JSON
{
  "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#

POST
/api/v6/pay/x402/subscriptions

创建订阅。Buyer 双签(SubscriptionTerms + Permit2 PermitSingle)由 Seller 后端转发提交,合约创建订阅并按首期参数扣首期。

请求参数#

参数类型必传描述
chainIndexLong链索引,如 196
termsObject订阅条款,详见公共数据结构 SubscriptionTerms
permitObjectPermit2 授权,详见公共数据结构 PermitSingle
termsSigStringterms 的 EIP-712 签名(65 字节 hex),签名者 = terms.payer
permitSigStringpermit 的 EIP-712 签名(65 字节 hex),签名者 = terms.payer
syncSettleBoolean见通用约定 syncSettle

约束:

  • 创建时 terms.changeFromSubId 必须为全 0,terms.changeEffectiveAt 必须为 0

  • 首期规则initialChargePeriods > 0 时要求 initialChargeAmount ≤ initialChargePeriods × amountPerPeriodinitialChargePeriods = 0 且 initialChargeAmount > 0 为 pre-start 预收费(要求 ≤ amountPerPeriodstartAt > now);

  • permit 额度须覆盖订阅全额承诺,permit.details.expiration 不得早于订阅服务窗口结束;

  • 创建会产生资金转移,对 payermerchant 执行合规筛查,命中黑名单拒绝。

响应参数#

参数类型描述
subIdString订阅 ID(bytes32,= terms 的 EIP-712 摘要)
txHashString创建交易哈希
stateInteger订阅状态,见枚举码

请求示例(自然月月付)#

Bash
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=0periodSec 填正数(如月付 2592000)。

响应示例#

JSON
{
  "code": "0",
  "msg": "",
  "data": {
    "subId": "0x9a8b...termsDigest...c7d6",
    "txHash": "0xabc...create...def",
    "state": 1
  }
}

3. /api/v6/pay/x402/subscriptions/charge#

POST
/api/v6/pay/x402/subscriptions/charge

周期扣款。计费周期到期后由 Seller 后端发起,无需 Buyer 再次签名。仅创建该订阅的商户(API Key)可调用。

请求参数#

参数类型必传描述
subIdString订阅 ID
syncSettleBoolean见通用约定 syncSettle

校验链:订阅必须 active;未扣完全部周期(否则 all_periods_charged);当前周期已到期(否则 period_not_due);到期时对 payermerchant 执行合规筛查。

响应参数#

参数类型描述
subIdString原订阅 ID(回显请求)
periodLong本次扣款的周期号(= 当前周期;跳期不补扣)
txHashString扣款交易哈希
stateInteger扣款流水状态:0 pending / 1 success / 2 failed
planChangeTriggeredBoolean本周期是否触发了已排程的降级切换
newSubIdStringplanChangeTriggered=true 时为降级后新订阅 ID;否则 null

请求示例#

Bash
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 }'

响应示例 — 普通扣款#

JSON
{
  "code": "0",
  "msg": "",
  "data": {
    "subId": "0x9a8b...c7d6",
    "period": 4,
    "txHash": "0xabc...charge...def",
    "state": 1,
    "planChangeTriggered": false,
    "newSubId": null
  }
}

响应示例 — 本周期触发降级切换#

JSON
{
  "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#

POST
/api/v6/pay/x402/subscriptions/change

套餐升降级。升级(changeEffectiveAt=1)立即生效并扣新档首期;降级(changeEffectiveAt=2)排程到当前周期末,由下一次 charge 触发切换。仅创建该订阅的商户可调用。

请求参数#

参数类型必传描述
chainIndexLong链索引
oldSubIdString旧订阅 ID(信息字段;服务端以 newTerms.changeFromSubId 为准)
newTermsObject新条款:changeFromSubId 必须 = 旧 subId,见公共数据结构 SubscriptionTerms
permitObject新 Permit2 PermitSingle(覆盖新档全额承诺)
termsSig / permitSigStringEIP-712 签名,签名者 = newTerms.payer
syncSettleBoolean见通用约定 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(从交易时间重新建锚)。

响应参数#

参数类型描述
newSubIdString新订阅 ID(= newTerms 摘要)。升级即刻生效;降级为待生效的新 subId
txHashString交易哈希
stateInteger升级:新订阅状态;降级:旧订阅状态(仍为 1 active)

请求示例 — 升级(立即生效)#

Bash
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
}'

响应示例 — 升级#

JSON
{
  "code": "0",
  "msg": "",
  "data": { "newSubId": "0x6b5c...upgradedSubId...a1f2", "txHash": "0xabc...upgrade...def", "state": 1 }
}

响应示例 — 降级(排程到周期末)#

请求体差异:newTerms.changeEffectiveAt=2planTier 更低、startAt=0initialChargeAmount="0"

JSON
{
  "code": "0",
  "msg": "",
  "data": { "newSubId": "0x2c1d...pendingNewSubId...8f0a", "txHash": "0xabc...schedule...def", "state": 1 }
}

降级响应的 state 是旧订阅状态(仍 1 active);newSubId 为待生效的新 subId,到下一次 charge 触发激活后才生效。


5. /api/v6/pay/x402/subscriptions/cancel#

POST
/api/v6/pay/x402/subscriptions/cancel

取消订阅。必须携带 CancelAuth 链下签名(payer 或 merchant 任一方可发起),取消后停止后续扣款并释放预留额度。

请求参数#

参数类型必传描述
subIdString订阅 ID(与 cancelAuth.subId 交叉校验)
cancelAuthObject取消授权,详见公共数据结构 CancelAuth
syncSettleBoolean见通用约定 syncSettle

校验:订阅必须 activecancelAuth.subId = body subIddeadline > now;签名恢复地址 = payer(initiator=0)或 merchant(initiator=1);initiator=1 时调用方 API Key 还必须是该订阅的创建商户。

响应参数#

参数类型描述
subIdString订阅 ID
txHashString预留字段,当前返回 null(取消交易可通过订阅详情 / 扣款流水侧观测)
stateInteger订阅状态,成功后为 3 canceled

请求示例#

Bash
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
}'

响应示例#

JSON
{
  "code": "0",
  "msg": "",
  "data": { "subId": "0x9a8b...c7d6", "txHash": null, "state": 3 }
}

6. /api/v6/pay/x402/subscriptions/cancel-pending-change#

POST
/api/v6/pay/x402/subscriptions/cancel-pending-change

取消一条待生效降级。仅 payer 可签署授权。

请求参数#

参数类型必传描述
subIdString订阅 ID
cancelAuthObject见公共数据结构 PendingChangeCancelAuth,须含目标 newSubId
syncSettleBoolean见通用约定 syncSettle

校验:存在状态为 pending 的降级排程(否则 no_pending_change_or_not_pending);cancelAuth.subId = 排程的 subIdcancelAuth.newSubId = 排程的 newSubId(否则 pending_cancel_target_mismatch);deadline > now;签名恢复地址 = 订阅 payer。

newSubId 可从订阅详情接口响应的 pendingPlanChange.newSubId 获取。

响应参数#

参数类型描述
subIdString订阅 ID
txHashString待生效降级记录关联的交易哈希
stateInteger待生效变更状态(非订阅状态):成功后为 2 canceled

请求示例#

Bash
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
}'

响应示例#

JSON
{
  "code": "0",
  "msg": "",
  "data": { "subId": "0x9a8b...c7d6", "txHash": "0xabc...schedule...def", "state": 2 }
}

7. /api/v6/pay/x402/subscriptions/finalize-expired#

POST
/api/v6/pay/x402/subscriptions/finalize-expired

清理已过服务窗口但未终结的订阅,释放其在订阅合约中的预留额度,使 Buyer 可将额度用于新订阅。

请求参数#

参数类型必传描述
subIdString订阅 ID

校验:订阅 active 且服务窗口已结束(固定模式 now ≥ startAt + maxPeriods × periodSec;自然月 now ≥ addMonths(锚点, maxPeriods);否则 not_ended)。

响应参数#

参数类型描述
subIdString订阅 ID
txHashString预留字段,当前返回 null
stateInteger预留字段,当前返回 null(终态可通过订阅详情查询,完成后为 2 completed)

请求示例#

Bash
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" }'

响应示例#

JSON
{
  "code": "0",
  "msg": "",
  "data": { "subId": "0x9a8b...c7d6", "txHash": null, "state": null }
}

8. /api/v6/pay/x402/subscriptions/detail#

GET
/api/v6/pay/x402/subscriptions/detail

查询订阅详情(公开只读,无需 API Key)。Buyer 可直接调用,例如读取 pendingPlanChange.newSubId 用于签署取消降级授权。

请求参数#

参数位置类型必传描述
subIdqueryString订阅 ID

响应参数#

参数类型描述
subId / state / payer / merchant / token基础字段
amountPerPeriod / periodSec / maxPeriods / startAt订阅条款(自然月 periodSec=0
periodModeInteger0 固定秒数 / 1 自然月
billingAnchorAtLong自然月账单锚点(Unix 秒);0 = 待链上回填;固定模式忽略
lastChargedPeriodLong已扣到的周期号
totalPulledString累计拉款(原子单位)
planId / planTierString / Integer套餐标识 / 档位
changedToSubIdString升降级切换后的新 subId(无则 null
isActiveBooleanstate=1 且未过服务窗口
serviceEndedBooleanstate=1 但已过服务窗口(未清理)
currentPeriodLong按时钟推导的当前周期号(封顶到 maxPeriods)。不要用它判断过期,过期看 serviceEnded / isActive
elapsedPeriodsLong真实流逝周期号(不封顶,展示用);> maxPeriods 即服务窗口已结束
nextChargeableAtLong下一个可扣款时间(Unix 秒);全部扣完则 null
pendingPlanChangeObject内嵌的待生效降级(无则 null):subId / newSubId / effectiveFromPeriod / state

请求示例#

Bash
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/subscriptions/detail?subId=0x9a8b...c7d6'

响应示例#

JSON
{
  "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#

GET
/api/v6/pay/x402/subscriptions/charges

查询订阅扣款流水(商户接口,需 API Key)。

请求参数#

参数位置类型必传默认描述
subIdqueryString订阅 ID
limitqueryInteger501 到 100,按创建时间倒序
offsetqueryInteger0≥ 0

响应参数#

charges 数组,每项:

参数类型描述
subIdString订阅 ID
periodLong周期号
chargeTypeInteger1 首期 / 2 周期扣款 / 3 降级后首期 / 4 过期清理标记
amountString扣款金额(原子单位)
stateInteger0 pending / 1 success / 2 failed
txHashString交易哈希
planChangeTriggeredBoolean该笔扣款是否触发了降级切换
newSubIdString触发降级时的新订阅 ID

请求示例#

Bash
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'

响应示例#

JSON
{
  "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#

GET
/api/v6/pay/x402/subscriptions/pending

查询订阅最近一条待生效降级记录(任意状态,可观测 canceled / activated / expired 终态;商户接口,需 API Key)。无记录时各字段为 null

请求参数#

参数位置类型必传描述
subIdqueryString订阅 ID

响应参数#

参数类型描述
subIdString订阅 ID
newSubIdString降级目标的新订阅 ID
effectiveFromPeriodLong从第几周期开始生效
stateInteger0 pending / 1 activated / 2 canceled / 3 expired

请求示例#

Bash
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'

响应示例 — 有排程#

JSON
{
  "code": "0",
  "msg": "",
  "data": { "subId": "0x9a8b...c7d6", "newSubId": "0x2c1d...8f0a", "effectiveFromPeriod": 5, "state": 0 }
}

响应示例 — 无排程#

JSON
{
  "code": "0",
  "msg": "",
  "data": { "subId": null, "newSubId": null, "effectiveFromPeriod": null, "state": null }
}

11. /api/v6/pay/x402/buyers/{buyer}/allowance-status#

GET
/api/v6/pay/x402/buyers/{buyer}/allowance-status

查询 Buyer 的两层授权状态(公开只读,无需 API Key)。Buyer SDK 用它拼 PermitSingle 并判断是否需要先做 ERC-20 approve。结果不缓存(在途交易可能改变 nonce)。

请求参数#

参数位置类型必传描述
buyerpathString付款人地址
tokenqueryStringERC-20 代币地址
chainIndexqueryLong链索引

响应参数#

参数类型描述
permit2AllowanceString第 1 层ERC20.allowance(buyer, Permit2)。不足则 Buyer 必须先执行 token.approve(permit2Contract, ...)
approvedAmountString第 2 层:Permit2 内授给订阅合约的额度
expirationLong第 2 层额度的过期时间
nonceLongPermit2 当前 nonce,下一笔 permit 直接用此值签名
reservedAmountString订阅合约中活跃订阅已占用的预留额度
reservedExpirationLong预留额度的过期时间,新 permit expiration 的下限
tokenBalanceStringBuyer 代币余额(UX 提示用)
availableAmountString派生:max(approvedAmount - reservedAmount, 0),可供新订阅的余量
subscriptionContractString订阅合约地址(PermitSingle.spender
permit2ContractStringPermit2 合约地址(第 1 层 approve 的目标)

请求示例#

Bash
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/buyers/0x1111111111111111111111111111111111111111/allowance-status?token=0x4ae46a509f6b1d9056937ba4500cb143933d2dc8&chainIndex=196'

响应示例#

JSON
{
  "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#

GET
/api/v6/pay/x402/buyers/{buyer}/subscriptions

查询 Buyer 自己的订阅列表(公开只读,无需 API Key)。不返回商户身份信息(无 merchant / facilitator / subscriptionContract)。

请求参数#

参数位置类型必传默认描述
buyerpathString付款人地址
limitqueryInteger501 到 100,按创建时间倒序
offsetqueryInteger0≥ 0

响应参数#

subscriptions 数组,每项:

参数类型描述
chainIndexLong链索引
subId / state / payer / token基础字段
amountPerPeriod / periodSec / maxPeriods / startAt订阅条款
periodMode / billingAnchorAt周期模式 / 自然月锚点
initialChargePeriods / initialChargeAmount首期参数
lastChargedPeriod / totalPulled扣款进度
planId / planTier / changedToSubId套餐信息
isActive / serviceEnded / currentPeriod / elapsedPeriods / nextChargeableAt派生字段,语义同订阅详情接口

请求示例#

Bash
curl --location --request GET 'https://web3.okx.com/api/v6/pay/x402/buyers/0x1111111111111111111111111111111111111111/subscriptions?limit=20&offset=0'

响应示例#

JSON
{
  "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 摘要。

字段类型链上类型必传描述
payerStringaddress付款人(签名者)
merchantStringaddress收款商户(链上地址)
facilitatorStringaddressfacilitator EOA,必须原样取自 /supported
tokenStringaddressERC-20 代币地址
amountPerPeriodStringuint160每期金额(原子单位)
periodSecLonguint64周期秒数;自然月模式必须 = 0
maxPeriodsLonguint32总周期数
startAtLonguint64起始时间;0 = 合约取上链时刻;非 0 不得早于当前时间
initialChargePeriodsLonguint32首期覆盖的周期数(0 = 无独立首期)
initialChargeAmountStringuint160首期金额(原子单位)
termsDeadlineLonguint64terms 签名有效期
permitHashStringbytes32= PermitSingle 的 EIP-712 struct hash(绑定 permit)
saltStringbytes32Buyer 生成的防重放随机值
planIdStringbytes32套餐 ID(业务标识,不进链上签名)
planTierIntegeruint8套餐档位(> 0;升降级方向比较用)
changeFromSubIdStringbytes32创建 = 全 0;升降级 = 旧 subId
changeEffectiveAtIntegeruint80 none / 1 immediate / 2 period_end
periodModeIntegeruint80 固定秒数 / 1 自然月

PermitSingle#

Permit2 AllowanceTransfer 授权对象。

字段类型描述
details.tokenString代币地址(须 = terms.token
details.amountString授权额度(uint160 字符串),须覆盖订阅全额承诺
details.expirationLong额度过期时间(uint48 秒),不得早于订阅服务窗口结束
details.nonceLongPermit2 nonce(uint48),取自 allowance-status 接口
spenderString必须 = 订阅合约地址
sigDeadlineStringpermit 签名有效期(uint256 字符串)

CancelAuth#

取消订阅授权(payer 或 merchant 签名)。

字段类型描述
actionInteger固定 0(cancel_subscription)
initiatorInteger0 payer / 1 merchant
subIdStringbytes32,目标订阅 ID
nonceStringbytes32,防重放
deadlineLongUnix 秒
signatureStringEIP-712 签名(65 字节 hex)

PendingChangeCancelAuth#

取消降级排程授权(仅 payer 签名)。

字段类型描述
subIdStringbytes32,订阅 ID
newSubIdStringbytes32,待取消降级的目标新 subId(须 = 当前排程的 newSubId
nonceStringbytes32,防重放
deadlineLongUnix 秒
signatureStringEIP-712 签名(65 字节 hex)

EIP-712 签名定义#

域(domain separator)#

用途nameversionverifyingContract
terms / cancelAuth / pendingChangeCancelAuthA2APaySubscription1订阅合约
PermitSinglePermit2(无 version)Permit2 合约

最终摘要:keccak256(0x1901 ‖ domainSeparator ‖ structHash)

TypeString#

Plaintext
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 Layer196已支持

X Layer 支持的稳定币:

币种合约地址
USDC0x74b7f16337b8972027f6196a17a631ac6de26d22
USDG0x4ae46a509f6b1d9056937ba4500cb143933d2dc8
USD₮00x779ded0c9e1022225f8e0630b35a9b54be713736

错误码#

错误响应统一使用包络 {"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 状态描述
50011429用户请求频率过快,超过该接口允许的限额

3. 业务错误#

订阅接口的业务错误统一返回 HTTP 200,code 为业务错误码,msg 携带机器可读的错误标识(部分附 : 加人类可读补充说明):

错误码含义
30001参数 / 业务校验失败,具体原因见 msg 错误标识(下表)
8000系统内部错误
10051合规拦截(payer 或 merchant 命中风险地址)
-1未分类系统错误,请稍后重试

4. msg 错误标识#

常见取值(按触发接口分组):

通用

标识描述
unsupported_chainchainIndex 不支持订阅
feature_disabled订阅功能灰度关停(仅拦创建 / 升降级,存量扣款不受影响)
contract_not_configured该链未配置订阅合约
facilitator_not_registeredfacilitator 地址无可用签名者
missing_required_terms_fields / invalid_address_format / invalid_bytes32字段缺失 / 格式错误
unauthorized_callerAPI Key 商户与订阅创建商户不一致
subscription_not_found / sub_not_found订阅不存在
system_error未分类异常

创建 / 升降级(条款与签名校验)

标识描述
amount_per_period_invalid / period_sec_invalid / max_periods_invalid / plan_tier_invalid数值非法
period_mode_invalidperiodMode 不是 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_mismatchterms.tokenpermit.details.token
permit_spender_mismatchpermit.spender ≠ 订阅合约
permit_hash_mismatchterms.permitHash ≠ 实际 permit struct hash
allowance_insufficient / allowance_expiredpermit 额度 / 有效期不足以覆盖承诺
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_mismatchstartAt 违反升降级规则
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_invalidCancelAuth 校验失败
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交易提交失败
目录