Портал разработчиков
Тема

Subscription#

Subscription payment lets a Buyer authorize once, after which the Seller's backend actively triggers each period's charge — no re-signing or top-up per period. Funds always stay in the Buyer's own wallet; how much is charged each period, how often, and to whom are all fixed when the Buyer signs — the Seller can't change them, and can't overcharge.

This page is for HTTP Sellers (Subscription doesn't yet support Agent Sellers). For the definition, mechanism, state machine, and upgrade/downgrade semantics, see Core Concepts · Subscription; this page focuses on integration.

When it fits#

Your businessFits?
Recurring, fixed-amount billing (monthly / quarterly / yearly)
Memberships / content subscriptions / API plans
Needs tiers (e.g. Basic / Pro) with upgrade & downgrade

Prerequisites#

  • API Key: Apply for an API Key on the OKX Developer Portal first (the API Key / Secret Key / Passphrase trio — request headers OK-ACCESS-*, and in code OKX_API_KEY / OKX_SECRET_KEY / OKX_PASSPHRASE).
  • Token: You may use any standard ERC-20 token as the payment currency; native coins are not supported (e.g. OKB).

Business flow#

The Buyer double-signs once (subscription terms + Permit2 authorization) to create the subscription; after that you trigger each period's charge, and the contract transfers the signed, locked amount directly to the payee.

Seller integration#

Integration comes in two flavors, chosen by whether you let users switch plans:

  • Basic integration — fixed plan, users don't switch after subscribing; you define plans, receive payment, and auto-renew.
  • Advanced: support plan changes — on top of basic, let users upgrade or downgrade within the subscription period.

Basic integration (fixed plan)#

Set up a subscription#

  1. 1
    Define your plans

    Pick a billing mode first, then configure each tier's price, periods, and promotions.

    bash
    pnpm add @okxweb3/app-x402-core @okxweb3/app-x402-evm @okxweb3/app-x402-express express viem
    pnpm add -D tsx typescript @types/express @types/node
    
    ts
    import type { PlanCatalogEntry } from "@okxweb3/app-x402-core/subscription";
    
    const payTo = process.env.PAY_TO_ADDRESS!;   // recipient address (EIP-55)
    
    // Amounts in token base units (USDT/USDC 6 decimals; "5000" = $0.005).
    export const basic: PlanCatalogEntry = {
      id: "basic_monthly",         // business plan id (checked on access)
      tier: 1,                     // higher tier = higher plan (drives up/downgrade)
      payTo,                       // recipient address
      amountPerPeriod: "5000",     // charged each period
      periodSec: 2_592_000,        // week=604800 / month=2592000 / year=31536000
      periodMode: 0,               // 0 = fixed interval, 1 = calendar month
      maxPeriods: 12,              // allowance cap = maxPeriods × amountPerPeriod
      initialCharge: {             // first-charge policy (promotions)
        periodCount: 1,            // covers this many leading periods
        totalAmount: "5000",       // total (<= periodCount × amountPerPeriod)
      },
      name: "Basic Monthly",       // display metadata
      // asset: token address override; omit → SDK picks the network's default stablecoin.
    };
    
    // Pro: higher tier + amount, same shape as `basic`.
    export const pro: PlanCatalogEntry = {
      id: "pro_monthly",
      tier: 2,
      payTo,
      amountPerPeriod: "20000",
      periodSec: 2_592_000,
      periodMode: 0,
      maxPeriods: 12,
      initialCharge: { periodCount: 1, totalAmount: "20000" },
      name: "Pro Monthly",
    };
    
    // The x402 route wants a full `PaymentRequirements` accept, not the raw plan;
    // wrap once so every route can reuse the same mapping.
    export const NETWORK = (process.env.CHAIN_NETWORK ?? "eip155:196") as `eip155:${string}`;
    export function toAccept(plan: PlanCatalogEntry) {
      return {
        scheme: "period",
        network: NETWORK,
        payTo: plan.payTo,
        asset: plan.asset,                                // undefined → SDK default stablecoin
        price: { amount: plan.amountPerPeriod, asset: plan.asset },
        maxTimeoutSeconds: 600,                           // 402 challenge validity
        extra: {
          amountPerPeriod: plan.amountPerPeriod,
          periodMode: plan.periodMode ?? 0,
          periodSec: plan.periodSec,
          maxPeriods: plan.maxPeriods,
          initialCharge: plan.initialCharge,
          plan: { id: plan.id, tier: plan.tier, name: plan.name },
        },
      };
    }
    

    Billing modes

    By subscription date (same day each month, rolls forward at month-end)By fixed interval (every fixed span, e.g. every 30 days)
    User experienceSubscribe 3/15 → charge first period that day, then 4/15, 5/15…Subscribe 3/15, 30-day period → charge first period that day, then 4/14, 5/14… (the date drifts)
    periodModeCalendar monthFixed interval
    periodSec0Custom seconds
    startAt0 (start now)0 (start now)
    billingAnchorAt= subscribe dateN/A
    • Fixed-interval spans: week = 604800, month (30 days) = 2592000, year (365 days) = 31536000; to align to calendar dates, use calendar month.
    • Month-end rule: charge on the subscribe-date's day-of-month each month; if a month lacks that day (e.g. February has no 31st), charge the last day of that month — 1/31 → 2/28 → 3/31.

    Plan parameters — each tier's price and periods (example: two monthly tiers, billed by subscription date)

    PlanplanIdplanTieramountPerPeriodmaxPeriods
    Basicbasic_m110 USDC12
    Propro_m230 USDC12
    • Amounts are passed in the token's smallest unit (USDC has 6 decimals, 10 USDC = 10000000).
    • planTier: the higher the value, the higher the tier; upgrade/downgrade direction is judged by it. Even if you're not offering plan switching now, set it to the real tier from the start.

    Promotions — all done via the first-period params initialChargePeriods / initialChargeAmount, independent of which billing mode you pick. With amountPerPeriod=$30 and maxPeriods=12:

    StrategyinitialChargePeriodsinitialChargeAmountEffect
    Standard monthly00Charge $30 each period
    First N periods free trial30Periods 1–3 free, $30 from period 4
    First-period discount / coupon1Discounted amount (e.g. $15 for half off)First period at the discounted price; the chain doesn't recognize coupon codes — your backend computes the amount
    First N periods promo prepay3$30Prepay $30 total for the first 3 periods
    Annual discount (pay 10, get 12)12$300Charge $300 once, completes immediately
    Credit / one period free1Amount after credit (0 for a free period)Fold the credit into the first period: next period $30 minus $20 → enter $10
    • Constraint: initialChargeAmount ≤ initialChargePeriods × amountPerPeriod (no markup allowed).
    • Credits can only be set when the user signs (subscribe, plan change); once the subscription starts, each period charges the fixed amount — you can't grant an ad-hoc discount for a single period.
  2. 2
    Serve and gate the paid endpoint

    Expose an endpoint for the paid resource: an unsubscribed Buyer gets 402 + optional plans; a subscribed one gets the content.

    Whether to admit is a two-step check:

    ① Verify identity (pick one)

    • Wallet credential: the Buyer signs a credential (accessProof) with their wallet; you verify it to obtain their wallet address.
    • Your account system: bind the subId to a user in your system at subscribe time, then keep identifying via API Key / login session as usual (recommended if you already have an account system).

    ② Check permission: this user's subscription is active AND their plan (planId) is allowed to access this endpoint → admit; otherwise return 402. (Within one system, an endpoint can be opened to only some tiers — e.g. a premium endpoint only for Pro.)

    After subscribing, every service request the Buyer makes goes through this:

    ts
    import express from "express";
    import { OKXFacilitatorClient } from "@okxweb3/app-x402-core";
    import { x402HTTPResourceServer, x402ResourceServer } from "@okxweb3/app-x402-core/server";
    import {
      InMemoryStore,
      SubscriptionClient,
      type OnBeforeAccessHook,
    } from "@okxweb3/app-x402-core/subscription";
    import { PermitSubscriptionScheme } from "@okxweb3/app-x402-evm/subscription";
    import { paymentMiddlewareFromHTTPServer } from "@okxweb3/app-x402-express";
    
    import { basic, pro, toAccept, NETWORK } from "./plans";
    
    function requireEnv(k: string): string {
      const v = process.env[k];
      if (!v) throw new Error(`Missing env: ${k}`);
      return v;
    }
    
    // ── facilitator + scheme + store: same wiring every route reuses ─────
    const facilitator = new OKXFacilitatorClient({
      apiKey:     requireEnv("OKX_API_KEY"),
      secretKey:  requireEnv("OKX_SECRET_KEY"),
      passphrase: requireEnv("OKX_PASSPHRASE"),
      baseUrl:    process.env.OKX_BASE_URL,   // omit → OKX production
    });
    
    const store  = new InMemoryStore();       // swap for a persistent store in prod
    const scheme = new PermitSubscriptionScheme({
      facilitator,
      network: NETWORK,
      store,                                  // shared with SubscriptionClient below
    });
    const client = new SubscriptionClient({ scheme, store });   // used for charge / cancelBySeller / syncFromChain
    
    // register(network, scheme) then initialize() → fetches /supported and caches
    // the (facilitatorAddress, subscriptionContract, permit2Contract) triple.
    const server = new x402ResourceServer(facilitator).register(NETWORK, scheme);
    await server.initialize();
    
    // ── seller-global onBeforeAccess (matches Rust `SubscriptionSupport::on_before_access`) ──
    // Fires AFTER `verifyAccess` (signature + payer + plan-allowlist + period math)
    // succeed, BEFORE the route handler runs. Called on every access-verified
    // request. Return `{ ok: false, error }` to deny → 402.
    //
    // Context fields on `ctx`:
    //   ctx.subscription       — full Subscription: subId / payer / merchant /
    //                            planId / planTier / amountPerPeriod / periodSec /
    //                            periodMode / maxPeriods / state / lastChargedPeriod /
    //                            elapsedPeriods / nextChargeableAt / pendingPlanChange / …
    //   ctx.request.path       — request pathname, e.g. "/premium"
    //   ctx.request.method     — real HTTP method (not hard-coded)
    //   ctx.request.headers    — lowercase-keyed request headers
    //   ctx.route.acceptedPlanIds — plan ids listed in this route's `accepts`
    //   ctx.route.accepts      — full `PaymentRequirements[]`: each entry carries
    //                            plan metadata in `extra.plan` = { id, tier, name }
    //                            plus `extra.amountPerPeriod` / `extra.periodSec` /
    //                            `extra.periodMode` / `extra.maxPeriods`. Read these
    //                            when policy depends on catalog details (upgrade
    //                            offers, tier ceilings, per-plan feature flags) —
    //                            no separate catalog table needed on the seller.
    //
    // Multiple hooks stack: call `.onBeforeAccess(hook)` several times and they
    // run in registration order; the first `{ ok:false }` denies.
    // `RouteConfig.onBeforeAccess` (per-route) runs AFTER all global hooks.
    const denied = new Set<string>();
    const quotaByPayer = new Map<string, number>();   // per-day counter, keyed by payer
    const DAILY_QUOTA = 10_000;
    
    const banGuard: OnBeforeAccessHook = async (ctx) => {
      if (denied.has(ctx.subscription.subId)) {
        return { ok: false, error: "access_denied_by_merchant" };
      }
      return { ok: true };
    };
    
    const quotaGuard: OnBeforeAccessHook = async (ctx) => {
      const key = ctx.subscription.payer;
      const used = (quotaByPayer.get(key) ?? 0) + 1;
      quotaByPayer.set(key, used);
      if (used > DAILY_QUOTA) {
        return { ok: false, error: "quota_exhausted", retryAfter: 86_400 };
      }
      return { ok: true };
    };
    
    const headerGuard: OnBeforeAccessHook = async (ctx) => {
      // Region gate driven by CDN header — arbitrary logic keyed on ctx.request.
      if (ctx.request.headers["x-region"] === "restricted") {
        return { ok: false, error: "region_blocked" };
      }
      return { ok: true };
    };
    
    // Example: use ctx.route.accepts (full plan metadata) to log an upgrade hint
    // when the buyer's current plan doesn't reach the route's highest tier.
    const upgradeHint: OnBeforeAccessHook = async (ctx) => {
      const currentTier = ctx.subscription.planTier;
      const acceptTiers = (ctx.route.accepts ?? [])
        .map((r) => (r.extra?.plan as { tier?: number } | undefined)?.tier ?? 0);
      const maxAcceptTier = Math.max(0, ...acceptTiers);
      if (currentTier < maxAcceptTier) {
        // Not a deny — just log / metric. Business logic still allows.
        console.log(`sub ${ctx.subscription.subId}: on tier ${currentTier}, route accepts up to ${maxAcceptTier}`);
      }
      return { ok: true };
    };
    
    // routes.accepts = plan gating: only a sub whose planId matches one of the
    // listed accepts is admitted; otherwise 402.
    const routes = {
      "GET /weather": {
        accepts: [toAccept(basic)],              // Basic only
        description: "Weather data (Basic plan)",
        mimeType: "application/json",
      },
      "GET /premium": {
        accepts: [toAccept(pro)],                // Pro only; a Basic sub → 402
        description: "Premium analytics (Pro plan only)",
        mimeType: "application/json",
      },
    };
    
    // Builder-style wiring: attach seller-global onBeforeAccess hooks on the
    // HTTPResourceServer, then hand it to the express factory.
    const httpServer = new x402HTTPResourceServer(server, routes)
      .onBeforeAccess(banGuard)
      .onBeforeAccess(quotaGuard)
      .onBeforeAccess(headerGuard)
      .onBeforeAccess(upgradeHint);
    
    const app = express();
    app.use(express.json());
    app.use(paymentMiddlewareFromHTTPServer(httpServer));
    
    app.get("/weather", (req, res) => {
      // req.x402.subscription is set once the middleware completes access verification.
      res.json({ report: { weather: "sunny", temperature: 23 } });
    });
    app.get("/premium",  (_req, res) => res.json({ report: { premium: true } }));
    
    app.listen(4022, "0.0.0.0", () => console.log("listening on :4022"));
    
  3. 3
    Create the subscription

    The SDK forwards the Buyer's double signature to the Facilitator; the contract creates the subscription and charges the first period.

    ts
    // Subscribe is automatic: buyer POSTs the double-signed payload
    // (Permit2 PermitSingle + SubscriptionTerms) to any subscription route
    // whose `accepts` lists the target plan. Middleware verifies + settles
    // via the facilitator. No extra seller code.
    
    const routes = {
      "GET /weather": {
        accepts: [toAccept(basic)],              // plan(s) a buyer can subscribe to here
        description: "Weather data (Basic plan)",
        mimeType: "application/json",
      },
    };
    
    const app = express();
    app.use(express.json());
    app.use(paymentMiddleware(routes, server));  // enables automatic subscribe
    
    app.get("/weather", (req, res) => {
      // On the FIRST successful request the middleware runs the subscribe flow
      // and attaches the subscription onto req.
      const x402 = (req as any).x402;
      res.json({
        report: { weather: "sunny", temperature: 23 },
        subId: x402?.subscription?.subId,
      });
    });
    
    // On success the middleware returns a `PAYMENT-RESPONSE` header with
    // { subId, txHash, state } (state===1 → active). Failure → HTTP 402.
    
  4. 4
    Schedule the charges

    Trigger charges on a schedule per nextChargeableAt.

    ts
    // ── Path A: use the SDK layer (SubscriptionClient) ──
    // list() reads every persisted sub; nextChargeableAt (populated on every
    // getSubscription / charge) marks the next due boundary. `client.charge`
    // deducts once AND reconciles the store (advance lastChargedPeriod, migrate
    // subId on downgrade activation). Missed periods aren't backfilled: one
    // period per call, contract-side.
    //
    // Reuse the `store` / `client` from step 02 — no re-wiring needed.
    const nowSec = () => Math.floor(Date.now() / 1000);
    
    const timer = setInterval(async () => {
      const subs = await store.list();
      for (const sub of subs) {
        if (sub.state !== "active") continue;                          // canceled / completed / changed: skip
        if (sub.nextChargeableAt == null) continue;                    // uncharged snapshot; refresh via syncFromChain
        if (sub.nextChargeableAt > nowSec()) continue;                 // not due yet
        try {
          const r = await client.charge(sub.subId);
          // r.txHash / r.state / r.planChangeTriggered (downgrade activation:
          // when true, `sub.changedToSubId` becomes the new active subId — the
          // store row has already been migrated by scheme.charge).
        } catch (_e) {
          // dunning: retry with backoff, notify buyer, auto-cancel after N failures.
        }
      }
    }, 60_000);
    
    // ── Path B: fully custom orchestration + low-level charge ──
    // Drive scheduling / selection / state yourself; call the facilitator's
    // `chargeSubscription` directly and handle the raw ChargeResult
    // (period / state / txHash / planChangeTriggered) in your own store.
    import type { SubscriptionFacilitatorClient } from "@okxweb3/app-x402-core/subscription";
    // `facilitator` is your OKXFacilitatorClient — it implements
    // SubscriptionFacilitatorClient (subscribe / change / cancel / charge /
    // getSubscription).
    const dueSubIds: string[] = /* your query — Redis, Postgres, cron shard, … */ [];
    for (const subId of dueSubIds) {
      await (facilitator as SubscriptionFacilitatorClient).chargeSubscription(subId);
    }
    

Cancel a subscription#

Both the Buyer and the Seller can initiate cancellation:

  • Stops all future charges immediately
  • No refund of what's already been paid
  • Can only cancel while the subscription is active
ts
// Declare `operation` routes; the middleware relays the buyer-signed
// CancelAuth (or PendingChangeCancelAuth) to the facilitator. No 402
// handshake — the buyer POSTs an already-signed auth, middleware verifies
// and settles before the handler runs.

const routes = {
  // ... resource routes ...

  "POST /subscription/cancel": {                    // buyer POSTs a signed CancelAuth
    accepts: [toAccept(basic), toAccept(pro)],      // list every plan you support
    description: "Cancel a subscription",
    mimeType: "application/json",
    operation: "cancel" as const,
  },
  "POST /subscription/cancel-pending": {            // revert a not-yet-effective downgrade
    accepts: [toAccept(basic), toAccept(pro)],
    description: "Cancel a scheduled downgrade",
    mimeType: "application/json",
    operation: "cancel-pending-change" as const,
  },
};

const app = express();
app.use(express.json());
app.use(paymentMiddleware(routes, server));

// Register the routes; middleware intercepts them BEFORE the handler runs.
app.post("/subscription/cancel",         (_req, res) => res.json({ ok: true }));
app.post("/subscription/cancel-pending", (_req, res) => res.json({ ok: true }));

// On success the middleware writes a `PAYMENT-RESPONSE` header with
// { subId, txHash, state } — state===3 → canceled.
//
// Merchant-initiated cancel bypasses the buyer entirely: sign a CancelAuth
// with `initiator="merchant"` locally and call `client.cancelBySeller(subId, auth)`.

Advanced: support plan changes#

On top of basic integration, add one endpoint that handles plan changes; both upgrade and downgrade go through it, and everything else stays the same:

Example code

ts
// One change endpoint; direction (up/downgrade) is derived from target vs.
// current tier. Two phases, same URL:
//   phase 1: APP-Access header alone → 402 with extra.changeFrom (current sub)
//   phase 2: buyer signs new SubscriptionTerms bound to changeFrom, resubmits
//            with PAYMENT-SIGNATURE → middleware executes the change.

const routes = {
  // ... resource routes, cancel routes ...

  "GET /subscription/change": {
    accepts: [                                    // list every switchable plan
      toAccept(basic),
      toAccept(pro),
    ],
    description: "Change your subscription plan",
    mimeType: "application/json",
    operation: "change" as const,
  },
};

const app = express();
app.use(express.json());
app.use(paymentMiddleware(routes, server));

// Reached only after a successful change: new subId / txHash / state
// are on `PAYMENT-RESPONSE`, and req.x402.settleResult.data carries them
// for handler consumption.
app.get("/subscription/change", (req, res) => {
  const data = (req as any).x402?.settleResult?.data;
  res.json({
    result: "subscription plan changed",
    newSubId: data?.newSubId,
    operationType: data?.operationType,          // "upgrade" | "downgrade"
    scheduledFromPeriod: data?.scheduledFromPeriod,
  });
});

// Upgrade activates immediately (state===1). Downgrade schedules a pending
// change at period_end; buyer sees the target planId in
// subscription.pendingPlanChange until the boundary. To roll it back before
// it activates, use the `cancel-pending-change` route from step 05.

Upgrade vs. downgrade

UpgradeDowngrade
DirectionNew tier's planTier is higherNew tier's planTier is lower (same tier is rejected)
When it takes effectImmediately (auto-completes at the moment of switching)After the current period ends — your backend must trigger a charge for the switch to happen
How much is charged thenThe new tier's first period: full by default; to charge only the difference / apply a credit, compute the amount yourself and fill initialChargeAmountThe new (lower) tier's first period
subId changeReturns the new subId on the spotReturns one at registration (state pending), activated once effective

Example (Basic $10/mo, Pro $30/mo, billed by subscription date, using "keep the billing date")

  • Subscribe to Basic on 3/15 → charge $10 on the 15th of each month.
  • Upgrade to Pro on 6/20 (effective immediately): charge the current-period difference $30 − $10 = $20 that day; from 7/15 charge $30 for Pro, billing date still the 15th.
  • Downgrade back to Basic on 9/20 (effective at period end): only registered that day, keep using Pro through September; on 10/15 you trigger a charge → switch to Basic and charge $10; the Pro already paid is not refunded.

Billing date after upgrade

After the upgrade, keep charging on the original billing date and end date; on the upgrade day charge only the current-period difference. Good for merchants who need consistent reconciliation.

Example: originally charged on the 15th each month, user upgrades 6/20 → charge the current-period difference on 6/20, charge the new tier from 7/15, billing date still the 15th.

Parameters:

  • startAt = the current billing period's start
  • initialChargePeriods = 1
  • initialChargeAmount = new tier's current-period due − old tier's current-period paid (≥0)
  • maxPeriods = original periods − current period index + 1

Consecutive changes

Operation sequenceWhat to do
Upgrade → then want to go back downInitiate a downgrade on the new subscription (an upgrade can't be undone instantly, only downgraded at period end)
Registered downgrade → then want to upgradeRevert the downgrade first, then upgrade (only one pending change at a time)
Same-tier changeRejected outright

Integration notes & APIs#

  • Scheduled charges must be reliable — missed periods aren't backfilled, at most 1 period per charge; select due subscriptions by nextChargeableAt and trigger idempotently.
  • Failures are known synchronously — no webhook. Input errors surface directly as API errors; on-chain results must be read from the response data.state — it may return pending (0), so poll the query API to confirm the final state.
  • Allowance must cover the whole subscription period — multiple subscriptions share one Permit2 allowance, and a new authorization overrides the old; when upgrading to a pricier / longer plan, the Buyer must re-sign a larger allowance.
  • subId changes — after an upgrade/downgrade, update your mapping to the new subId returned by the API; no need to trace history.

For the subscription query APIs (subscription details / charge history / pending downgrade / authorization status / a Buyer's subscription list), fields, auth, and full error codes, see API Reference.

Buyer integration#

Buyers subscribe using an AI Agent that supports Onchain OS (e.g. Claude Code, Cursor) — just tell the Agent what you want in natural language.

  1. 1
    Install Onchain OS + log into your wallet

    Have the Agent install the skills, then log into the Agentic Wallet with your email (first login auto-creates a wallet; the private key is generated inside a TEE):

    Run npx skills add okx/onchainos-skills, then log into the Agentic Wallet with your email

    See Install the Agentic Wallet.

  2. 2
    Subscribe to a Seller's service

    Have the Agent access the Seller's paid endpoint. The Agent receives the 402 and optional plans; pick the tier you want, confirm as prompted, and the subscription authorization is done:

    Visit <the Seller's paid endpoint> and subscribe to the Pro plan
  3. 3
    Start using it

    After that, visiting the same endpoint returns the service; the Seller charges each period automatically — no further action from you.

  4. 4
    Upgrade / downgrade

    Have the Agent do:

    Upgrade/downgrade <the Seller's paid endpoint> to the Pro plan
  5. 5
    Cancel the subscription

    Have the Agent do:

    Cancel the Pro plan subscription on <the Seller's paid endpoint>

Limits and trade-offs#

When not to use Subscription

FAQ#

Can I refund users?
No on-chain refunds. Upgrade differences, downgrades, and cancellations never refund amounts already paid.
If you need to refund, do it off-chain via a separate transfer from your backend; or apply a credit through the first-period amount on the next upgrade/downgrade.
The old subscription immediately becomes "Changed" and stops charging; a new subscription is created and a new subId is returned.
Just update your "user ↔ subscription" mapping to the new subId — no need to trace history.
A downgrade takes effect at the end of the current period, letting the user finish the period they already paid for — matching mainstream subscription conventions.
After the period ends, your backend must trigger one charge for the switch to happen, and it must do so before the pending change expires.
Missed periods aren't backfilled — at most 1 period per charge; across several missed periods only the current one is charged and the intermediate ones are void.
So your scheduled charge job must be reliable and fire on time.
Pausing isn't supported. If needed, cancel and have the user re-subscribe later.
The per-period amount is fixed throughout. For tiered pricing, you can only override the first N periods via the first-period params — e.g. a promo price for the first 3 periods, then a uniform price from period 4.

Next#