Stripe Checkout API
Attribute revenue from the Stripe Checkout API.
Attribute payments from Checkout Sessions that your own server creates. This takes about ten minutes.
Before you start
Connect Stripe first. Install the web SDK on your pricing page and the Node SDK on your server.
Send the visitor IDs to your server
The browser holds the two IDs that identify the visitor. Read them with
checkoutMetadata() and post them with the rest of the checkout request.
// app/pricing/checkout-button.tsx
"use client";
import { founderhq } from "@founderhq/events";
export function CheckoutButton({ priceId }: { priceId: string }) {
async function checkout() {
const identity = founderhq.checkoutMetadata();
const response = await fetch("/api/checkout", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ priceId, identity }),
});
const { url } = await response.json();
window.location.href = url;
}
return <button onClick={checkout}>Start the growth plan</button>;
}Place the metadata on the session and on the renewal object
Put the same metadata in two places. Session metadata alone does not survive renewals, so the second placement is what keeps a subscription attributed.
// app/api/checkout/route.ts
import Stripe from "stripe";
import { checkoutMetadata } from "@founderhq/events-node";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId, identity } = await request.json();
const attribution = checkoutMetadata({
anonymousId: identity.fhq_anonymous_id,
sessionId: identity.fhq_session_id,
});
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
success_url: "https://acme.com/thanks",
cancel_url: "https://acme.com/pricing",
metadata: attribution,
subscription_data: { metadata: attribution },
});
return Response.json({ url: session.url });
}Selling a one-time product? Use payment_intent_data: { metadata: attribution }
in place of subscription_data.
checkoutMetadata() throws when either ID is empty. Catch that and create the
session without the metadata rather than blocking the sale.
Let renewals resolve themselves
FounderHQ binds the Stripe customer and subscription on the first payment. Later renewals arrive with no browser session, and FounderHQ resolves them through that binding.
Verify
- Start a checkout on your pricing page.
- In Stripe, open the Checkout Session. The metadata shows
fhq_anonymous_idandfhq_session_id. - Open the created subscription. It shows the same two keys.
- Pay with a test card. In FounderHQ, the payment shows the channel that brought the buyer.
Troubleshoot
The session has metadata but the subscription does not
You set metadata only. Add subscription_data.metadata with the same object
and create a new session.
The payment arrives with no channel
The browser sent empty IDs. checkoutMetadata() in the browser returns an
empty object when the visitor denied consent. Check consent before you blame
the server.