Documentation
Revenue

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 purchase

Read 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. Newly issued account context tokens are at most 168 characters, including for 256-character user IDs, and fit the 500-character Stripe and Dodo metadata value limit. Treat tokens as opaque; do not decode or shorten them. Tokens from before this format are no longer accepted; request a new token for each checkout. Historical payments are verified at their occurrence time.

For account billing, request the token on your server for the signed-in payer and the account they are buying for. Add it to the provider metadata there, overriding any browser-supplied account context. Keep it out of browser code and logs. If issuance fails or times out, complete checkout without the token. Before sending metadata, omit an account context longer than your provider's limit and log a warning without the token. What happens to a payment without the token is in Which account gets a payment.

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 chargeWhere the IDs go
Stripe Checkout SessionSession metadata, plus subscription_data.metadata for subscriptions or payment_intent_data.metadata for one-time payments
Stripe PaymentIntentThe PaymentIntent's metadata
Stripe Payment Link or Pricing TableA minted token in client_reference_id
DodoThe metadata object at checkout creation
Mobile storesNot metadata. A UUID in Apple's appAccountToken or Google's obfuscatedExternalAccountId

Which account gets a payment

This section applies when Revenue owner is Organization. You set it under Attribution in Analytics → Settings. With Individual, every payment belongs to the contact who paid, and nothing here changes that.

An account is the company or workspace your customer buys for. FounderHQ lists accounts under Organizations. A payment belongs to one owner: one account, or one contact. FounderHQ never splits a payment or copies it to each member.

FounderHQ checks these in order and stops at the first answer:

#What FounderHQ findsWho gets the payment
1A valid fhq_account_context token on the checkoutThe account in the token
2A subscription or provider customer that already belongs to one accountThat account
3A subscription that FounderHQ already gave to its payerThat contact
4A payer who is a member of exactly one accountThat account
5A payer who is a member of several accounts, or of noneThe contact who paid
6No payer that FounderHQ knowsNobody yet: the payment is unmatched

App Store and Google Play purchases follow the same steps when the app did not prepare the purchase. A prepared purchase, or a purchase claim, names its account exactly, like the token in step 1. A purchase claim that arrives later also replaces an account that FounderHQ chose in step 4. See Mobile purchase claims.

The checkout carries its account

The fhq_account_context token names the account that the payer chose at checkout. Your server requests it for the signed-in payer. FounderHQ checks that the token belongs to that payer and to your brand. A valid token decides: the payment goes to that account, even when the payer is a member of other accounts too.

A token that is expired, altered, or issued for a different payer does not count. FounderHQ then continues with step 2.

2 and 3. The subscription keeps its owner

The first payment decides who owns a subscription. FounderHQ binds the provider's subscription and customer to that owner. Renewals carry no checkout, and the token has expired by then. So each renewal goes to the owner of its subscription, an account or a contact.

A later change to the payer's accounts does not move a subscription. Say the first payment went to the payer because they were a member of two accounts. The renewals stay with the payer after they leave one of them. A subscription that a contact owned before you chose Organization goes through these steps once, at its first renewal after the change. From then on it keeps the owner of that renewal.

A new payment of the same provider customer, outside that subscription, goes to the account that the customer paid for before. When the customer paid only as a contact, FounderHQ continues with step 4.

4 and 5. The payer's accounts

When the payment carries no account, FounderHQ finds the payer: first by fhq_anonymous_id, then by the email address that the provider sends. Then it looks at the accounts the payer was a member of when they paid.

  • Exactly one account. The payment goes to that account.
  • Several accounts. The payment goes to the contact who paid. FounderHQ does not guess between the accounts.
  • No account. The payment goes to the contact who paid.

A payment that goes to the contact is not lost. It appears in the contact's revenue and in your totals, but in no account's revenue.

Only these memberships count:

  • Joins and leaves that your server declares with the Node SDK (accountMembership).
  • A browser or app event that carries a valid account context token for its account.
  • Contacts that your team adds to or removes from an account by hand in FounderHQ.

An account that a browser or app event names without a signed context does not count. It shows where a contact worked, not which accounts they are a member of.

A change by hand wins

When someone on your team adds a contact to an account by hand, that account wins over the accounts your app declares. Your app lists a contact in two accounts, and your team adds them to a third by hand: the contact pays for the third. A contact removed by hand does not count for that account, even when your app still lists them. Two accounts added by hand are again a tie, and the payment goes to the contact.

A change by hand applies to payments made after the change. It does not move payments that FounderHQ already recorded. It does not move a subscription that already has its owner.

Unmatched payments

When FounderHQ cannot find the payer, the payment is unmatched. The checkout had no fhq_anonymous_id that FounderHQ knows, and the provider sent no email that belongs to a contact in your brand. When that contact is identified later, FounderHQ matches the payment again with the steps above.

When the account arrives late

Some providers send the checkout after the payment. For example, Stripe can deliver checkout.session.completed after the payment itself. In subscription mode that payment is the first payment of the session's subscription. Say the payment went to the contact, and the checkout carries a valid token. FounderHQ then moves the payment to the token's account. The subscription, its renewals, and its MRR move with it, including renewals that FounderHQ recorded before the move. The same happens when FounderHQ can check the token only after the payer is identified: the move runs when the identify arrives.

Send the account with the checkout

Steps 4 and 5 are a fallback. They depend on the memberships that FounderHQ has at the time of the payment. Consultants, agencies, and founders with two workspaces pay as a contact. A customer who pays before your app declares their workspace pays as a contact too, and their subscription stays with them.

The token is the only way to name the account exactly. For every checkout that is for an account:

  1. On your server, request the token for the signed-in payer and the account they buy for, with accountContextToken in the Node SDK.
  2. Put it in the checkout metadata as fhq_account_context, next to fhq_anonymous_id.
  3. For subscriptions, put it on the session and on the subscription, as the table above shows. For a Stripe Checkout in subscription mode, that is metadata and subscription_data.metadata. The first invoice's payment takes it from there.

Some payments carry no account: a Dodo payment link, an invoice that you send by hand, a payment that you take in the provider's dashboard, or a checkout that you created without metadata. FounderHQ finds the payer by the email that the provider sends and applies steps 2 to 6.

A Stripe Payment Link or Pricing Table carries an account only through its fhqref_ token. The page must have had an account context when it requested the token. Otherwise the payment follows steps 2 to 6 too.

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 (subscription_data.metadata; FounderHQ reads it for the first invoice), 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. For Dodo account trials, enable both subscription.active and payment.succeeded. FounderHQ binds the account from the first event with valid checkout context: subscription metadata when present, otherwise the zero-total trial payment's metadata and subscription_id. The zero payment books no revenue. Verify which event carries the metadata in a Dodo test-mode checkout before deploying; subscription metadata is not guaranteed by our recorded fixture. Charges after the trial use that binding even after the token expires. A zero-total start without a token binds the subscription to the owner that a payment would get: the payer's one account, or the payer. FounderHQ finds the payer by fhq_anonymous_id or by the email.

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 null when 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, the payment counts for the buyer's channel in the Channel card of Analytics → Overview, under Closed by and Introduced by.

AI agent or LLM? Read this page as markdown

On this page