Stripe direct subscriptions
Use this path when your application already owns its Stripe Elements or saved-billing-method flow and needs Proxy delegated checkout without a Checkout Session. Your authenticated merchant server calls openSetupIntent and openSubscription from @proxy-checkout/stripe-server-js with a merchant-injected Stripe client. Proxy’s API, workers, and reconciliation key never create or mutate SetupIntents, Subscriptions, Invoices, PaymentIntents, Charges, refunds, or disputes.
Choose one branch:
- SetupIntent: use for a shared payer presentation that collects a payment method for this purchase/shared billing account. A succeeded SetupIntent is preparation only; it never grants, completes, or earns a fee.
- Saved Payment Method: use only when an authorized shared billing account already selected the method before the handoff. Do not select whichever invited parent’s private saved method happens to open the link first.
Both branches reserve or join the same order-scoped direct-subscription acquisition before Stripe creation. Do not create a Proxy session, reservation, SetupIntent, or Subscription per viewer.
After choosing a branch, use its technical ID for the canonical facts check:
proxy stripe doctor --path SUB-SI --format jsonproxy stripe doctor --path SUB-SAVED --format jsonThe copyable example below pins the proven Stripe API 2022-11-15 lane with
stripe 12.18.0. Create its matching merchant-server client once and pass
the same compatibility declaration to every helper call:
import Stripe from "stripe";
const directSubscriptionStripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2022-11-15",});
const directSubscriptionCompatibility = { apiVersion: "2022-11-15", serverSdkVersion: "12.18.0",} as const;Setup Intent
Section titled “Setup Intent”Install the Stripe server package before copying this path's backend example. The exact version comes from the same canonical capability manifest as stripe doctor.
npm install --save-exact stripe@12.18.0
pnpm add --save-exact stripe@12.18.0
yarn add --exact stripe@12.18.0
- Create or recover one durable merchant purchase request, one Proxy session, and one shared handoff URL.
- From the authenticated payer bootstrap route, call
openSetupIntentwith that Proxy session, your server-side Stripe client, the purchase/shared-billing Customer, and only supported SetupIntent parameters. - Mount the returned client secret in your existing Elements UI. Multiple authorized viewers receive the same attached SetupIntent presentation.
- After Stripe reports the SetupIntent as
succeeded, call the SetupIntent branch ofopenSubscription. The helper retrieves the SetupIntent, verifies Proxy metadata and the Customer/PaymentMethod relationship inside your server process, validates every fixed recurring Price, creates or recovers one Subscription, and attaches its direct Subscription root. - If the initial Invoice requires authentication, return only the helper’s discriminated client-secret action to the authorized browser. Do not grant from the create response or browser return.
- Wait for signed Stripe evidence to reach Proxy, then treat Proxy’s signed event as a wakeup to read current state and run your idempotent merchant fulfillment policy.
For Stripe API 2022-11-15, use the legacy expanded initial PaymentIntent
shape:
import { openSetupIntent, openSubscription } from "@proxy-checkout/stripe-server-js";
const buildSubscriptionParams = ({ cart }: { cart: SubscriptionCart }) => ({ items: cart.lines.map((line) => ({ price: line.priceId, quantity: line.quantity })), trial_period_days: cart.trialDays || undefined,});
const setup = await openSetupIntent({ compatibility: directSubscriptionCompatibility, proxy, stripe: directSubscriptionStripe, proxySessionId, customerId: sharedBillingAccount.stripeCustomerId, initialInvoiceActionShape: "legacy_payment_intent", buildSetupIntentParams: () => ({ usage: "off_session" }), buildSubscriptionParams,});
if (setup.outcome !== "ready") { return Response.json({ cart: setup.cart, outcome: setup.outcome, sessionStatus: setup.sessionStatus });}
// Return setup.clientSecret only to an authorized Elements page. Keep the// SetupIntent object and Customer identity inside this server process.After Elements reports completion, the same server retrieves and verifies the SetupIntent before creating the Subscription:
const subscription = await openSubscription({ compatibility: directSubscriptionCompatibility, proxy, stripe: directSubscriptionStripe, proxySessionId, initialInvoiceActionShape: "legacy_payment_intent", buildSubscriptionParams, fromSetupIntent: { acquisitionAttemptId: setup.acquisitionAttemptId, setupIntentId: setup.setupIntentId, buildSetupIntentParams: () => ({ usage: "off_session" }), },});
if (subscription.outcome !== "ready") { return Response.json({ cart: subscription.cart, outcome: subscription.outcome, sessionStatus: subscription.sessionStatus, });}
return Response.json({ subscriptionId: subscription.subscriptionId, initialInvoiceAction: subscription.initialInvoiceAction,});Pass the same normalized builders to both calls so retries preserve one reservation fingerprint. For current Stripe API shapes, select initialInvoiceActionShape: "confirmation_secret". Do not attempt a runtime fallback between shapes; pin the lane your merchant integration has proven.
Never send the Customer ID, PaymentMethod ID, or either client secret to Proxy. Proxy retains only the first coherent nullable Customer ID diagnostic observed from already-required signed/read evidence, and exposes it only in merchant-secret diagnostics—not payer reads, ordinary merchant events, logs, or authority decisions.
Capability subscription.direct_setup_intent uses capability manifest v2 and parser contract v2. Its current availability isgeneral_availability.
| Compatibility fact | Minimum |
|---|---|
| Stripe request API | 2022-11-15 |
| stripe-node | 12.18.0 |
| Stripe.js | 2.4.0 |
| React Stripe.js | 2.9.0 |
Restricted-key reads
Setup Intents: Read, Subscriptions: Read, Invoices: Read, Payment Intents: Read. Leave Customer and Payment Methods access set to None.
| Operation | Read |
|---|---|
setup_intents.retrieve | /v1/setup_intents/:id |
subscriptions.retrieve | /v1/subscriptions/:id |
subscription_items.list_by_subscription | /v1/subscription_items?subscription=:id&limit=100 |
invoices.retrieve | /v1/invoices/:id |
invoices.list_by_subscription | /v1/invoices?subscription=:id&limit=100 |
invoices.line_items | /v1/invoices/:id/lines |
payment_intents.retrieve | /v1/payment_intents/:id |
setup_intents.list_probe | /v1/setup_intents?limit=1 |
subscriptions.list_probe | /v1/subscriptions?limit=1 |
invoices.list_probe | /v1/invoices?limit=1 |
payment_intents.list_probe | /v1/payment_intents?limit=1 |
Webhook Endpoint events
| Endpoint API profile | Required events | Optional compatibility events |
|---|---|---|
2022-11-15 through 2024-10-27 | customer.subscription.createdcustomer.subscription.deletedcustomer.subscription.trial_will_endcustomer.subscription.updatedinvoice.finalization_failedinvoice.marked_uncollectibleinvoice.paidinvoice.payment_action_requiredinvoice.payment_failedinvoice.voidedpayment_intent.canceledpayment_intent.payment_failedpayment_intent.processingpayment_intent.succeededsetup_intent.canceledsetup_intent.createdsetup_intent.requires_actionsetup_intent.setup_failedsetup_intent.succeeded | customer.subscription.pausedcustomer.subscription.resumed |
2024-10-28.acacia and newer | customer.subscription.createdcustomer.subscription.deletedcustomer.subscription.trial_will_endcustomer.subscription.updatedinvoice.finalization_failedinvoice.marked_uncollectibleinvoice.paidinvoice.payment_action_requiredinvoice.payment_failedinvoice.voidedpayment_intent.canceledpayment_intent.payment_failedpayment_intent.processingpayment_intent.succeededsetup_intent.canceledsetup_intent.createdsetup_intent.requires_actionsetup_intent.setup_failedsetup_intent.succeeded | customer.subscription.pausedcustomer.subscription.resumed |
Proxy wakeups
provider_acquisition.cancelled provider_acquisition.prepared provider_acquisition.processing provider_acquisition.reconciliation_required provider_acquisition.requires_action proxy_session.expired proxy_session.merchant_action_required proxy_session.paid proxy_session.provisionable subscription.activated subscription.cancel_scheduled subscription.cancel_schedule_removed subscription.cancelled subscription.changed subscription.configuration_action_required subscription.invoice_finalization_failed subscription.invoice_uncollectible subscription.invoice_voided subscription.paused subscription.payment_action_required subscription.payment_failed subscription.resumed subscription.renewed subscription.trial_ending
Visible unsupported configurations
automatic_tax connect direct_trial_without_payment_method managed_payments metered_billing multiple_invoice_payments out_of_band_payment pending_invoice_items payment_records promotion_code_choice send_invoice subscription_schedules
Saved Payment Method
Section titled “Saved Payment Method”Install the Stripe server package before copying this path's backend example. The exact version comes from the same canonical capability manifest as stripe doctor.
npm install --save-exact stripe@12.18.0
pnpm add --save-exact stripe@12.18.0
yarn add --exact stripe@12.18.0
The saved-method branch skips SetupIntent creation. Your merchant server retrieves the selected PaymentMethod and verifies that its attached Customer equals the supplied purchase/shared-billing Customer before openSubscription creates anything. Proxy does not retrieve Customers or PaymentMethods and requires no restricted-key permission for either resource.
This branch is multi-viewer-safe only when every authorized payer is acting for the same shared billing account and the selected method is legitimately available to that account. One parent’s private wallet or saved card is not a shared method merely because that parent opened the delegated link first. Use the SetupIntent branch when that invariant is not already true.
const subscription = await openSubscription({ compatibility: directSubscriptionCompatibility, proxy, stripe: directSubscriptionStripe, proxySessionId, initialInvoiceActionShape: "legacy_payment_intent", buildSubscriptionParams, fromSavedPaymentMethod: { // Load both values from the trusted shared billing-account record. Never // accept them directly from the payer browser. customerId: sharedBillingAccount.stripeCustomerId, paymentMethodId: sharedBillingAccount.selectedStripePaymentMethodId, },});The helper retrieves the PaymentMethod with your injected Stripe client and requires its attached Customer to match before reservation/create can continue. It sets only this Subscription’s default_payment_method; it does not change the Customer-wide default.
Capability subscription.direct_saved_payment_method uses capability manifest v2 and parser contract v2. Its current availability isgeneral_availability.
| Compatibility fact | Minimum |
|---|---|
| Stripe request API | 2022-11-15 |
| stripe-node | 12.18.0 |
| Stripe.js | 2.4.0 |
| React Stripe.js | 2.9.0 |
Restricted-key reads
Subscriptions: Read, Invoices: Read, Payment Intents: Read. Leave Customer and Payment Methods access set to None.
| Operation | Read |
|---|---|
subscriptions.retrieve | /v1/subscriptions/:id |
subscription_items.list_by_subscription | /v1/subscription_items?subscription=:id&limit=100 |
invoices.retrieve | /v1/invoices/:id |
invoices.list_by_subscription | /v1/invoices?subscription=:id&limit=100 |
invoices.line_items | /v1/invoices/:id/lines |
payment_intents.retrieve | /v1/payment_intents/:id |
subscriptions.list_probe | /v1/subscriptions?limit=1 |
invoices.list_probe | /v1/invoices?limit=1 |
payment_intents.list_probe | /v1/payment_intents?limit=1 |
Webhook Endpoint events
| Endpoint API profile | Required events | Optional compatibility events |
|---|---|---|
2022-11-15 through 2024-10-27 | customer.subscription.createdcustomer.subscription.deletedcustomer.subscription.trial_will_endcustomer.subscription.updatedinvoice.finalization_failedinvoice.marked_uncollectibleinvoice.paidinvoice.payment_action_requiredinvoice.payment_failedinvoice.voidedpayment_intent.canceledpayment_intent.payment_failedpayment_intent.processingpayment_intent.succeeded | customer.subscription.pausedcustomer.subscription.resumed |
2024-10-28.acacia and newer | customer.subscription.createdcustomer.subscription.deletedcustomer.subscription.trial_will_endcustomer.subscription.updatedinvoice.finalization_failedinvoice.marked_uncollectibleinvoice.paidinvoice.payment_action_requiredinvoice.payment_failedinvoice.voidedpayment_intent.canceledpayment_intent.payment_failedpayment_intent.processingpayment_intent.succeeded | customer.subscription.pausedcustomer.subscription.resumed |
Proxy wakeups
provider_acquisition.reconciliation_required proxy_session.expired proxy_session.merchant_action_required proxy_session.paid proxy_session.provisionable subscription.activated subscription.cancel_scheduled subscription.cancel_schedule_removed subscription.cancelled subscription.changed subscription.configuration_action_required subscription.invoice_finalization_failed subscription.invoice_uncollectible subscription.invoice_voided subscription.paused subscription.payment_action_required subscription.payment_failed subscription.resumed subscription.renewed subscription.trial_ending
Visible unsupported configurations
automatic_tax connect managed_payments metered_billing multiple_invoice_payments out_of_band_payment pending_invoice_items payment_records promotion_code_choice send_invoice subscription_schedules
Supported initial outcomes
Section titled “Supported initial outcomes”| Stripe state | Proxy effect | Merchant action |
|---|---|---|
| SetupIntent created, requires action, failed, canceled, or succeeded | Retain non-financial acquisition progress; never grant, complete, or earn | Keep the cart actionable according to current state; a succeeded SetupIntent may proceed to Subscription creation |
| Positive automatic initial Invoice paid | Persist one Invoice-keyed payment, link one supported succeeded PaymentIntent when present, earn one fee, and allow one initial completion winner | Read current state and idempotently grant the merchant entitlement |
| Initial Invoice requires action or payment fails | Retain actionable failure; no fee or grant | Resume authentication or retry the same Invoice through merchant-owned Stripe UI/current-state logic |
| Exact-zero initial Invoice or coherent free trial | Provisionable under the zero/trial rule; no payment or fee | Apply the merchant’s trial/zero-access policy idempotently |
| Structurally incomplete or unsupported positive Invoice | Configuration or reconciliation action required; no grant or fee | Correct the Stripe configuration or investigate retained evidence |
Direct Subscription roots never receive a fake Checkout Session identity.
Complete lifecycle
Section titled “Complete lifecycle”Proxy observes initial activation, trial conversion, every positive renewal or proration Invoice, failed/retried payment, authentication required, Invoice finalization failure, trial ending, pause/resume, fixed plan or quantity changes, scheduled cancellation and removal, terminal deletion, and bounded known-object recovery. Each distinct supported positive charge_automatically Invoice is the payment and fee identity, even when it has no PaymentIntent. A linked PaymentIntent supports correlation and refund/dispute observation but cannot earn a second fee.
The earned fee is 3% of the supported positive Invoice basis plus 30 minor units. Exact-zero and free-trial Invoices earn nothing. Refund and dispute observation never reverses an earned fee.
Metered items, send_invoice, managed or PaymentRecord allocation, paid-out-of-band evidence, multiple paid Invoice children, incomplete payment pagination, automatic tax/provider-finalized direct pricing, promotion-code choice, pending Invoice items, schedules, and Connect fields remain unsupported. Proxy retains visible configuration/reconciliation evidence but does not provision, renew, or pretend that these models are supported.
Refund and dispute observation is optional. When enabled and linkable through an already-known PaymentIntent or Charge, Proxy emits bounded signed wakeups. It never creates a refund, manages a dispute, reverses a fee, cancels a Subscription, or decides access policy.
Fulfillment remains yours
Section titled “Fulfillment remains yours”Keep your independent Stripe webhook and current-state logic. Proxy’s signed merchant events are wakeups, not fulfillment commands and not a replacement source of entitlement authority. Route Stripe and Proxy wakeups into one merchant-owned idempotent state transition keyed by your durable purchase/entitlement and current Stripe/Proxy state. A duplicate or reordered wakeup must not grant twice, extend twice, or revoke from stale evidence.
Treat subscription.changed as a current-state refresh, never renewal evidence. Treat subscription.configuration_action_required, payment/action/finalization failures, trial ending, pause/resume, cancellation schedule changes, and refund/dispute events as prompts to re-read and apply your policy. Proxy does not fulfill or revoke for you.
Validate
Section titled “Validate”- Open one durable handoff in two payer views and confirm both recover the same cart and SetupIntent or saved-method subscription acquisition.
- Complete the initial Invoice, then confirm the other view becomes completed without a second Subscription.
- Exercise authentication-required, failed Invoice, free trial or zero initial Invoice where supported, and cancellation behavior.
- Retry and reorder signed events. Confirm initial fulfillment, renewal, and entitlement transitions remain idempotent.
- For the SetupIntent branch, prove SetupIntent success alone never grants access or records payment.
Continue with Entitlements and Stripe events and permissions.