Skip to content

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:

Terminal window
proxy stripe doctor --path SUB-SI --format json
proxy stripe doctor --path SUB-SAVED --format json

The 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;

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
  1. Create or recover one durable merchant purchase request, one Proxy session, and one shared handoff URL.
  2. From the authenticated payer bootstrap route, call openSetupIntent with that Proxy session, your server-side Stripe client, the purchase/shared-billing Customer, and only supported SetupIntent parameters.
  3. Mount the returned client secret in your existing Elements UI. Multiple authorized viewers receive the same attached SetupIntent presentation.
  4. After Stripe reports the SetupIntent as succeeded, call the SetupIntent branch of openSubscription. 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.
  5. 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.
  6. 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 factMinimum
Stripe request API2022-11-15
stripe-node12.18.0
Stripe.js2.4.0
React Stripe.js2.9.0

Restricted-key reads

Setup Intents: Read, Subscriptions: Read, Invoices: Read, Payment Intents: Read. Leave Customer and Payment Methods access set to None.

OperationRead
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 profileRequired eventsOptional compatibility events
2022-11-15 through 2024-10-27customer.subscription.created
customer.subscription.deleted
customer.subscription.trial_will_end
customer.subscription.updated
invoice.finalization_failed
invoice.marked_uncollectible
invoice.paid
invoice.payment_action_required
invoice.payment_failed
invoice.voided
payment_intent.canceled
payment_intent.payment_failed
payment_intent.processing
payment_intent.succeeded
setup_intent.canceled
setup_intent.created
setup_intent.requires_action
setup_intent.setup_failed
setup_intent.succeeded
customer.subscription.paused
customer.subscription.resumed
2024-10-28.acacia and newercustomer.subscription.created
customer.subscription.deleted
customer.subscription.trial_will_end
customer.subscription.updated
invoice.finalization_failed
invoice.marked_uncollectible
invoice.paid
invoice.payment_action_required
invoice.payment_failed
invoice.voided
payment_intent.canceled
payment_intent.payment_failed
payment_intent.processing
payment_intent.succeeded
setup_intent.canceled
setup_intent.created
setup_intent.requires_action
setup_intent.setup_failed
setup_intent.succeeded
customer.subscription.paused
customer.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

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

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 factMinimum
Stripe request API2022-11-15
stripe-node12.18.0
Stripe.js2.4.0
React Stripe.js2.9.0

Restricted-key reads

Subscriptions: Read, Invoices: Read, Payment Intents: Read. Leave Customer and Payment Methods access set to None.

OperationRead
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 profileRequired eventsOptional compatibility events
2022-11-15 through 2024-10-27customer.subscription.created
customer.subscription.deleted
customer.subscription.trial_will_end
customer.subscription.updated
invoice.finalization_failed
invoice.marked_uncollectible
invoice.paid
invoice.payment_action_required
invoice.payment_failed
invoice.voided
payment_intent.canceled
payment_intent.payment_failed
payment_intent.processing
payment_intent.succeeded
customer.subscription.paused
customer.subscription.resumed
2024-10-28.acacia and newercustomer.subscription.created
customer.subscription.deleted
customer.subscription.trial_will_end
customer.subscription.updated
invoice.finalization_failed
invoice.marked_uncollectible
invoice.paid
invoice.payment_action_required
invoice.payment_failed
invoice.voided
payment_intent.canceled
payment_intent.payment_failed
payment_intent.processing
payment_intent.succeeded
customer.subscription.paused
customer.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

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.

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.

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.

  1. Open one durable handoff in two payer views and confirm both recover the same cart and SetupIntent or saved-method subscription acquisition.
  2. Complete the initial Invoice, then confirm the other view becomes completed without a second Subscription.
  3. Exercise authentication-required, failed Invoice, free trial or zero initial Invoice where supported, and cancellation behavior.
  4. Retry and reorder signed events. Confirm initial fulfillment, renewal, and entitlement transitions remain idempotent.
  5. For the SetupIntent branch, prove SetupIntent success alone never grants access or records payment.

Continue with Entitlements and Stripe events and permissions.