Checkout metadata and payment links
Attach FounderHQ attribution to checkout metadata and payment links.
A payment carries no attribution on its own. Your checkout has to carry two IDs from the visitor. This page explains the IDs, where to put them, and what to do when the checkout page is not yours.
The two IDs
fhq_anonymous_id the visitor, across sessions
fhq_session_id the visit that led to the purchaseRead them in the browser:
import { founderhq } from "@founderhq/events";
const identity = founderhq.checkoutMetadata();
// { fhq_anonymous_id, fhq_session_id }The browser returns an empty object when the visitor has not granted consent. Treat that as normal and complete the sale without attribution.
If you set an account on the SDK, the same call also returns fhq_account_key
and, when one is issued, fhq_account_context. Pass them through untouched.
Build the same object on your server with the Node SDK:
import { checkoutMetadata } from "@founderhq/events-node";
const attribution = checkoutMetadata({
anonymousId: identity.fhq_anonymous_id,
sessionId: identity.fhq_session_id,
});It throws when either ID is empty, so wrap it if a missing ID must not block the sale.
Where the IDs go
| How you charge | Where the IDs go |
|---|---|
| Stripe Checkout Session | Session metadata, plus subscription_data.metadata for subscriptions or payment_intent_data.metadata for one-time payments |
| Stripe PaymentIntent | The PaymentIntent's metadata |
| Stripe Payment Link or Pricing Table | A minted token in client_reference_id |
| Dodo | The metadata object at checkout creation |
| Mobile stores | Not metadata. A UUID in Apple's appAccountToken or Google's obfuscatedExternalAccountId |
The renewal trap
Session metadata describes one checkout. It does not travel to next year's renewal.
Put the metadata on the object that renews as well: the Stripe subscription for subscriptions, the PaymentIntent for one-time payments. FounderHQ also binds the provider's customer and subscription IDs on the first payment, so a renewal with no metadata at all still resolves to the same contact.
FounderHQ works out a renewal's channel from the contact's touches at the time of the renewal, then freezes it. Nothing later rewrites a payment.
Hosted pages you cannot edit
A Stripe Payment Link runs on Stripe's domain, so there is no place to put metadata. Ask the SDK for a token instead:
const token = await founderhq.paymentLinkToken();- The token starts with
fhqref_and is valid for 30 days. - FounderHQ mints it server-side, so attribution does not depend on the buyer returning to your site.
- It returns
nullwhen consent is denied, when the visitor opted out, or when the page origin is not allowed for your publishable key.
Put it in client_reference_id on a Payment Link, or in the
client-reference-id attribute on a Pricing Table.
Verify
Start a checkout, then open the object in your provider's dashboard. It shows
both keys, or the link URL carries client_reference_id. After payment,
FounderHQ fills in Introduced by and Closed by.