Quickstart
This guide will walk you through getting Proxy up and running so you can provide a delegated checkout experience for your customers.
Before You Code
Section titled “Before You Code”Before using any SDKs, connect your payment provider, set up its webhook, create Proxy API keys, and configure the handoff experience.
Connect Your Payment Provider
Section titled “Connect Your Payment Provider”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 Stripe private key with read permission for all core resource types
Connect a Proxy Webhook on Your PSP
Section titled “Connect a Proxy Webhook on Your PSP”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 | At least 2024-06-20 |
| Events | payment_intent.succeededpayment_intent.payment_failedpayment_intent.canceledcheckout.session.completedinvoice.paidinvoice.payment_failedcustomer.subscription.updatedcustomer.subscription.deleted |
| 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.
Create Proxy API Keys
Section titled “Create Proxy API Keys”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.
Configure Hosted Handoff
Section titled “Configure Hosted Handoff”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
- Checkout URL: the page in your app where the payer completes checkout
- Allowed hosts: the domains Proxy is allowed to forward payers to
The Checkout URL’s domain must be included in Allowed hosts. Save this before creating payment links; without it, Proxy cannot create a shareable handoff link.
Install SDKs
Section titled “Install SDKs”For your backend apps:
npm install @proxy-checkout/server-js @proxy-checkout/stripe-server-js stripepnpm add @proxy-checkout/server-js @proxy-checkout/stripe-server-js stripeyarn add @proxy-checkout/server-js @proxy-checkout/stripe-server-js stripeWe currently don’t have SDKs for other languages. Refer to the API reference for the available endpoints.
For your frontend apps:
npm install @proxy-checkout/client-jspnpm add @proxy-checkout/client-jsyarn add @proxy-checkout/client-jsCreate the Handoff Link
Section titled “Create the Handoff Link”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;}Create the Proxy client and your Stripe instance in server-only code.
import { createProxyCheckoutServerClient } from "@proxy-checkout/server-js";import Stripe from "stripe";
export const proxy = createProxyCheckoutServerClient({ apiKey: process.env.PROXY_SECRET_KEY!, publishableKey: process.env.PROXY_PUBLISHABLE_KEY!,});
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);Then implement the endpoint your button calls. Authenticate the buyer, load the current cart, pass that cart snapshot to Proxy, then ask Proxy for a shareable handoff link.
// POST /api/proxy/handoffsexport async function POST(request: Request) { const { payerEmail } = (await request.json()) as { payerEmail?: string };
// Use the buyer from your current checkout session. const buyer = { id: "buyer_123" }; // Use the cart shown in your checkout. const cartSnapshot = { currency: "usd", lineItems: [ { name: "Annual membership", amountMinor: 5000, quantity: 1, }, ], }; const amountMinor = cartSnapshot.lineItems.reduce( (total, item) => total + item.amountMinor * item.quantity, 0, );
const handoff = await proxy.sessions.createHandoff({ amountMinor, buyerReference: buyer.id, cartSnapshot, currency: cartSnapshot.currency, idempotencyKey: `checkout:${buyer.id}:${amountMinor}`, payerContact: payerEmail ? { email: payerEmail } : undefined, });
return Response.json({ expiresAt: handoff.expiresAt, handoffUrl: handoff.handoffUrl, proxySessionId: handoff.id, });}The cart snapshot is not a replacement for your pricing logic. Treat it as checkout context that Proxy returns later, then re-check price, eligibility, and availability before creating the Stripe Checkout Session.
Build the Payer Checkout
Section titled “Build the Payer Checkout”When the payer opens the handoff link, Proxy sends them to the Checkout URL you configured in the dashboard with a proxy_session_id query parameter. Use your existing checkout UI on that page.
Parse the Proxy session ID, load the checkout state from your backend, then ask your backend for a Stripe client secret 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 createStripeCheckout(proxySessionId: string) { const response = await fetch( `/api/proxy/sessions/${encodeURIComponent(proxySessionId)}/checkout-session`, { method: "POST" }, );
if (!response.ok) { return null; }
return (await response.json()) as { clientSecret: string };}The only Proxy-specific browser step is reading the proxy_session_id. Use the returned clientSecret with your existing Stripe client flow.
First, return payer-safe checkout state. Retrieve the Proxy session with your secret key, parse the cart snapshot Proxy returns, and return only the checkout details the payer page needs.
Read the returned cart snapshot and rebuild public checkout details from your source of truth.
// GET /api/proxy/sessions/:proxySessionIdtype CartSnapshot = { currency: string; lineItems: Array<{ name: string; amountMinor: number; quantity: number; }>;};
export async function GET( _request: Request, { params }: { params: { proxySessionId: string } },) { const session = await proxy.sessions.retrieve(params.proxySessionId); const cart = session.cartSnapshot as CartSnapshot;
return Response.json({ cart, proxySessionId: session.id, status: session.status, });}Then create or retrieve the Stripe Checkout Session for this Proxy session.
// POST /api/proxy/sessions/:proxySessionId/checkout-sessionimport { openCheckout } from "@proxy-checkout/stripe-server-js";
type CartSnapshot = { currency: string; lineItems: Array<{ name: string; amountMinor: number; quantity: number; }>;};
export async function POST( _request: Request, { params }: { params: { proxySessionId: string } },) { const checkout = await openCheckout({ proxy, stripe, proxySessionId: params.proxySessionId, buildCheckoutSessionParams: ({ cart }) => { const checkoutCart = cart as CartSnapshot;
return { // Use "subscription" for recurring carts and include recurring price data. mode: "payment", ui_mode: "custom", line_items: checkoutCart.lineItems.map((item) => ({ price_data: { currency: checkoutCart.currency, product_data: { name: item.name }, unit_amount: item.amountMinor, }, quantity: item.quantity, })), return_url: "https://example.com/checkout/complete?session_id={CHECKOUT_SESSION_ID}", }; }, });
if (!checkout.clientSecret) { throw new Error("Stripe did not return a client secret."); }
return Response.json({ clientSecret: checkout.clientSecret });}Return the clientSecret to your checkout page and complete payment with your Stripe client flow.