Documentation
RevenueConnect Stripe

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

  1. Start a checkout on your pricing page.
  2. In Stripe, open the Checkout Session. The metadata shows fhq_anonymous_id and fhq_session_id.
  3. Open the created subscription. It shows the same two keys.
  4. 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.

AI agent or LLM? Read this page as markdown

On this page