Portale di sviluppo
Tema

HTTP API - Subscribtion Payment#

Subscription Model Overview#

Subscription payments are built on an on-chain subscription contract (Permit2 AllowanceTransfer authorization model), enabling "sign once, pull payment every billing period":

  • Buyer (payer) signs two objects off-chain: SubscriptionTerms (the subscription terms) + a Permit2 PermitSingle (allowance authorization). No further signature is needed for each period's payment;

  • Seller (merchant) backend collects the Buyer's two signatures, calls the Broker API to create the subscription, and initiates a charge after each billing period becomes due (Seller-Driven);

  • Broker (facilitator) verifies the signatures and terms, then submits the on-chain transaction on behalf of the parties. terms.facilitator must equal the facilitator address returned by /supported.

DimensionDescription
schemeperiod
Asset transferPermit2 AllowanceTransfer → subscription contract pulls funds per period
Token compatibilityAll ERC-20 tokens (requires a prior approve to the Permit2 canonical contract)
Billing periodFixed seconds (periodMode=0) or calendar month (periodMode=1)
Settlement timingAsynchronous by default; write endpoints support syncSettle=true to block until the on-chain terminal state
Amount semanticsEach period charges amountPerPeriod; skipped periods are never back-charged (only the current period is charged)
Plan managementUpgrades take effect immediately and charge the new tier's first period; downgrades are scheduled to take effect at period end
  • Base URL: https://web3.okx.com

  • Path prefix: /api/v6/pay/x402

  • Network: X Layer (chainId 196, CAIP-2 identifier eip155:196)

Authentication#

Endpoints fall into two authentication tiers:

  • API Key authentication: all write endpoints (create / charge / change / cancel / cancel-pending-change / finalize-expired) plus the two merchant-facing queries charges and pending. Requests carry the OK-ACCESS-* headers. The merchant identity behind the API Key is also the authorization baseline: it is persisted when the subscription is created, and subsequent charge / change / merchant-initiated cancel calls must come from the same merchant.

  • Public read-only: /supported, subscriptions/detail, and buyers/{buyer}/* require no API Key (everything they return is derivable from on-chain data) and can be called directly by the Buyer; rate limits still apply.

HeaderRequiredDescription
OK-ACCESS-KEYYesAPI Key
OK-ACCESS-SIGNYesRequest signature
OK-ACCESS-PASSPHRASEYesAPI passphrase
OK-ACCESS-TIMESTAMPYesISO 8601 timestamp
Content-TypeYesMust be application/json for POST requests

All responses use a unified business envelope:

JSON
{
  "code": "0",
  "msg": "",
  "data": { /* business fields */ }
}

On business errors, code is non-"0", data is null, and msg carries a machine-readable error identifier (e.g. period_not_due). See the Error Codes section at the end.

Common Conventions#

syncSettle (all write endpoints)#

Every write endpoint body supports the syncSettle field:

  • true: block and poll until the on-chain terminal state or a timeout (default 5000ms); the returned data reflects the latest persisted state;

  • false / omitted: return immediately after submission (state is usually pending); poll the query endpoints afterwards.

Field Representation#

  • Amounts (amountPerPeriod / initialChargeAmount / amount): decimal strings in atomic units;

  • Times (periodSec / startAt / termsDeadline / expiration / deadline): Unix seconds;

  • subId / salt / nonce / permitHash / changeFromSubId: bytes32 as 0x + 64 hex chars;

  • Addresses: 0x + 40 hex chars, lowercase.

Enums#

EnumValues
Subscription state state0 pending / 1 active / 2 completed / 3 canceled / 4 changed / 99 failed (local state for off-chain submission failure)
Charge record state charge.state0 pending / 1 success / 2 failed
Charge type chargeType1 initial / 2 periodic / 3 first period after downgrade / 4 expired-finalize marker
Pending change state pendingChange.state0 pending / 1 activated / 2 canceled / 3 expired
Change effectiveness changeEffectiveAt0 none (create) / 1 immediate (upgrade) / 2 period_end (downgrade)
Cancel initiator cancelAuth.initiator0 payer / 1 merchant
Period mode periodMode0 fixed_seconds / 1 calendar_month

Period Mode (periodMode)#

ModeperiodSec requirementPeriod boundary
0 fixed secondsMust be > 0startAt + n × periodSec
1 calendar monthMust be 0addMonths(billingAnchorAt, n) (clamped to month end, preserving time of day)

Key calendar-month semantics:

  • The anchor never drifts: every boundary is computed by adding n months to the original anchor billingAnchorAt, e.g. 1/31 12:00 → 2/28 → 3/31 → 4/30; it does not chain-drift onto a day-28 rhythm;

  • The exact boundary instant belongs to the next period;

  • billingAnchorAt: for a plain new subscription it equals startAt; upgrades / downgrade activation inherit the old subscription's anchor (the month-end rhythm continues); when startAt=0 it is backfilled from the chain;

  • All timestamps are UTC;

  • Skipped periods are never back-charged: missed intermediate periods release their reserved allowance; a charge only pulls the current period (identical semantics in both modes).


1. /api/v6/pay/x402/supported#

GET
/api/v6/pay/x402/supported

Queries the schemes, networks, and signer list supported by the Broker (no API Key required). Call this endpoint before subscription integration to obtain the subscription contract address and facilitator address.

Subscription capability is advertised as entries with scheme="period" in the kinds array (emitted dynamically based on rollout switches and per-chain contract configuration):

kinds[].extra subfieldDescription
facilitatorAddressFacilitator EOA address. The Buyer must copy it verbatim into terms.facilitator — the contract requires the transaction submitter to match it
subscriptionContractSubscription contract address; the Permit2 PermitSingle.spender must equal it
permit2ContractPermit2 canonical contract address; the target of the Buyer's first-layer ERC-20 approve

signers[network] lists all available facilitator EOAs on that chain, so Sellers can verify that a given address belongs to this facilitator.

Request Example#

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

Response Example#

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

Creates a subscription. The Buyer's two signatures (SubscriptionTerms + Permit2 PermitSingle) are forwarded and submitted by the Seller backend; the contract creates the subscription and charges the initial amount per the initial-charge parameters.

Request Parameters#

ParameterTypeRequiredDescription
chainIndexLongYesChain index, e.g. 196
termsObjectYesSubscription terms, see Common Data Structures: SubscriptionTerms
permitObjectYesPermit2 authorization, see Common Data Structures: PermitSingle
termsSigStringYesEIP-712 signature over terms (65-byte hex), signer = terms.payer
permitSigStringYesEIP-712 signature over permit (65-byte hex), signer = terms.payer
syncSettleBooleanNoSee Common Conventions: syncSettle

Constraints:

  • On create, terms.changeFromSubId must be all zeros and terms.changeEffectiveAt must be 0;

  • Initial-charge rules: when initialChargePeriods > 0, initialChargeAmount ≤ initialChargePeriods × amountPerPeriod is required; initialChargePeriods = 0 with initialChargeAmount > 0 is a pre-start upfront fee (must be ≤ amountPerPeriod and requires startAt > now);

  • The permit allowance must cover the subscription's full commitment, and permit.details.expiration must not be earlier than the end of the subscription's service window;

  • Creation moves funds, so compliance screening runs on both payer and merchant; blocklisted addresses are rejected.

Response Parameters#

ParameterTypeDescription
subIdStringSubscription ID (bytes32, = the EIP-712 digest of terms)
txHashStringCreation transaction hash
stateIntegerSubscription state, see Enums

Request Example (calendar-month billing)#

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-byte hex>",
  "permitSig": "0x<65-byte hex>",
  "syncSettle": true
}'

Fixed-seconds mode: set periodMode=0 and a positive periodSec (e.g. 2592000 for monthly).

Response Example#

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

Periodic charge. Initiated by the Seller backend once the billing period is due; no further Buyer signature is required. Only the merchant (API Key) that created the subscription may call it.

Request Parameters#

ParameterTypeRequiredDescription
subIdStringYesSubscription ID
syncSettleBooleanNoSee Common Conventions: syncSettle

Validation chain: the subscription must be active; not all periods charged yet (otherwise all_periods_charged); the current period must be due (otherwise period_not_due); when due, compliance screening runs on payer and merchant.

Response Parameters#

ParameterTypeDescription
subIdStringOriginal subscription ID (echoes the request)
periodLongPeriod number charged this time (= the current period; skipped periods are never back-charged)
txHashStringCharge transaction hash
stateIntegerCharge record state: 0 pending / 1 success / 2 failed
planChangeTriggeredBooleanWhether this period triggered a scheduled downgrade switch
newSubIdStringNew subscription ID after the downgrade when planChangeTriggered=true; otherwise null

Request Example#

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

Response Example — Regular Charge#

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

Response Example — Downgrade Switch Triggered This Period#

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

Plan upgrade / downgrade. An upgrade (changeEffectiveAt=1) takes effect immediately and charges the new tier's first period; a downgrade (changeEffectiveAt=2) is scheduled to the end of the current period and is switched by the next charge. Only the merchant that created the subscription may call it.

Request Parameters#

ParameterTypeRequiredDescription
chainIndexLongYesChain index
oldSubIdStringYesOld subscription ID (informational; the server relies on newTerms.changeFromSubId)
newTermsObjectYesNew terms: changeFromSubId must equal the old subId, see Common Data Structures: SubscriptionTerms
permitObjectYesNew Permit2 PermitSingle (covering the new tier's full commitment)
termsSig / permitSigStringYesEIP-712 signatures, signer = newTerms.payer
syncSettleBooleanNoSee Common Conventions: syncSettle

Direction rules and constraints:

  • newTerms.planTier > old planTier requires changeEffectiveAt=1 (upgrade); < requires changeEffectiveAt=2 (downgrade); equal tiers are rejected (tier_same);

  • The following must be identical across old and new subscriptions: payer / merchant / facilitator / token / periodSec / periodMode (the period mode cannot be switched);

  • The old subscription must be active with no scheduled-but-not-yet-effective downgrade (otherwise pending_change_exists);

  • For downgrades, newTerms.initialChargeAmount must be 0;

  • newTerms.startAt rules: if the old subscription has not started yet (pre-start), it must equal the old subscription's startAt; for downgrades and for in-effect upgrades in fixed mode it must be 0; for in-effect calendar-month upgrades it may equal the start of the old subscription's current period (aligned upgrade — inherits the old anchor; period 1 retroactively covers the old current period and is charged in full) or 0 (re-anchor from the transaction time).

Response Parameters#

ParameterTypeDescription
newSubIdStringNew subscription ID (= digest of newTerms). Effective immediately for upgrades; the pending new subId for downgrades
txHashStringTransaction hash
stateIntegerUpgrade: new subscription's state; downgrade: old subscription's state (still 1 active)

Request Example — Upgrade (immediate)#

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<new permit struct hash>",
    "salt": "0x<new 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-byte hex>",
  "permitSig": "0x<65-byte hex>",
  "syncSettle": true
}'

Response Example — Upgrade#

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

Response Example — Downgrade (scheduled to period end)#

Request-body differences: newTerms.changeEffectiveAt=2, a lower planTier, startAt=0, initialChargeAmount="0".

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

The downgrade response's state is the old subscription's state (still 1 active); newSubId is the pending new subId, which only takes effect after the next charge triggers activation.


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

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

Cancels a subscription. Requires an off-chain CancelAuth signature (either payer or merchant can initiate). After cancellation, future charges stop and the reserved allowance is released.

Request Parameters#

ParameterTypeRequiredDescription
subIdStringYesSubscription ID (cross-checked against cancelAuth.subId)
cancelAuthObjectYesCancellation authorization, see Common Data Structures: CancelAuth
syncSettleBooleanNoSee Common Conventions: syncSettle

Validation: the subscription must be active; cancelAuth.subId = body subId; deadline > now; the recovered signer = payer (initiator=0) or merchant (initiator=1); for initiator=1 the caller's API Key must also be the subscription's creating merchant.

Response Parameters#

ParameterTypeDescription
subIdStringSubscription ID
txHashStringReserved field, currently returns null (the cancel transaction can be observed via subscription detail / charge records)
stateIntegerSubscription state, 3 canceled on success

Request Example#

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-byte hex, signed by payer>"
  },
  "syncSettle": true
}'

Response Example#

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

Cancels a scheduled but not-yet-effective downgrade. Only the payer can sign the authorization.

Request Parameters#

ParameterTypeRequiredDescription
subIdStringYesSubscription ID
cancelAuthObjectYesSee Common Data Structures: PendingChangeCancelAuth; must include the target newSubId
syncSettleBooleanNoSee Common Conventions: syncSettle

Validation: a downgrade schedule in pending state must exist (otherwise no_pending_change_or_not_pending); cancelAuth.subId = the schedule's subId; cancelAuth.newSubId = the schedule's newSubId (otherwise pending_cancel_target_mismatch); deadline > now; the recovered signer = the subscription's payer.

newSubId can be obtained from the pendingPlanChange.newSubId field of the subscription detail response.

Response Parameters#

ParameterTypeDescription
subIdStringSubscription ID
txHashStringTransaction hash associated with the downgrade schedule record
stateIntegerPending-change state (not the subscription state): 2 canceled on success

Request Example#

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-byte hex, signed by payer only>"
  },
  "syncSettle": true
}'

Response Example#

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

Finalizes a subscription whose service window has ended but which was never terminated, releasing its reserved allowance in the subscription contract so the Buyer can use it for a new subscription.

Request Parameters#

ParameterTypeRequiredDescription
subIdStringYesSubscription ID

Validation: the subscription is active and its service window has ended (fixed mode: now ≥ startAt + maxPeriods × periodSec; calendar month: now ≥ addMonths(anchor, maxPeriods); otherwise not_ended).

Response Parameters#

ParameterTypeDescription
subIdStringSubscription ID
txHashStringReserved field, currently returns null
stateIntegerReserved field, currently returns null (query the subscription detail for the terminal state; 2 completed after finalization)

Request Example#

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

Response Example#

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

Queries subscription details (public read-only, no API Key). The Buyer can call it directly — for example to read pendingPlanChange.newSubId when signing a cancel-pending-change authorization.

Request Parameters#

ParameterLocationTypeRequiredDescription
subIdqueryStringYesSubscription ID

Response Parameters#

ParameterTypeDescription
subId / state / payer / merchant / tokenBase fields
amountPerPeriod / periodSec / maxPeriods / startAtSubscription terms (periodSec=0 in calendar-month mode)
periodModeInteger0 fixed seconds / 1 calendar month
billingAnchorAtLongCalendar-month billing anchor (Unix seconds); 0 = pending on-chain backfill; ignored in fixed mode
lastChargedPeriodLongLast period number charged
totalPulledStringCumulative amount pulled (atomic units)
planId / planTierString / IntegerPlan identifier / tier
changedToSubIdStringNew subId after an upgrade/downgrade switch (null if none)
isActiveBooleanstate=1 and the service window has not ended
serviceEndedBooleanstate=1 but the service window has ended (not finalized)
currentPeriodLongClock-derived current period number (clamped to maxPeriods). Do not use it to detect expiry — use serviceEnded / isActive
elapsedPeriodsLongActual elapsed period number (unclamped, for display); > maxPeriods means the service window has ended
nextChargeableAtLongNext chargeable time (Unix seconds); null when all periods have been charged
pendingPlanChangeObjectEmbedded pending downgrade (null if none): subId / newSubId / effectiveFromPeriod / state

Request Example#

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

Response Example#

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

Queries a subscription's charge records (merchant endpoint, API Key required).

Request Parameters#

ParameterLocationTypeRequiredDefaultDescription
subIdqueryStringYesSubscription ID
limitqueryIntegerNo501..100, ordered by creation time descending
offsetqueryIntegerNo0≥ 0

Response Parameters#

charges array, each item:

ParameterTypeDescription
subIdStringSubscription ID
periodLongPeriod number
chargeTypeInteger1 initial / 2 periodic / 3 first period after downgrade / 4 expired-finalize marker
amountStringCharge amount (atomic units)
stateInteger0 pending / 1 success / 2 failed
txHashStringTransaction hash
planChangeTriggeredBooleanWhether this charge triggered a downgrade switch
newSubIdStringNew subscription ID when a downgrade was triggered

Request Example#

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'

Response Example#

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

Queries the subscription's most recent downgrade schedule record (any state, so terminal states canceled / activated / expired are observable; merchant endpoint, API Key required). All fields are null when no record exists.

Request Parameters#

ParameterLocationTypeRequiredDescription
subIdqueryStringYesSubscription ID

Response Parameters#

ParameterTypeDescription
subIdStringSubscription ID
newSubIdStringNew subscription ID targeted by the downgrade
effectiveFromPeriodLongPeriod from which the change takes effect
stateInteger0 pending / 1 activated / 2 canceled / 3 expired

Request Example#

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'

Response Example — Schedule Exists#

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

Response Example — No Schedule#

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

Queries the Buyer's two-layer allowance status (public read-only, no API Key). The Buyer SDK uses it to build the PermitSingle and to decide whether an ERC-20 approve is needed first. Results are not cached (in-flight transactions may change the nonce).

Request Parameters#

ParameterLocationTypeRequiredDescription
buyerpathStringYesPayer address
tokenqueryStringYesERC-20 token address
chainIndexqueryLongYesChain index

Response Parameters#

ParameterTypeDescription
permit2AllowanceStringLayer 1: ERC20.allowance(buyer, Permit2). If insufficient, the Buyer must first call token.approve(permit2Contract, ...)
approvedAmountStringLayer 2: allowance granted inside Permit2 to the subscription contract
expirationLongExpiration of the layer-2 allowance
nonceLongCurrent Permit2 nonce; sign the next permit with this exact value
reservedAmountStringAllowance already reserved by active subscriptions in the subscription contract
reservedExpirationLongExpiration of the reserved amount; the lower bound for a new permit's expiration
tokenBalanceStringBuyer's token balance (for UX hints)
availableAmountStringDerived: max(approvedAmount - reservedAmount, 0), the headroom available for new subscriptions
subscriptionContractStringSubscription contract address (the PermitSingle.spender)
permit2ContractStringPermit2 contract address (the target of the layer-1 approve)

Request Example#

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

Response Example#

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

Queries the Buyer's own subscription list (public read-only, no API Key). No merchant identity information is returned (no merchant / facilitator / subscriptionContract).

Request Parameters#

ParameterLocationTypeRequiredDefaultDescription
buyerpathStringYesPayer address
limitqueryIntegerNo501..100, ordered by creation time descending
offsetqueryIntegerNo0≥ 0

Response Parameters#

subscriptions array, each item:

ParameterTypeDescription
chainIndexLongChain index
subId / state / payer / tokenBase fields
amountPerPeriod / periodSec / maxPeriods / startAtSubscription terms
periodMode / billingAnchorAtPeriod mode / calendar-month anchor
initialChargePeriods / initialChargeAmountInitial-charge parameters
lastChargedPeriod / totalPulledCharging progress
planId / planTier / changedToSubIdPlan information
isActive / serviceEnded / currentPeriod / elapsedPeriods / nextChargeableAtDerived fields, same semantics as the subscription detail endpoint

Request Example#

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

Response Example#

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
      }
    ]
  }
}

Common Data Structures#

SubscriptionTerms#

The subscription terms signed by the Buyer — 17 fields, all included in the EIP-712 signature (except planId). subId = the EIP-712 digest of the terms.

FieldTypeOn-chain typeRequiredDescription
payerStringaddressYesPayer (the signer)
merchantStringaddressYesReceiving merchant (on-chain address)
facilitatorStringaddressYesFacilitator EOA, must be copied verbatim from /supported
tokenStringaddressYesERC-20 token address
amountPerPeriodStringuint160YesAmount per period (atomic units)
periodSecLonguint64YesPeriod in seconds; must be 0 in calendar-month mode
maxPeriodsLonguint32YesTotal number of periods
startAtLonguint64YesStart time; 0 = the contract uses the on-chain timestamp; a non-zero value must not be earlier than now
initialChargePeriodsLonguint32YesNumber of periods covered by the initial charge (0 = no separate initial charge)
initialChargeAmountStringuint160YesInitial charge amount (atomic units)
termsDeadlineLonguint64YesTerms signature validity deadline
permitHashStringbytes32Yes= the EIP-712 struct hash of the PermitSingle (binds the permit)
saltStringbytes32YesRandom anti-replay value generated by the Buyer
planIdStringbytes32YesPlan ID (business identifier, not part of the on-chain signature)
planTierIntegeruint8YesPlan tier (> 0; used to compare upgrade/downgrade direction)
changeFromSubIdStringbytes32YesCreate = all zeros; upgrade/downgrade = the old subId
changeEffectiveAtIntegeruint8Yes0 none / 1 immediate / 2 period_end
periodModeIntegeruint8Yes0 fixed seconds / 1 calendar month

PermitSingle#

Permit2 AllowanceTransfer authorization object.

FieldTypeDescription
details.tokenStringToken address (must equal terms.token)
details.amountStringAllowance amount (uint160 string); must cover the subscription's full commitment
details.expirationLongAllowance expiration (uint48 seconds); must not be earlier than the end of the subscription's service window
details.nonceLongPermit2 nonce (uint48), obtained from the allowance-status endpoint
spenderStringMust equal the subscription contract address
sigDeadlineStringPermit signature validity deadline (uint256 string)

CancelAuth#

Subscription cancellation authorization (signed by payer or merchant).

FieldTypeDescription
actionIntegerFixed 0 (cancel_subscription)
initiatorInteger0 payer / 1 merchant
subIdStringbytes32, the target subscription ID
nonceStringbytes32, anti-replay
deadlineLongUnix seconds
signatureStringEIP-712 signature (65-byte hex)

PendingChangeCancelAuth#

Authorization to cancel a scheduled downgrade (signed by the payer only).

FieldTypeDescription
subIdStringbytes32, subscription ID
newSubIdStringbytes32, the target new subId of the downgrade to cancel (must equal the current schedule's newSubId)
nonceStringbytes32, anti-replay
deadlineLongUnix seconds
signatureStringEIP-712 signature (65-byte hex)

EIP-712 Signature Definitions#

Domain (domain separator)#

PurposenameversionverifyingContract
terms / cancelAuth / pendingChangeCancelAuthA2APaySubscription1Subscription contract
PermitSinglePermit2(no version)Permit2 contract

Final digest: 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)

Signature requirements:

  • All signatures are 65-byte secp256k1 (r‖s‖v) with EIP-2 low-s enforced (s ≤ N/2, otherwise rejected with signature_high_s);

  • Before signing, you can eth_call the subscription contract's hashSubscriptionTerms(terms) / hashPermitSingle(permit) view functions to cross-check your locally computed digests;

  • Permit2 on-chain prerequisite: the Buyer must first grant a sufficient approve to the Permit2 canonical contract 0x000000000022d473030f116ddee9f6b43ac78ba3, otherwise creation is rejected.

Supported Networks and Tokens#

NetworkChain IndexStatus
X Layer196Supported

Stablecoins supported on X Layer:

TokenContract address
USDC0x74b7f16337b8972027f6196a17a631ac6de26d22
USDG0x4ae46a509f6b1d9056937ba4500cb143933d2dc8
USD₮00x779ded0c9e1022225f8e0630b35a9b54be713736

Error Codes#

Error responses use the unified envelope {"code": "<code>", "msg": "<message>", "data": null}.

1. Authentication Errors (HTTP 401)#

CodeDescription
50103Request header OK-ACCESS-KEY cannot be empty
50104Request header OK-ACCESS-PASSPHRASE cannot be empty
50105Request header OK-ACCESS-PASSPHRASE incorrect
50106Request header OK-ACCESS-SIGN cannot be empty
50107Request header OK-ACCESS-TIMESTAMP cannot be empty
50111Invalid OK-ACCESS-KEY
50112Invalid OK-ACCESS-TIMESTAMP
50113Invalid signature

2. Request Errors#

CodeHTTP statusDescription
50011429Requests too frequent; the endpoint's rate limit was exceeded

3. Business Errors#

Business errors on subscription endpoints return HTTP 200, with code set to the business error code and msg carrying a machine-readable error identifier (some append : plus a human-readable note):

CodeMeaning
30001Parameter / business validation failure; see the msg error identifier (table below)
8000Internal system error
10051Compliance block (payer or merchant matched a risk address)
-1Uncategorized system error, please retry later

4. msg Error Identifiers#

Common values (grouped by triggering endpoint):

General

IdentifierDescription
unsupported_chainchainIndex does not support subscriptions
feature_disabledSubscription feature disabled by rollout switch (blocks create / change only; charges on existing subscriptions are unaffected)
contract_not_configuredNo subscription contract configured for the chain
facilitator_not_registeredNo available signer for the facilitator address
missing_required_terms_fields / invalid_address_format / invalid_bytes32Missing fields / format errors
unauthorized_callerAPI Key merchant does not match the subscription's creating merchant
subscription_not_found / sub_not_foundSubscription does not exist
system_errorUncategorized exception

Create / Change (terms and signature validation)

IdentifierDescription
amount_per_period_invalid / period_sec_invalid / max_periods_invalid / plan_tier_invalidInvalid numeric value
period_mode_invalidperiodMode is not 0/1
period_sec_not_allowedperiodSec ≠ 0 in calendar-month mode
start_at_in_pastNon-zero startAt earlier than now
initial_charge_mismatch / initial_charge_periods_exceeds_max / initial_charge_exceeds_limitInitial-charge parameters out of bounds (must be 0 for downgrades; pre-start upfront fee out of bounds)
token_mismatchterms.tokenpermit.details.token
permit_spender_mismatchpermit.spender ≠ subscription contract
permit_hash_mismatchterms.permitHash ≠ actual permit struct hash
allowance_insufficient / allowance_expiredPermit allowance / validity insufficient to cover the commitment
terms_deadline_expired / permit_sig_deadline_expiredSignature expired
terms_signature_invalid / terms_binding_invalid / permit_signature_invalidRecovered signer ≠ payer
signature_high_s / signature_recovery_failedMalformed signature
salt_already_used / subscription_already_existsAnti-replay rejection
create_must_have_zero_changeFromSubId / create_must_have_none_changeEffectiveAtCreate must not carry change fields

Change-specific

IdentifierDescription
change_must_have_nonzero_changeFromSubId / change_must_have_non_none_effectiveAtChange must carry change fields
changeFromSubId_mismatch / payer_mismatch / merchant_mismatch / facilitator_mismatch / period_sec_mismatch / period_mode_mismatchOld/new subscription invariant violated
tier_same / change_effective_at_mismatchTier and effectiveness direction do not match
start_at_mismatchstartAt violates the change rules
sub_not_active_for_change / pending_change_existsOld subscription not active / a pending downgrade already exists
change_in_flightAnother change for the same subscription is in flight

Charge

IdentifierDescription
subscription_not_activeSubscription not active
all_periods_chargedAll periods already charged
period_not_dueCurrent period not yet due
charge_in_flightAnother charge for the same subscription is in flight
insufficient_allowance / insufficient_balance / permit_expiredInsufficient allowance / balance / expired permit

Cancel / Cancel-pending-change / Finalize

IdentifierDescription
cancel_auth_required / cancel_subId_mismatch / cancel_deadline_expired / cancel_signature_invalidCancelAuth validation failure
no_pending_change_or_not_pendingNo pending downgrade
pending_cancel_subId_mismatch / pending_cancel_target_mismatch / pending_cancel_deadline_expired / pending_cancel_signature_invalidCancel-pending-change authorization validation failure
not_endedService window not ended yet (finalize-expired)

On-chain

IdentifierDescription
on_chain_simulation_failedPre-execution simulation reverted
on_chain_tx_failedOn-chain transaction failed
intent_submit_failedTransaction submission failed
Table of contents