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 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.
| Model | Who initiates the transfer? | What the merchant stores | When service should change |
|---|---|---|---|
| Automatic debit | A pre-authorized payment mechanism | A reusable authorization or payment credential | After the debit result is reliable |
| Recurring stablecoin invoice | The payer, for every invoice | Subscription, invoice, payment-order, and event records | After 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 idempotentlyPlans 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.
| Signal | Appropriate use | Not sufficient for |
|---|---|---|
| Checkout opened or browser returned | Display billing progress | Marking an invoice paid or renewing access |
payment.detected | "Payment received" messaging | A non-reversible renewal or ledger entry |
payment.confirmed | Progress UI or deliberately reversible work | Assuming final chain risk has passed |
Verified payment.finalized | Recording finality of the underlying chain transfer | Assuming subscription settlement has already run |
end_user_invoice.paid | Recording that the invoice settlement transaction committed | Deciding whether this was activation or renewal |
end_user_subscription.activated or end_user_subscription.renewed | Activating or renewing service idempotently | Skipping 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 situation | Current lifecycle behavior | Merchant action |
|---|---|---|
| First invoice paid | Invoice becomes paid; subscription emits activated and becomes active | Grant initial access idempotently |
| First invoice not paid | After its due window, with no in-flight order, the invoice becomes uncollectible and the incomplete subscription becomes expired | Treat the record as terminal and route the customer into your re-enrollment or support policy |
| Renewal invoice opened | The current subscription remains active while the payer receives a new invoice | Issue a fresh Portal session and Checkout link; the first payment grants no mandate |
| Renewal not paid | After the current period ends, the subscription enters past_due; after grace, it becomes expired and open invoices become uncollectible | Apply your documented grace and access policy, and reconcile state periodically |
| Payment arrives after collection | An uncollectible invoice stays unsettled and emits end_user_invoice.payment_late | Review, refund, credit, or restore service through an explicit support workflow |
| Upgrade to a higher-priced plan | An upgrade_proration invoice for the price difference opens immediately; the plan switches only after that invoice settles | Present and collect the difference invoice |
| Downgrade or equal-price change | The target plan remains pending until the next renewal | Show the future effective date |
| Cancel at period end | cancelAtPeriodEnd is set; the subscription can be resumed before it reaches a terminal state | Keep access until the current period ends according to your policy |
| Cancel immediately | Open invoices become uncollectible and the subscription becomes canceled | Stop 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
successUrlonly 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, anduncollectibleinvoices, together withpast_dueandexpiredsubscriptions, 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.