Back to Blog
Engineering
2026-07-159 min readBy StableOps

USDC Subscriptions: How to Collect Recurring Stablecoin Payments Without Storing Wallets

Build USDC subscription payments with recurring invoices, payer-initiated checkout, and verified subscription settlement events.

USDC
Subscription payments
Stablecoin payments

USDC subscription payments should be designed as recurring billing, not as an attempt to recreate card auto-debit with a wallet private key or a standing wallet approval. Your service defines the plan and billing schedule; each invoice gives the customer a fresh payment request; the customer initiates a transfer through Hosted Checkout or their own wallet flow. Your backend grants or renews service only after a verified subscription event shows that the invoice settled and the subscription activated or renewed.

That model is practical for SaaS, API credits, and digital services. For stablecoin recurring payments, it also keeps the custody boundary clear: you control your receiving addresses, StableOps does not hold the payer's private key, wallet credential, or reusable debit authorization, and the payer decides when to sign each on-chain transfer.

Recurring invoices are not automatic wallet debits

The phrase "crypto subscription billing" can describe two very different systems. Treating them as the same is how a billing design accidentally makes promises its wallet flow cannot keep.

ModelWho initiates the transfer?What the merchant storesWhen service should change
Automatic debitA pre-authorized payment mechanismA reusable authorization or payment credentialAfter the debit result is reliable
Recurring stablecoin invoiceThe payer, for every invoiceSubscription, invoice, payment-order, and event recordsAfter a verified subscription event records activation or renewal

StableOps uses the second model. A plan determines the amount and interval, a subscription connects that plan to your merchantUserId, and an open invoice can start a payment attempt. A payer can pay its invoice in the hosted page or in a wallet UI you build. Neither the initial payment nor a browser return URL is a permission to pull USDC from that wallet in a later period.

This distinction should appear in your product copy, dunning email, and account UI. Say when the next invoice will be issued and how the customer will pay it; do not say that a wallet will be charged automatically unless you operate a separate, explicitly authorized payment mechanism.

Model the subscription as a bill-to-settlement lifecycle

For a monthly USDC plan, the durable path is straightforward:

Plan: USD amount + billing interval
          |
          v
Subscription for merchantUserId
          |
          v
Open invoice for the first or renewal period
          |
          v
Portal session -> Hosted Checkout (or your wallet flow)
          |
          v
Payer selects an accepted USDC chain and sends the exact payment instruction
          |
          v
payment.detected -> payment.confirmed -> payment.finalized
          |
          v
Invoice settles -> end_user_invoice.paid
          |
          v
end_user_subscription.activated or end_user_subscription.renewed
          |
          v
Merchant grants or renews service idempotently

Plans and invoices are USD-denominated rather than tied to one stablecoin. At checkout time, pass the chain and asset combinations you can receive, such as Base USDC. StableOps creates the invoice payment order and allocates receiving addresses for those accepted pairs. The invoice's settled asset is backfilled only when the Payment Order reaches FINALIZED and the invoice settlement transaction marks it paid; before then, including after creating a payment request, asset can still be null.

For a USDC-only launch, offer one supported USDC chain first. It reduces support ambiguity around network choice, address capacity, and the exact payment instruction. Add other accepted pairs only when your receiving-address pool and reconciliation process cover each of them. The Subscriptions guide explains the plan, invoice, Portal, and settlement model.

Create the first invoice and redirect the payer to checkout

Keep merchant operations on your backend. It creates plans, subscriptions, and Portal sessions with a secret API key. A Portal token is short-lived and scoped to one merchant user; your backend can use it to create an invoice checkout session, then send only the returned Checkout URL to the browser.

import { StableOps } from '@stableops/api-sdk'

const stableops = new StableOps({
  apiKey: process.env.STABLEOPS_API_KEY!,
})

const planId = process.env.STABLEOPS_PLAN_ID
if (!planId) throw new Error('STABLEOPS_PLAN_ID is required')

const created = await stableops.merchantSubscriptions.subscriptions.create(
  {
    planId,
    merchantUserId: 'user_123',
  },
  { idempotencyKey: 'subscription:user_123:starter' },
)

// Trial subscriptions do not issue the first invoice until the trial ends.
const invoice = created.invoice
if (!invoice) throw new Error('No invoice is due yet')

const portalSession = await stableops.merchantSubscriptions.portalSessions.create({
  merchantUserId: 'user_123',
})

const portal = stableops.portal(portalSession.portalToken)
const checkout = await portal.invoices.checkoutSession(invoice.id, {
  acceptedAssets: [{ chain: 'base-sepolia', asset: 'USDC' }],
  successUrl: 'https://merchant.example/billing/return',
  cancelUrl: 'https://merchant.example/billing/canceled',
})

return Response.redirect(checkout.checkoutUrl, 303)

STABLEOPS_PLAN_ID must contain the actual ID returned when the plan was created, not its human-readable code. Use a stable, merchant-owned idempotency key when creating the subscription, and persist the StableOps subscription and invoice IDs beside your own account record. A trial subscription returns invoice: null until its first invoice is due, so production code must handle that branch instead of forcing a non-null value. The complete public SDK flow, including plan creation and a custom wallet alternative, is in the Subscriptions guide.

successUrl is a browser return path, not a payment receipt. A customer may close the tab, return before the chain reaches finality, or revisit the URL without paying. Update the billing UI from invoice state and signed webhooks rather than using that redirect to extend an entitlement.

Let final settlement, not checkout completion, control access

An invoice checkout session wraps the invoice's payment order. The same chain lifecycle applies to a subscription invoice as to a one-time payment.

SignalAppropriate useNot sufficient for
Checkout opened or browser returnedDisplay billing progressMarking an invoice paid or renewing access
payment.detected"Payment received" messagingA non-reversible renewal or ledger entry
payment.confirmedProgress UI or deliberately reversible workAssuming final chain risk has passed
Verified payment.finalizedRecording finality of the underlying chain transferAssuming subscription settlement has already run
end_user_invoice.paidRecording that the invoice settlement transaction committedDeciding whether this was activation or renewal
end_user_subscription.activated or end_user_subscription.renewedActivating or renewing service idempotentlySkipping webhook verification or event deduplication

The normal payment events describe the underlying Payment Order, but end_user_invoice.* and end_user_subscription.* are the source of truth for subscription state. Subscribe to end_user_invoice.open and .paid, plus end_user_subscription.activated and .renewed; handle end_user_invoice.payment_late as an exception that requires an explicit recovery decision. For dunning, periodically read subscription state so past_due and expired accounts are not dependent on one notification path. Verify the raw webhook body, deduplicate by X-Event-Id, and make the actual entitlement operation idempotent by your internal account or subscription ID. The Webhook guide and Stablecoin Payment Webhooks cover the verification, retry, and fulfillment boundary.

Make renewal, change, and recovery explicit

Stablecoin billing has fewer hidden payment permissions, so the customer-facing lifecycle must be deliberate.

Billing situationCurrent lifecycle behaviorMerchant action
First invoice paidInvoice becomes paid; subscription emits activated and becomes activeGrant initial access idempotently
First invoice not paidAfter its due window, with no in-flight order, the invoice becomes uncollectible and the incomplete subscription becomes expiredTreat the record as terminal and route the customer into your re-enrollment or support policy
Renewal invoice openedThe current subscription remains active while the payer receives a new invoiceIssue a fresh Portal session and Checkout link; the first payment grants no mandate
Renewal not paidAfter the current period ends, the subscription enters past_due; after grace, it becomes expired and open invoices become uncollectibleApply your documented grace and access policy, and reconcile state periodically
Payment arrives after collectionAn uncollectible invoice stays unsettled and emits end_user_invoice.payment_lateReview, refund, credit, or restore service through an explicit support workflow
Upgrade to a higher-priced planAn upgrade_proration invoice for the price difference opens immediately; the plan switches only after that invoice settlesPresent and collect the difference invoice
Downgrade or equal-price changeThe target plan remains pending until the next renewalShow the future effective date
Cancel at period endcancelAtPeriodEnd is set; the subscription can be resumed before it reaches a terminal stateKeep access until the current period ends according to your policy
Cancel immediatelyOpen invoices become uncollectible and the subscription becomes canceledStop access; do not expect resume to revive it

For an upgrade or cancellation, use the subscription API or Merchant Portal instead of changing your internal plan label alone. resume only clears a pending period-end cancellation on a non-terminal subscription; it cannot revive a subscription that is already canceled or expired. Your application should retain the StableOps subscription ID, current plan, invoice IDs, and payment-order references so support and reconciliation can identify the exact billing period at issue. A Merchant Portal session gives an end user a limited context for their own subscription and invoices; it is not a substitute for your backend's authorization rules.

Production checklist for USDC subscription payments

  • Describe the product as periodic invoicing with payer-initiated USDC transfers, not automatic wallet debits.
  • Create plans and subscriptions from the backend; do not expose a merchant secret API key in the browser.
  • Store your account ID with the StableOps subscription ID, invoice IDs, and associated payment-order IDs.
  • Start with explicit supported USDC chain and asset pairs, and maintain adequate receiving-address capacity.
  • Treat the invoice amount and returned payment instructions as the source of truth for the payer's transfer.
  • Treat successUrl only as a user-experience path; settle access from verified, deduplicated subscription events and reconciled subscription state.
  • Process renewal, past-due, upgrade, cancellation, and resume transitions as explicit state changes.
  • Make entitlement writes idempotent by your internal account or subscription ID, even when webhook events are deduplicated.
  • Reconcile open, paid, void, and uncollectible invoices, together with past_due and expired subscriptions, before expanding to more chains or plans.

FAQ

Can USDC subscriptions charge a customer's wallet automatically?

Not in this recurring-invoice flow. Each invoice is paid by the customer through Hosted Checkout or a wallet flow they initiate. StableOps does not hold the customer's private key, wallet credential, or reusable debit authorization, and it does not treat the first payment as permission to transfer USDC in a later period.

Which USDC network should I support for subscription billing?

Start with one supported (chain, asset) pair that your customers already use and for which you have receiving addresses. The payer's selected payment instruction, including chain, USDC asset, address, and exact amount, is what the invoice payment order matches.

When should I renew a customer's service after they pay?

Activate on a verified and deduplicated end_user_subscription.activated event, and renew on end_user_subscription.renewed, with API reconciliation as a recovery path. The underlying payment.finalized event proves chain finality but can arrive before subscription settlement completes. Never renew on a wallet transaction hash, a Hosted Checkout return, or an unverified webhook.

Start with one plan and one subscription-event path

Create one monthly plan, issue a Sandbox subscription, and let a test user pay its first invoice through Hosted Checkout. Then connect the signed invoice and subscription webhook path before offering the plan to customers. The non-custodial USDC payment architecture explains the underlying Payment Order lifecycle. Once your service can collect a payer-initiated USDC transfer, activate or renew from subscription events, and recover a missed or late payment, you have a stable base for recurring billing without holding payer wallet credentials.

Related articles

Accept USDT payments with chain-specific instructions, exact order matching, confirmation tracking, and verified webhook fulfillment.

Build reliable crypto payment webhook handling with raw-body signature verification, event deduplication, idempotent fulfillment, and replay-safe recovery.

Accept USDC payments into merchant-controlled wallets with payment orders, chain monitoring, finality, and reliable webhook fulfillment.