> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clickbase.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Track revenue

> There are two kinds of revenue in Clickbase, and the difference matters.

There are two kinds of revenue in Clickbase, and the difference matters.

* **Verified revenue** is money we confirmed with your payment provider. It comes from your **Stripe** connection or the **Payments API**, and it drives the numbers you bill and forecast on: the Revenue page, MRR, and the payments ledger.
* **Reported revenue** is a signal your own site sends from the browser. It is great for attribution — *which visits turn into sales* — but anyone can fire it from their console, so it is never treated as money. It shows up against your **goals**, never on the Revenue page.

Pick the surface that fits what you need. Attribution and funnels? Report it from the browser. Accounting, MRR, invoices? Send it verified from your server.

## Report revenue from the browser

The tracker gives you three ways to report revenue. All amounts are **integer minor units** — cents, not dollars — so `$49.90` is `4990`.

```js theme={null}
// A one-off sale.
clickbase.sale('purchase', { amount: 4990, currency: 'USD' });

// A recurring charge.
clickbase.subscription('pro-plan', { amount: 2900, currency: 'USD' });

// Any named event can carry revenue too.
clickbase.event('upgrade', { revenue: { amount: 2000, currency: 'USD' } });
```

The first argument is the **goal name**. For the amount to count, create a matching **revenue goal** with that name and currency in your site's goals — the event still records without it, just without the revenue attached. This mirrors how handles revenue events.

`sale()` and `subscription()` read the same way but tell a clearer story than a generic event: a sale is a first-class thing, not "just another custom event." They record identically; the names are for you.

**You cannot fake verified revenue this way.** The browser refuses to send the reserved Stripe lifecycle names — `payment`, `free_trial`, `trial_started`, `trial_converted`, `subscription_started`, `subscription_renewed`, and the other `subscription_*` events — the names that only Stripe and the Payments API are allowed to write. After Connect they appear as filterable goals automatically. A spoofed sale can inflate a custom goal's reported number, but it can never reach your MRR or your Revenue page.

## Send verified revenue from your server

Verified revenue comes from one of two places.

* **Connect Stripe** on your site's Revenue settings. We import your full payment history — charges, refunds, disputes, subscriptions — and keep it live from then on. No code.
* **Call the Payments API** from your own backend when you record a sale in your system. Authenticate with a **Developers personal access token** (`Authorization: Bearer {token}`) — the same Passport token as the rest of the API, not the tracking key. Create one on **Settings → Developers**; optionally limit it to specific sites. Requires workspace admin and an active subscription. See [Tracking & events → Payments](/api/tracking) for the full request shape.

## Attribute a Stripe checkout to a visitor

A Stripe Checkout Session is created on your server, so it has no idea which visit it belongs to. Bridge the two: read the visitor id (and optionally the session id) from the SDK in the browser, send them to your server, and attach them to the session.

```js theme={null}
// 1. In the browser, ask the SDK for the visitor id — never read the
//    cookie yourself — and send it to your server at checkout.
const visitorId = window.clickbase?.getVisitorId?.() ?? null;
// Optional companion: same-visit session id from the SDK (`_cb_sid`).
// Visitor alone is enough for attribution; omit session if null.
const sessionId = window.clickbase?.getSessionId?.() ?? undefined;

const clickbaseMetadata = {
  clickbase_visitor_id: visitorId,
  ...(sessionId ? { clickbase_session_id: sessionId } : {}),
};

// 2. On your server, attach them to the Checkout Session.
// Subscription Checkout — Stripe rejects payment_intent_data in this mode.
const session = await stripe.checkout.sessions.create({
  // ...line_items, success_url
  mode: 'subscription',
  metadata: clickbaseMetadata,
  subscription_data: {
    metadata: clickbaseMetadata,
  },
});

// One-time payment Checkout — stamp the PaymentIntent so
// payment_intent.succeeded still attributes when it races ahead of
// checkout.session.completed. Do NOT pass subscription_data here.
// const session = await stripe.checkout.sessions.create({
//   mode: 'payment',
//   metadata: clickbaseMetadata,
//   payment_intent_data: { metadata: clickbaseMetadata },
// });
```

`getVisitorId()` returns the current visitor id, or `null` before the tracker has loaded or for a visitor with no cookie. When the payment lands, we match it back to the visit that earned it. Without the id, the payment still counts — it just lands under **Unattributed**. The same id on `subscription_data.metadata` stamps first-touch attribution onto the subscription for MRR filters. Always optional-chain `window.clickbase?.getVisitorId?.()` / `getSessionId?.()` so an older CDN tracker cannot throw before the method exists.

`getSessionId()` returns the current analytics session id (`_cb_sid`), or `null` before load / in cookieless mode. `clickbase_session_id` is optional metadata — when present we store it on the payment and subscription; money filters still join on `visitor_id`. The same metadata keys work on PaymentIntent creation for Elements / mobile SDKs.

## Where each one shows up

| You sent                                           | Trusted as | Appears on                         |
| -------------------------------------------------- | ---------- | ---------------------------------- |
| `sale()` / `subscription()` / `event({ revenue })` | Reported   | Goals                              |
| Stripe / Payments API                              | Verified   | Revenue page, MRR, payments ledger |
