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.

In subscription mode the session has no PaymentIntent, and Stripe charges the first invoice of the subscription. FounderHQ gives that first payment the session's metadata, or the metadata that Stripe copies onto the subscription. Stripe sends the checkout, the invoice, and the two money events in any order, often in the same second. FounderHQ gives the payment and the subscription the same owner in every order. Keep these events enabled, with the rest of the list of events: checkout.session.completed, customer.subscription.created, invoice.paid, and the two money events payment_intent.succeeded and invoice_payment.paid. In API version 2025-05-28.basil, only invoice_payment.paid connects a payment to its invoice and subscription.

Billing for an account? Add the server-issued fhq_account_context to attribution before you create the session, on both metadata and subscription_data.metadata. See Which account gets a payment.

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