Skip to content

Stripe setup and hosted Checkout quickstart

This guide configures Proxy with Stripe and implements the recommended starting path: a one-time payment that redirects the payer to Stripe-hosted Checkout. Use it when you are starting fresh or do not need to keep payment inside your own page.

If your product already creates a PaymentIntent, needs subscriptions, or keeps Checkout inside your page, first use the Stripe path chooser. It will send you to the guide that matches your existing payment flow.

Before using any SDKs, connect Stripe, create Proxy API keys, and configure the handoff experience. Configure the webhook promptly so Proxy can observe lifecycle changes.

This quickstart’s technical path ID is OT-CO-H. The Proxy CLI requires Node.js 22 or newer. Confirm the runtime, install the public CLI, then inspect its canonical compatibility requirements:

Terminal window
node --version
npm install --global @proxy-checkout/cli
proxy stripe doctor --path OT-CO-H --format json

One Stripe configuration with valid credentials includes every implemented one-time and subscription path; no Proxy operator enables paths individually. Webhook endpoint verification reports setup health but never disables an acquisition path. Validate in Stripe test mode; live mode requires separate approval.

Navigate to the payment providers section of your dashboard. In the Stripe section, enter the following:

  • Stripe account ID (found here)
  • Your Stripe publishable key
  • A dedicated Stripe restricted key for Proxy reconciliation

In Stripe’s API keys settings, create a restricted key with this exact manifest:

Stripe resourceAccess
Checkout SessionsRead
InvoicesRead
Payment IntentsRead
Setup IntentsRead
SubscriptionsRead
Webhook EndpointsRead

Leave every other Stripe permission set to None. Permission manifest v2.

Proxy uses this credential only for the exact read operations required to reconcile PaymentIntents, hosted/embedded/custom Checkout Sessions, SetupIntents, Subscriptions, and Invoices and to check the configured Webhook Endpoint. It is separate from:

  • your application’s server-side Stripe key used to create SetupIntents, PaymentIntents, Checkout Sessions, or subscriptions;
  • the Stripe publishable key used by Stripe Elements; and
  • the webhook signing secret copied from the Stripe webhook endpoint.

Use Stripe keys that match the current Proxy merchant mode. Test merchants require rk_test_ plus pk_test_ if you provide a publishable key. Live merchants require rk_live_ plus pk_live_. Set up test and live payment providers separately, including separate restricted keys, Stripe webhook endpoints, and signing secrets.

Proxy runs the exact list probes for the capabilities being configured before saving the connection. An invalid or under-permissioned key is rejected immediately with per-operation setup guidance; response bodies and provider objects are never persisted by the setup check.

The permission table above is the complete default profile for every implemented one-time and subscription path. Direct SetupIntent and saved-method subscriptions do not grant Proxy Customer or Payment Methods access. See Stripe direct subscriptions or Stripe-hosted subscriptions for the operations and events used by those paths.

If you connected Stripe before direct subscriptions became generally available, re-save or rotate the restricted key once before using those paths. Proxy validates the expanded read profile and then makes both direct-subscription branches available; webhook endpoint verification remains advisory.

Webhook setup is strongly recommended but is not an activation gate. An unverified endpoint remains visibly action_required in Proxy while every implemented acquisition path stays available. Without the required events, Proxy may not observe payment or subscription lifecycle changes, so its lifecycle and reconciliation state can remain stale.

In Proxy’s payment providers dashboard, add a webhook config and enter a route key. Proxy shows the webhook URL as soon as the route key is set. Copy that URL, but keep the Proxy form open.

Go to Stripe’s Webhooks dashboard and create a new webhook endpoint with the following settings:

Setting Value
Event destination scope Your account
API version 2022-11-15 for the exact classic Elements lane, or another version supported by the generated capability profile
Events checkout.session.async_payment_failed
checkout.session.async_payment_succeeded
checkout.session.completed
checkout.session.expired
payment_intent.canceled
payment_intent.payment_failed
payment_intent.processing
payment_intent.succeeded
Destination type Webhook endpoint
Endpoint URL The webhook URL copied from Proxy

After Stripe creates the endpoint, copy the new webhook’s signing secret. Paste it into the Proxy webhook secret field and create the webhook config.

That event list is the Checkout quickstart baseline, not the complete setup profile. Configure the exact sorted event union shown by Proxy for the full implemented path set plus any optional observations. Direct subscriptions add SetupIntent progress, trial ending, payment action required, Invoice finalization/uncollectible/voided, Subscription creation, and complete lifecycle events as listed in Stripe direct subscriptions. proxy listen --stripe derives the same union from the configuration and rejects a stale profile.

Head over to the API Keys section of your Proxy dashboard and create a secret key and a publishable key. Our SDKs will use these keys to connect to Proxy.

A handoff is the Proxy-hosted payment link the buyer shares when someone else needs to pay. Proxy uses this setup to know what name to show on that link and where to send the payer after they open it.

In the Hosted handoff section, save these required fields:

  • Display name: the merchant or product name shown to the payer
  • Default checkout URL: the default page in your app where the payer completes checkout
  • Allowed destination hosts: the domains Proxy is allowed to forward payers to

The default checkout URL’s domain must be included in Allowed destination hosts. Save this before creating payment links; without it, Proxy cannot create a shareable handoff link.

See Hosted Handoff for the Logo, Title, Description, and Site name fields, plus custom-domain preview behavior.

For your backend apps:

Terminal window
npm install @proxy-checkout/server-js @proxy-checkout/stripe-server-js

The selected terminal section also provides a stripe install command pinned to the server_sdk version reported by stripe doctor. Run it before copying that path’s backend example; do not install an unversioned current Stripe SDK and report an older compatibility lane.


For a hosted Checkout frontend, install the Proxy client:

Terminal window
npm install @proxy-checkout/client-js

Embedded and custom Checkout also need Stripe’s browser packages. The selected terminal section provides exact install commands generated from the same canonical versions as stripe doctor; run them before copying its client example.

Start in the checkout page the buyer already uses. Proxy does not render this button for you. Add your own delegated payment button and call your backend when it is clicked.

type HandoffResponse = {
expiresAt: string;
handoffUrl: string;
proxySessionId: string;
};
async function startDelegatedPayment(
payerEmail?: string,
): Promise<HandoffResponse | null> {
const response = await fetch("/api/proxy/handoffs", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ payerEmail }),
});
if (!response.ok) {
// fallback
return null;
}
const handoff = (await response.json()) as HandoffResponse;
// Present the url to the user. Whether you copy it to clipboard,
// show a modal, or open up a mobile share sheet is your decision.
return handoff;
}

The handoff example above stores a one-time cart. Before calling createHandoff, replace that cartSnapshot with the subscription shape used by your selected terminal guide. Load Stripe Price IDs from trusted server configuration or your merchant-owned catalog—never accept them from the payer browser.

For a subscription created by Stripe Checkout, store the recurring Price. Add the one-time Price only when the first invoice actually includes one:

const amountMinor = 7500; // Calculate from your trusted catalog.
const initialOneTimePriceId = process.env.STRIPE_SETUP_PRICE_ID;
const cartSnapshot = {
currency: "usd",
recurringPriceId: process.env.STRIPE_RECURRING_PRICE_ID!,
...(initialOneTimePriceId ? { initialOneTimePriceId } : {}),
quantity: 1,
};

For a direct subscription using a SetupIntent or an already-authorized saved payment method, store the fixed recurring lines:

const amountMinor = 5000; // Calculate from your trusted catalog.
const cartSnapshot = {
currency: "usd",
lines: [
{
priceId: process.env.STRIPE_RECURRING_PRICE_ID!,
quantity: 1,
},
],
trialDays: 0,
};

Use the initial amount expected for this purchase; use 0 when a supported free trial or zero-value initial invoice makes nothing due now. The terminal guide revalidates the stored Price IDs, quantities, eligibility, and current pricing before creating anything in Stripe.

When the payer opens the handoff link, Proxy sends them to the default checkout URL you configured in the dashboard with a proxy_session_id query parameter. This example opens OT-CO-H and redirects to Stripe’s hosted one-time Checkout page.

Parse the Proxy session ID, load the checkout state from your backend, then ask your backend for the hosted redirect when the payer is ready to pay.

import { parseProxySessionIdFromUrl } from "@proxy-checkout/client-js";
async function loadPayerCheckout() {
const proxySessionId = parseProxySessionIdFromUrl(window.location.href);
if (!proxySessionId) {
return null;
}
const response = await fetch(
`/api/proxy/sessions/${encodeURIComponent(proxySessionId)}`,
);
if (!response.ok) {
return null;
}
const checkout = await response.json();
return checkout;
}
async function openStripeCheckout(proxySessionId: string) {
const response = await fetch(
`/api/proxy/sessions/${encodeURIComponent(proxySessionId)}/checkout-session`,
{ method: "POST" },
);
if (!response.ok) {
return null;
}
const opened = (await response.json()) as
| { outcome: "ready"; redirectUrl: string }
| {
cart: unknown;
outcome: "already_paid" | "action_required" | "processing" | "unavailable";
sessionStatus: string;
};
if (opened.outcome === "ready" && "redirectUrl" in opened) {
location.assign(opened.redirectUrl);
}
return opened;
}

The only Proxy-specific browser input is proxy_session_id. Redirect only for ready. Render already_paid as an explicit completed order with the returned stable cart; render other terminal states without starting Stripe. The Checkout success URL is navigation, not fulfillment authority.