# Checkout metadata and payment links (https://www.getfounderhq.com/docs/analytics/revenue/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

```text
fhq_anonymous_id   the visitor, across sessions
fhq_session_id     the visit that led to the purchase
```

Read them in the browser:

```ts
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:

```ts
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](/analytics/revenue/connect-stripe/checkout-api) | Session `metadata`, plus `subscription_data.metadata` for subscriptions or `payment_intent_data.metadata` for one-time payments |
| [Stripe PaymentIntent](/analytics/revenue/connect-stripe/payment-intent-api) | The PaymentIntent's `metadata` |
| [Stripe Payment Link or Pricing Table](/analytics/revenue/connect-stripe/payment-links) | A minted token in `client_reference_id` |
| [Dodo](/analytics/revenue/connect-dodo) | The `metadata` object at checkout creation |
| [Mobile stores](/analytics/revenue/mobile-purchase-claims) | 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:

```ts
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,
FounderHQ fills in Introduced by and Closed by.
