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

# Stripe

> Attribute Stripe revenue to AI-referred sessions.

## What this lets you do

Connect Stripe to Traceten and see which AI sources drive paying customers, not just pageviews. When a Stripe charge succeeds, Traceten matches it back to the originating session and attributes the revenue to the correct AI source.

## Before you start

* A Traceten account with at least one site tracked.
* A Stripe account with live mode access, and permission to create restricted API keys on it.
* The Traceten snippet installed and verified on the site where Stripe Checkout runs.
* Workspace admin in Traceten. Connecting and disconnecting a revenue source affects the whole workspace, so only admins can do it.

## Connect Stripe

You connect Stripe by pasting a **restricted API key**. There is no OAuth redirect, and there is no webhook to configure: Traceten creates the webhook endpoint on your Stripe account for you.

<Steps>
  <Step title="Create a restricted key in Stripe">
    Open [Stripe → Developers → API keys](https://dashboard.stripe.com/apikeys) and click **Create restricted key**.

    Give it a name (for example, `Traceten`) and set these permissions:

    | Resource             | Permission |
    | -------------------- | ---------- |
    | Webhook Endpoints    | **Write**  |
    | Account              | Read       |
    | Charges              | Read       |
    | Invoices             | Read       |
    | Checkout Sessions    | Read       |
    | Customers            | Read       |
    | Subscriptions        | Read       |
    | Payment Intents      | Read       |
    | Balance Transactions | Read       |

    Leave everything else set to **None**. Webhook Endpoints needs Write because Traceten creates the endpoint for you. Everything else is read-only.

    Payment Intents and Balance Transactions are not used on every payment, only when a charge comes in in a currency Traceten cannot price directly (for example, an ECB-unlisted currency). In that rare case Traceten reads the charge's Balance Transaction, which carries the amount already converted into your Stripe account's own settlement currency, so the payment is still recorded instead of being dropped. Without these two permissions that fallback simply does not run for you and such a charge is skipped, not attributed incorrectly.

    Click **Create key**, then copy it. It starts with `rk_live_`.
  </Step>

  <Step title="Paste it into Traceten">
    Go to [app.traceten.com/dashboard](https://app.traceten.com/dashboard), open **Settings → Integrations**, and choose the site your checkout runs from at the top of the page.

    Pick **Stripe** from the list of payment providers, then paste the restricted key.

    Click **Connect**.
  </Step>

  <Step title="Confirm the connection">
    Stripe shows as **Connected**, with your account name, your Stripe default currency, and the last four characters of the key.

    Traceten stores the key encrypted. After this point it is never shown again, in the dashboard or in the API, and only those last four characters are readable back.
  </Step>
</Steps>

### What Traceten does with the key

In the same request, Traceten:

1. Reads your account name, account ID and default currency from Stripe.
2. Reads one charge **and one Checkout Session**, to prove the key can actually see the revenue it is being connected for. Both are read, because the import uses both.
3. Creates a webhook endpoint on your Stripe account pointed at Traceten, subscribed to exactly seven events.
4. Stores the key and the webhook signing secret, both encrypted.
5. Starts importing your last 90 days of completed Checkout Sessions and paid invoices.

You can see the endpoint Traceten created under [Stripe → Developers → Webhooks](https://dashboard.stripe.com/webhooks).

These are the seven events:

| Event                           | What Traceten does with it                                                                         |
| ------------------------------- | -------------------------------------------------------------------------------------------------- |
| `checkout.session.completed`    | Attributes a one-time purchase, and records the reference a subscription's first invoice will use. |
| `invoice.payment_succeeded`     | Attributes a subscription charge or a manually issued invoice. Every renewal arrives here.         |
| `charge.succeeded`              | Acknowledged, and no revenue is derived from it. See below.                                        |
| `customer.subscription.created` | Acknowledged. No figure in Traceten is derived from it.                                            |
| `customer.subscription.updated` | Acknowledged. No figure in Traceten is derived from it.                                            |
| `refund.created`                | Subtracts the refunded amount from the source that earned it.                                      |
| `refund.updated`                | Catches a pending refund becoming succeeded.                                                       |

**Subscription revenue arrives on `invoice.payment_succeeded`.** Stripe bills a renewal as an invoice, so every renewal is attributed to the AI source that won the customer, and it carries the subscription id into the per-subscription [lifetime value report](/api/revenue). The two `customer.subscription.*` events are on the endpoint, but no revenue, conversion or lifetime-value figure is computed from them.

`charge.succeeded` is on the endpoint and is deliberately not turned into revenue. A charge behind a Checkout Session or an invoice is already counted by one of the two events above, and counting the charge as well would double that order.

### Refunds and net revenue

Your attributed revenue is **net of refunds**. When Stripe refunds a charge, Traceten subtracts that amount from the AI source the original payment was attributed to.

* **The refund is dated on the day it was issued**, not the day of the original charge. A past day's revenue figure never changes after the fact, and a day on which you refunded more than you took in reads as a negative number.
* **Conversion counts stay gross.** A refunded charge still counts as a conversion. The money is reported separately as a **refunded** figure beside the net one.
* **Partial refunds subtract exactly what Stripe returned.** A Stripe refund states its own amount rather than a running total, so two partial refunds on one charge subtract each amount once.
* **A refund for a charge Traceten never attributed leaves your totals alone.** A charge from before you connected Stripe, or one outside the 90-day import, added no revenue in the first place, so there is nothing to take back off. Traceten holds such a refund and retries it rather than dropping it, in case the charge arrives later.
* **A cancelled subscription is not a refund.** Cancelling stops future charges. The charges already paid were earned and stay in your totals, so cancelling changes no figure in Traceten. Only a refund takes money back off.

### If the key is missing a permission

Traceten checks the key against Stripe before storing anything, and the error names the exact permission Stripe says is missing. Four permissions are proved at connect: Account read, Charges read, **Checkout Sessions read**, and Webhook Endpoints write. A key that can create the endpoint but cannot read charges or Checkout Sessions is refused too, rather than connecting and then failing the import.

The most common cause is leaving **Webhook Endpoints** on Read or None. Traceten will not fall back to a slower method: without that permission there is no webhook, and without a webhook there is no revenue attribution.

Payment Intents and Balance Transactions read are not checked at connect time. They are only exercised the first time a charge arrives in a currency Traceten cannot price directly. If they are missing, that one charge is skipped rather than attributed with a wrong or missing amount; add both permissions and replace the key if you see this.

Nothing is saved and no endpoint is created, so you can add the permission in Stripe and paste the key again. That holds whether this is a first connect or a replacement: when you are **replacing** a key, Traceten creates the new endpoint and stores it before removing the old one, so a refused replacement leaves your existing connection exactly as it was.

### Keys Traceten will not accept

* **Secret keys** (the ones starting `sk_`) are rejected. A secret key can do anything in your Stripe account, including moving money. Traceten does not need that and will not hold it.
* **Publishable keys** (starting `pk_`) are rejected. They cannot read the data Traceten needs.
* **Test-mode restricted keys** (`rk_test_`) are rejected in production. Test-mode charges carry synthetic amounts that would corrupt your revenue totals.

## Connecting Stripe with multiple sites

Stripe connects to **one site**, and each site connects its **own** Stripe account. If you track multiple sites under one account, connect Stripe once per site, choosing the site first.

<Steps>
  <Step title="Open the site you want to attribute">
    In the dashboard, open **Settings → Integrations** and choose the site whose revenue you want to attribute.
  </Step>

  <Step title="Connect that site's Stripe account">
    Paste a restricted key from the Stripe account that bills that site's customers. Traceten pins the
    connection to the site you started from.
  </Step>

  <Step title="Repeat for each site">
    Do the same for every other site, each with the Stripe account that charges its customers.
  </Step>
</Steps>

Two rules follow from this:

* **A Stripe account can be connected to only one site at a time.** Reconnecting a Stripe account that is already linked to another site *moves* the connection to the new site and removes revenue attribution from the first one, with no error, so long as the new site has no Stripe connection of its own. If it already has one, Traceten refuses rather than replacing it; disconnect that one first. If several sites all charge through one Stripe account, only one site can hold that connection at a time; split billing into separate Stripe accounts if each site needs its own revenue attribution.
* **Payments are matched only within the connecting site.** A charge from a site's Stripe account is attributed to sessions on that same site. Revenue never crosses between sites.

See [managing sites](/dashboard/sites#revenue-attribution-is-per-site-with-its-own-stripe-account) for how this fits the wider multi-site model.

## How attribution works

When a payment settles, Stripe sends a `checkout.session.completed` and/or
`invoice.payment_succeeded` webhook to Traceten. Traceten then tries to tie the
payment back to the originating session, in priority order:

1. **`client_reference_id`:** if you passed the visitor's `_traceten_vid`
   cookie through as `client_reference_id`, Traceten matches it to the originating
   session exactly. This field only exists on `checkout.session.completed`.
2. **Payment Link redirect:** if you set the Payment Link's confirmation page to
   your own site with `?session_id={CHECKOUT_SESSION_ID}`, the Traceten snippet on
   that page reports the Checkout Session id and Traceten matches it to the browser
   that completed the purchase. See below.
3. **Email hash:** if the buyer's session called `traceten.identify({ email })`,
   Traceten matches the payment's customer email (hashed, never stored) to that
   visitor. This is the only path available to invoice-only and subscription-renewal
   payments, which never carry a `client_reference_id`.
4. **No match:** the payment is still recorded so revenue totals stay complete,
   but it shows as **unattributed** (no AI source).

To get an exact match, give Traceten the visitor id. That's documented below.

<Note>
  The consumer also accepts `metadata.traceten_session_id`, checked before `client_reference_id` if
  present. It is intentionally not documented as something to send: it pins to whichever session was
  live when the Checkout Session was created, which skips the visitor-id match's own resolution,
  the one that already finds the visitor's most recent **AI-referred** session even when a more
  recent non-AI session exists. For a returning visitor, passing session\_id can attribute to the
  wrong (or no) source in exactly the case this product exists to catch. `client_reference_id` alone
  is both sufficient and the better match.
</Note>

## Payment Links: attribute without writing code

A bare Stripe Payment Link never goes through your server, so there is nowhere to set `client_reference_id`. Instead, have Stripe hand the Checkout Session id back to your own site after payment, and let the snippet do the rest.

<Steps>
  <Step title="Open the Payment Link in Stripe">
    Stripe dashboard, **Product catalogue → Payment Links → your link → Edit**.
  </Step>

  <Step title="Set the confirmation page to your own site">
    Under **After payment**, choose **Don't show confirmation page** and pick **Redirect to your website** (Stripe labels this "Confirmation page"). Enter a page on your site that has the Traceten snippet installed, with the session parameter appended:

    ```
    https://yourdomain.com/thanks?session_id={CHECKOUT_SESSION_ID}
    ```

    `{CHECKOUT_SESSION_ID}` is Stripe's own template variable. Stripe replaces it with the real session id when it redirects the buyer. Type it exactly as shown, braces included.
  </Step>

  <Step title="Save and buy something through the link">
    Complete a real purchase. The buyer lands on your thank-you page, the snippet reads `session_id` out of the URL, and the payment appears against its AI source in **Revenue**, usually within a minute or two.
  </Step>
</Steps>

Things worth knowing:

* **You change nothing in your code.** The only edit is the redirect URL in Stripe.
* **Stripe must be connected to Traceten.** Traceten ignores a thank-you-page report for a site whose Stripe account is not connected, and does not hold it until you connect. A report can also be discarded for up to a minute after you connect.
* **The order of events does not matter.** Stripe's webhook usually reaches Traceten before the buyer's browser finishes redirecting. When that happens the payment is recorded first and re-attributed the moment the browser reports in, on the original payment's own date.
* **The thank-you page must load within 30 minutes of payment.** Traceten pairs a report with a payment only when the two are no more than 30 minutes apart. Stripe's redirect happens straight after payment, so this only matters for a thank-you page opened again much later.
* **The thank-you page needs the snippet.** If the redirect points somewhere the snippet is not installed, nothing is reported and the payment stays unattributed.
* **Payment Links with "Create invoices" turned on work too.** Stripe then sends both the checkout and an invoice for one payment. Traceten records the revenue once, against whichever event reaches it first, and matches the thank-you page report to that record either way. If both events are processed at the same moment, the report can miss the pairing and the payment keeps whatever match it already had.
* **One-time payments are covered in both directions; subscriptions only when the browser gets there first.** A subscription's revenue is recorded against the invoice rather than the checkout, and the invoice carries no Checkout Session id, so there is nothing to match a late redirect against. For subscription Payment Links, pass `client_reference_id` from your own checkout flow where you can.

This path matches a browser, not a person. A buyer who completes checkout on their phone and opens the confirmation link on a laptop is not matched, and neither is a confirmation URL forwarded to somebody else. `client_reference_id`, where you can use it, is exact.

## Guest checkout: pass the visitor ID via `client_reference_id`

Stripe Checkout, Payment Links, and Pricing Tables capture the buyer's email for the first time **inside** Stripe's hosted page. By then the visitor session has already ended, so there is no email on your domain to match against. The fix is to pass the visitor's pseudonymous Traceten identifier through to Stripe as `client_reference_id` when you create the Checkout Session. Stripe surfaces the value on the `checkout.session.completed` event, and Traceten matches it back to the originating AI session exactly, with no email required.

<Tip>
  In the dashboard, open **Settings → Integrations**, pick **Stripe** and click **Set up with AI**.
  It copies a prompt for your coding assistant that wires the visitor ID into your checkout. The
  same card shows this site's **Visitor cookie name**.
</Tip>

The robust way to get the value is `window.traceten.getVisitorId()`, called client-side and forwarded to whatever creates the Checkout Session:

```typescript theme={null}
// Client-side: read the visitor id and send it to your own backend
const visitorId = window.traceten.getVisitorId(); // null before the first pageview resolves
```

```typescript theme={null}
// Server-side: create a Stripe Checkout Session, using the value your client sent
const session = await stripe.checkout.sessions.create({
  // ... your checkout config
  client_reference_id: visitorIdFromClient ?? null,
});
```

If you'd rather not change your client-side request shape, you can read the cookie directly server-side instead:

```typescript theme={null}
// Server-side: read whatever cookie name Sites → Settings → Cookies shows for this site
const visitorId = req.cookies["_traceten_vid"] ?? null;
const session = await stripe.checkout.sessions.create({
  // ... your checkout config
  client_reference_id: visitorId,
});
```

<Warning>
  Do not hardcode this cookie name. It is per-site and only correct on your configured domain (not
  on localhost). Find the real one under **Sites → Settings → Cookies → Visitor cookie
  name**.
</Warning>

Either path is the most reliable attribution method for guest checkouts. Traceten prefers it over email matching whenever it is present: the visitor ID is an exact identifier Traceten generated, while email matching is fuzzy (typos, capitalisation, multiple emails per visitor). No personal data leaves your server on this path: `client_reference_id` carries only the pseudonymous identifier, which is not PII.

<Note>
  `client_reference_id` must be 200 characters or fewer (a Stripe limit). The `_traceten_vid` cookie
  is a UUID and is well within this limit. If you already use `client_reference_id` for your own
  order number, attribution falls back to the email-matching path automatically.
</Note>

<Note>
  `client_reference_id` matching covers both **one-time Checkout** (Stripe Checkout, Payment Links,
  and Pricing Tables in `mode: 'payment'`, the guest-checkout case above) and **subscription
  Checkout** (`mode: 'subscription'`). For subscriptions, Traceten records the reference when the
  Checkout completes and applies it to the subscription's first invoice, so the first charge is
  attributed exactly once. Set `client_reference_id` the same way for both. If a subscription has no
  reference (for example, created directly via the Stripe API or dashboard rather than Checkout),
  Traceten falls back to email-based matching automatically. Attribution of recurring renewal
  charges is tracked separately.
</Note>

## Invoice and subscription payments: call `identify()`

`invoice.payment_succeeded` events (subscription renewals and manually-issued
invoices) **never carry a `client_reference_id`** (it is a Checkout Session
field). For these payments the only way Traceten can match the revenue to a
session is the **email hash**: call `traceten.identify({ email })` during the
visitor's session, using the same email the customer pays with.

```javascript theme={null}
// Client-side: call this as soon as you know the customer's email
// (e.g. after login, or after they submit a lead form).
traceten.identify({ email: "customer@example.com" });
```

Traceten hashes the email before it ever touches storage. The raw email is
never logged or stored. When the renewal invoice later settles, Traceten hashes
the payment's customer email the same way and matches the two.

Two things are worth being precise about, because "hashed" can imply more than
it should.

**The address does reach Traceten.** `traceten.identify()` sends the email to our
edge over HTTPS; the hash is computed there, on our servers, not in the browser.
We clear the plaintext from memory immediately and never write it to storage or
to a log. But it is transmitted, and a privacy notice that implies otherwise
would be wrong.

**There are two digests, and only one is keyed.** Every payment is matched on
**SHA-256 of the email plus a per-site salt**. Alongside it we compute an
**HMAC-SHA-256 under a per-site key**, which is what `identify()` produces and
what is tried first. A salt and a key are different constructions; only the
second is keyed. The key is generated for every site when the site is created,
so this does not depend on you having called `identify()` before.

<Note>
  Without an `identify()` call, a renewal invoice has no session to match and is recorded as
  **unattributed**. The subscription's **first** charge is still attributed exactly when the
  original Checkout passed `client_reference_id`; it is the recurring renewals that need
  `identify()` to stay attributed.
</Note>

## Testing your setup

For the fastest reliable test path, use a **Stripe Payment Link**. The Traceten
snippet auto-appends the visitor's `_traceten_vid` value to Payment Link URLs
on your site, so `client_reference_id` is populated with no server code. Complete
a purchase through a Payment Link and the resulting revenue row should attribute
to the originating AI session.

The purchase arrives on the webhook endpoint Traceten created during connect,
so it should show up within a minute or so of completing checkout.

If you create Checkout Sessions **server-side** instead, you must pass the cookie
yourself. See the [guest-checkout section](#guest-checkout-pass-the-visitor-id-via-client_reference_id)
above. A server-created session that does not set `client_reference_id` (and whose
visitor never called `identify()`) will record as unattributed.

You can confirm coverage on the dashboard's **Attribution coverage** card, which
shows what share of revenue Traceten tied back to a session and how much is still
unattributed.

## Backfilling historical orders

A webhook endpoint only delivers events created after it exists, so anything that happened before you connected has to be imported. After connecting Stripe, Traceten automatically backfills the last 90 days of:

* **completed Checkout Sessions:** one-time purchases, Payment Links and subscription checkouts. These carry `client_reference_id`, so a historical order can be attributed to the exact AI session that produced it.
* **paid invoices:** subscription charges and manually issued invoices, attributed by email hash.

Orders older than 90 days are not backfilled.

Backfill status is shown on **Settings → Integrations** for the site. It typically completes within a few hours of connecting.

## Rotating the key

To replace the key, create a new restricted key in Stripe with the same permissions, then pick Stripe on **Settings → Integrations** for that site, click **Replace key**, and paste it in. Traceten creates the replacement endpoint first, stores it, and only then deletes the one the old key created, so nothing stops delivering in between and stale endpoints do not accumulate on your Stripe account. For a moment both endpoints exist and a payment may be delivered twice; Traceten ignores the duplicate. Your imported history and your backfill are kept; you do not need to disconnect first.

Delete the old key in Stripe once the new one shows as connected.

## Using a processor we do not integrate with

Stripe, Shopify, [Lemon Squeezy](/integrations/lemonsqueezy), [Polar](/integrations/polar) and [Paddle](/integrations/paddle) have built-in integrations. [Dodo](/integrations/dodo) has a documented path with no server of yours required. For anything else, including Podia, Kajabi and your own checkout, send payments to the [Payment API](/api/payments) instead.

Do not do both for the same processor. If you connect Stripe here **and** post Stripe payments to the Payment API, every payment is recorded twice and your revenue is overstated. The two paths deduplicate on different keys and cannot see each other. See [avoiding duplicate payments](/api/payments#avoiding-duplicate-payments).

## Disconnecting Stripe

To remove the Stripe integration:

1. Open **Settings → Integrations** in the dashboard and choose the site.
2. Pick **Stripe**, then click **Disconnect** and confirm.
3. Delete the restricted key in your [Stripe dashboard](https://dashboard.stripe.com/apikeys).

Disconnecting deletes the webhook endpoint Traceten created on your Stripe account, then removes the stored key. If you already deleted or revoked the key in Stripe, the disconnect still completes and the stored key is still removed.

Disconnecting stops future charge attribution. Historical attributed charges remain in your Traceten data.
