# How to track Stripe revenue by traffic source

Source: https://databull.app/guides/track-stripe-revenue-by-traffic-source
Updated: 2026-10-30 · Author: Ahmed Gagan (maker of DataBull)

> Stripe records every payment and nothing about the visit before it. Here is how to get the source into each kind of Stripe checkout, and what to do with it after.

**Short answer:** Stripe doesn't record where a buyer came from, so you have to pass it in. On Payment Links, add `client_reference_id` to the URL. On Checkout Sessions your server creates, set `metadata` (plus `subscription_data.metadata` for renewals). On PaymentIntents, set `metadata`. Store a visitor id there, match it to that visitor's source, and sum revenue per source.

## Why the Stripe dashboard can't tell you

Stripe sees the checkout, not the website before it. A payment carries the amount, the customer, the product and whatever you attached to it. It has no referrer, no landing page and no campaign. The visit that started it happened on your site, maybe days earlier, so the link between the two has to be made by you.

The usual way is to give each visitor an id when they arrive, record their source against that id, and hand the id to Stripe at checkout. When the payment comes back (by webhook or API), you look the id up and credit the source. How you hand it over depends on which kind of Stripe checkout you use.

| Your setup | Where the id goes | Comes back on |
| --- | --- | --- |
| Payment Links (`buy.stripe.com`) | `client_reference_id` URL parameter | The Checkout Session, in `checkout.session.completed` |
| Checkout Sessions created by your server | `metadata`, plus `subscription_data.metadata` or `payment_intent_data.metadata` | The session, then the subscription's invoices or the PaymentIntent |
| Elements / your own PaymentIntents | `metadata` on the PaymentIntent | The PaymentIntent and its charge |

## 1. Payment Links

Payment links are the easiest to start with and the easiest to lose the source on, because nothing runs on your server. Stripe accepts a `client_reference_id` in the link's URL and attaches it to the Checkout Session it creates. It must be letters, numbers, dashes or underscores, up to 200 characters; invalid values are silently dropped.

```
https://buy.stripe.com/abc123?client_reference_id=v_8f3k2m9q
```

Your site adds the visitor's id to the link before the click (a few lines of JavaScript on every `buy.stripe.com` link), and your webhook reads `client_reference_id` from `checkout.session.completed`.

Stripe also accepts `utm_source`, `utm_medium`, `utm_campaign`, `utm_content` and `utm_term` on a payment link. Its docs describe them as being passed on to your after-payment redirect URL, so they only help if the link redirects back to your site and something there records them. A visitor id is more robust: it carries the whole visit, not just the tags on the last link.

## 2. Checkout Sessions created on your server

When your server calls `stripe.checkout.sessions.create`, read the visitor id (from a first-party cookie, for example) and put it in `metadata`. Stripe metadata holds up to 50 keys, with keys up to 40 characters and values up to 500.

One detail catches most people: **metadata does not copy itself to related objects**. Session metadata stays on the session. For a subscription, also pass `subscription_data.metadata`; Stripe then snapshots it onto every invoice the subscription creates (`parent.subscription_details.metadata`), so renewals can be credited too. For one-time payments, `payment_intent_data.metadata` puts it on the PaymentIntent, which is where refunds point.

```
const metadata = { visitor_id: visitorId };

await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: 'price_123', quantity: 1 }],
  metadata,
  subscription_data: { metadata },   // renewals carry it on each invoice
  success_url: 'https://yourapp.com/thanks?session_id={CHECKOUT_SESSION_ID}',
});
```

The `{CHECKOUT_SESSION_ID}` placeholder is worth keeping even when metadata is set: when the buyer lands on your thank-you page with the session id in the URL, the page itself can link that session to the visitor. It's a useful backup for buyers whose cookie was missing.

Full code for capturing UTM tags and reading them back in a webhook is in [UTM tracking with Stripe Checkout](https://databull.app/guides/utm-tracking-stripe-checkout).

## 3. Elements and custom PaymentIntents

If you build the payment form yourself with Stripe Elements, your server creates the PaymentIntent, so add the same `metadata` there. Stripe copies PaymentIntent metadata onto the charge it creates, so it's also visible on the payment in the dashboard.

## Turning ids into revenue by source

With the id on each payment, the rest is bookkeeping, and it's where most homemade setups stop being fun:

- **Which visit gets the credit?** The first visit, the last, or the last one that wasn't Direct? Each answers a different question. See [revenue attribution](https://databull.app/revenue-attribution) for the trade-offs.
- **Renewals** should be credited to the source of the first payment, or a channel that brought a yearly subscriber looks worthless after month one.
- **Refunds** should come off the source that got the sale.
- **Currencies** need converting before you add them up.
- **Missing ids** (blocked scripts, a buyer who switched devices) need a fallback or an honest "unattributed".

## How DataBull does it

DataBull connects to Stripe with a restricted key and does all of the above. Past payments are imported when you connect, and new ones arrive by webhook.

1. **Payment links are tagged for you.** The tracking script adds `client_reference_id` to every `buy.stripe.com` link on your site. Nothing to change.
2. **Server checkouts pass two fields.** Add `databull_visitor_id` and `databull_session_id` (read from DataBull's cookies) as `metadata` and `subscription_data.metadata`. If you're coming from DataFast, `datafast_visitor_id` works too.
3. **The thank-you page is a backup.** Set the success URL to `/thanks?session_id={CHECKOUT_SESSION_ID}` and the script links the session to the visitor who lands there.

Each payment is then credited to a visit with the model you choose per website (last non-direct by default, or first or last touch). Renewals inherit the source of the first payment, refunds come off it, and sales that couldn't be linked say why. The dashboard shows revenue by channel, referrer, campaign, landing page and country, and a [Telegram message](https://databull.app/stripe-sale-notifications-telegram) can name the source of every sale. Polar, Dodo Payments and Creem work the same way. Setup details are in the [revenue docs](https://databull.app/docs/revenue).

Checked in October 2026 against: [Stripe: Payment Links URL parameters and UTM codes](https://docs.stripe.com/payment-links/url-parameters), [Stripe: Metadata (limits, what copies where)](https://docs.stripe.com/metadata), [Stripe API: Create a Checkout Session](https://docs.stripe.com/api/checkout/sessions/create).

## FAQ

### Does Stripe store UTM parameters from a payment link?

Stripe accepts `utm_source`, `utm_medium`, `utm_campaign`, `utm_content` and `utm_term` on payment links, and its docs say they are passed to your after-payment redirect URL. To use them, the link has to redirect back to a page that records them.

### Does Checkout Session metadata reach the subscription?

No, not on its own. Pass the same values in `subscription_data.metadata`. Stripe then copies the subscription's metadata onto each invoice it creates, which is how renewals can be credited to the original source.

### Can I use client_reference_id with Checkout Sessions too?

Yes. It's a single string up to 200 characters on the session. Metadata is more flexible (several keys), and only metadata can be passed on to the subscription or PaymentIntent.

### What if the buyer pays on a different device?

The id from the first device won't reach the checkout, so the sale can't be tied to that visit by id. Matching by email (if the visitor gave it to you earlier) is the usual fallback; otherwise the sale is honestly unattributed.
