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

# Stripe Revenue Analytics Integration

> Connect Stripe to tinyanalytics with a restricted key, measure verified charges, refunds, and lost disputes, and attribute each payment to the channel that won the customer.

The Stripe revenue analytics integration brings server-verified charges, refunds, and lost disputes into your Revenue report. Connecting the key is step one; to see **which channel, campaign, or country produced each sale**, you also add one line of metadata to the payments you create — see [how to attribute Stripe revenue to a marketing channel](#how-do-i-attribute-stripe-revenue-to-a-marketing-channel).

## Before you connect

Create a Stripe **restricted key** that starts with `rk_live_` or `rk_test_`. Grant only:

* **Webhook Endpoints: Write**
* **Charges: Read**
* **Refunds: Read**

Test-mode restricted keys are supported.

<Warning>
  Never paste a Stripe secret key (`sk_…`) into tinyanalytics. The integration rejects secret keys. Use a least-privilege restricted key and keep it private.
</Warning>

## Connect Stripe

<Steps>
  <Step title="Open the integration">
    In your site, open **Settings → Integrations**, find **Stripe**, and select **Connect**.
  </Step>

  <Step title="Enter the restricted key">
    Paste the restricted key you created in Stripe. Turn on **Email fallback** if you want tinyanalytics to match a charge's customer email to an already identified user when no analytics ID is present. You can switch this on or off at any time after connecting.
  </Step>

  <Step title="Save the connection">
    tinyanalytics validates the key and creates the webhook automatically when the key has **Webhook Endpoints: Write** access.
  </Step>
</Steps>

Connecting Stripe also adds `stripe.com` to the site's [referrer exclusions](/docs/exclude-traffic-from-analytics#how-do-i-stop-a-referrer-from-showing-up-as-a-referral), so a return from Stripe does not start a new acquisition session.

## Set up the webhook manually

If the restricted key cannot create webhook endpoints, tinyanalytics shows a webhook URL and accepts a signing secret instead:

<Steps>
  <Step title="Create the Stripe webhook">
    In Stripe, open **Developers → Webhooks**, add the URL shown by tinyanalytics, and listen for `charge.succeeded`, `refund.created`, `refund.failed`, and `charge.dispute.closed`.
  </Step>

  <Step title="Copy the signing secret">
    Reveal the new endpoint's signing secret in Stripe. It starts with `whsec_`.
  </Step>

  <Step title="Finish the connection">
    Paste the signing secret into tinyanalytics and save.
  </Step>
</Steps>

## How do I attribute Stripe revenue to a marketing channel?

Write the visitor's identified user ID into Stripe metadata as `ta_identified_user_id` when you create the payment. A Stripe charge arrives on your server with no knowledge of the browser that caused it, so this ID is the link tinyanalytics uses to credit the sale to the visitor's entry channel, campaign, referrer, and country. Payments with no link are reported under `(no session)`.

You can find the same snippet in the app under **Settings → Integrations → Stripe → Attribution**.

<Steps>
  <Step title="Identify signed-in visitors">
    Call [`identify()`](/docs/user-identification-analytics) with your own user ID after login or signup. Pass the `email` trait too if you plan to use email fallback for guest checkouts:

    ```js theme={null}
    window.tinyanalytics.identify('user_12345', { email: 'ada@example.com' })
    ```

    The ID you choose here is the attribution key. It is stored in the browser's `localStorage`, so it survives reloads and is still available when the visitor reaches checkout.
  </Step>

  <Step title="Read the ID on your checkout page">
    `getUserId()` returns the identified ID, or `null` for a visitor who has not been identified. Send it to your backend alongside whatever your checkout call already posts:

    ```js theme={null}
    const taUserId = window.tinyanalytics.getUserId() // null until identify() has run

    await fetch('/api/create-checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ plan: 'pro', taUserId })
    })
    ```

    <Warning>
      Read the ID at checkout time — in your click handler or on page load — not in a synchronous script in `<head>`. Before the tracking script finishes loading, `getUserId()` returns `null`.
    </Warning>
  </Step>

  <Step title="Set the metadata where you create the payment">
    Add `ta_identified_user_id` to the Stripe call your backend already makes. Use the pattern that matches your integration:

    <CodeGroup>
      ```js Checkout (one-time) theme={null}
      const session = await stripe.checkout.sessions.create({
        mode: 'payment',
        line_items: [/* … */],
        payment_intent_data: {
          metadata: taUserId ? { ta_identified_user_id: taUserId } : {}
        },
        success_url: 'https://example.com/thanks',
        cancel_url: 'https://example.com/pricing'
      })
      ```

      ```js Checkout (subscription) theme={null}
      const session = await stripe.checkout.sessions.create({
        mode: 'subscription',
        line_items: [/* … */],
        subscription_data: {
          metadata: taUserId ? { ta_identified_user_id: taUserId } : {}
        },
        success_url: 'https://example.com/thanks',
        cancel_url: 'https://example.com/pricing'
      })
      ```

      ```js PaymentIntent theme={null}
      const paymentIntent = await stripe.paymentIntents.create({
        amount: 8900,
        currency: 'usd',
        metadata: taUserId ? { ta_identified_user_id: taUserId } : {}
      })
      ```
    </CodeGroup>

    Omit the key when the ID is `null` rather than writing an empty string. An empty value is not a link, and it stops the email fallback from running.
  </Step>

  <Step title="Turn on email fallback for guest checkouts (optional)">
    In **Settings → Integrations → Stripe**, switch on **Email fallback**. tinyanalytics then matches the charge's email against your identified users when the payment carries no `ta_*` metadata at all. It matches an identified user whose ID *is* that email address, or who has a matching `email` trait from `identify()`.

    The email is used only during matching and is never written to your analytics events.
  </Step>
</Steps>

### Which metadata keys does tinyanalytics read?

tinyanalytics looks for these keys on the **charge, payment intent, and Checkout Session**, in that order. The first one it finds wins, so setting the metadata in one place is enough.

| Key                     | What it links to                    | How to get the value                                                          |
| ----------------------- | ----------------------------------- | ----------------------------------------------------------------------------- |
| `ta_identified_user_id` | The user you passed to `identify()` | `window.tinyanalytics.getUserId()` — **use this one**                         |
| `ta_session_id`         | A single analytics session          | Not available in the browser; the tracking script does not expose session IDs |
| `ta_user_id`            | The cookieless visitor hash         | Not available in the browser; the hash is derived server-side                 |

<Note>
  `ta_session_id` and `ta_user_id` are still honored when a payment carries them, but the tracking script never hands those values to your page. `ta_identified_user_id` is the key to use.
</Note>

## What appears in the Revenue report?

The report separates revenue into three tabs:

| Tab               | Contains                                                                                      |
| ----------------- | --------------------------------------------------------------------------------------------- |
| **All**           | client revenue events and Stripe revenue together                                             |
| **Client events** | revenue sent with [`event()`](/docs/revenue-analytics#choose-a-revenue-source) or the Tracking API |
| **Stripe**        | server-verified charges, refunds, and lost disputes                                           |

Stripe charges add revenue. Refunds and lost disputes subtract revenue. A failed refund restores the amount that an earlier refund event subtracted.

The Stripe view includes **Total Revenue, Net Revenue, Refunds, Avg. Order Value, Paying Sessions,** and **Orders**. Net revenue subtracts refunds and lost disputes; it does not subtract Stripe fees, taxes, or other accounting costs.

<Warning>
  tinyanalytics does not deduplicate a purchase reported by both your client event and Stripe. If both sources record the same sale, **All** counts it twice. Use the **Stripe** tab as your source of truth, or stop sending the duplicate client revenue event.
</Warning>

## How does tinyanalytics decide which channel gets the credit?

Every Stripe payment resolves in this order, and the first match wins:

1. **Linked to a session** — the payment is credited to that session's entry channel, campaign, referrer, and country.
2. **Linked to an identified user** — the payment is credited to the entry of that person's most recent session **at or before the payment time**. This is last-touch attribution within your reporting window: the visit that immediately preceded the sale gets the credit.
3. **No link** — the payment is reported under `(no session)`.

tinyanalytics never guesses. It does not match payments to visits by timing, amount, or any other heuristic, because a confidently wrong channel is worse than an honest `(no session)`.

### Tag one payment per customer, not every payment

Links are stored against the **Stripe customer**, so a customer only needs to be tagged once. Later payments from that customer inherit the link automatically — which means **subscription renewals attribute without any repeated work**. Tag the subscription at signup and every renewal invoice lands on the channel that won the original sale.

Refunds and lost disputes inherit their original charge's customer, so a negative adjustment subtracts from the same channel the sale was credited to.

### Links are applied when reports are read

Attribution is resolved at report time, not frozen at payment time. If you add the metadata to your checkout today, the customers you tag from now on attribute correctly — and their other payments follow the same link the next time the report loads. You do not need to reprocess anything in Stripe.

<Note>
  Stripe data starts when you connect the integration. tinyanalytics does not backfill historical payments. Disconnecting stops new Stripe events but keeps the revenue already collected.
</Note>

## Why does my Stripe revenue show as (no session)?

`(no session)` means the payment reached tinyanalytics with no link to a visitor, so there is no channel to credit it to. It is a real bucket, not an error. Work through these causes in order:

| Cause                                    | Fix                                                                                                                                             |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| The payment carries no `ta_*` metadata   | Add `ta_identified_user_id` where you create the payment — see [the setup steps](#how-do-i-attribute-stripe-revenue-to-a-marketing-channel)     |
| `getUserId()` returned `null`            | Confirm `identify()` runs after login, and that you read the ID after the tracking script loads                                                 |
| The buyer checked out as a guest         | Turn on **Email fallback**, and pass an `email` trait when you call `identify()`                                                                |
| The metadata was added recently          | A customer's earlier payments attribute as soon as that customer is tagged once; payments from customers who are never tagged stay unattributed |
| The buyer never visited the tracked site | Nothing to attribute — an offline or manually created charge has no session behind it                                                           |

## Where does Stripe revenue apply?

Stripe revenue appears in the Revenue report and in `scoped_stripe_revenue` queries. [Goals](/docs/conversion-goal-tracking), [funnels](/docs/funnel-analytics), [experiments](/docs/ab-testing-analytics), and [revenue alerts](/docs/analytics-alerts) currently use client revenue events only.

## Frequently asked questions

<AccordionGroup>
  <Accordion title="Can I connect Stripe with a test-mode key?">
    Yes. Both `rk_test_…` and `rk_live_…` restricted keys are supported. The connection status shows whether the key is in test or live mode.
  </Accordion>

  <Accordion title="Does connecting Stripe import my old payments?">
    No. Collection starts when the connection is active; tinyanalytics does not backfill historical Stripe payments.
  </Accordion>

  <Accordion title="What happens when I disconnect Stripe?">
    New Stripe events stop entering tinyanalytics, while the revenue already collected remains in your reports.
  </Accordion>

  <Accordion title="Does net revenue subtract Stripe fees?">
    No. Net revenue is gross charge revenue minus refunds and lost disputes. It does not subtract Stripe fees, taxes, or other accounting costs.
  </Accordion>

  <Accordion title="Do I have to tag every Stripe payment with metadata?">
    No. Links are stored against the Stripe customer, so tagging one payment per customer is enough. Later payments from the same customer — including every subscription renewal — inherit the link automatically.
  </Accordion>

  <Accordion title="Do I need metadata if I already turned on email fallback?">
    Metadata is still the accurate option, and it takes precedence. Email fallback only runs when a payment carries no `ta_*` metadata at all, and it can only match buyers you have already identified by email address or with an `email` trait. Use metadata for signed-in buyers and email fallback to catch guest checkouts.
  </Accordion>

  <Accordion title="Why is Paying Sessions low or zero on the Stripe tab?">
    Paying Sessions counts payments linked to a specific analytics session. Stripe payments attributed through `ta_identified_user_id` credit revenue to a channel, campaign, and country, but they do not add to Paying Sessions. Use Orders as the Stripe purchase count.
  </Accordion>

  <Accordion title="Can I test attribution before going live?">
    Yes. Connect a `rk_test_…` restricted key, identify yourself in the browser, and complete a test checkout with Stripe's `4242 4242 4242 4242` card while your `ta_identified_user_id` metadata is set. The charge appears on the Stripe tab with your entry channel instead of `(no session)`.
  </Accordion>
</AccordionGroup>

## Verify

<Check>
  **The connection is working** when **Settings → Integrations → Stripe** shows a connected key, webhook status, and a recent **Last event** time after a test or live charge. Open **Revenue → Stripe** and confirm the charge appears; issue a test refund if you also want to verify negative adjustments.
</Check>

<Check>
  **Attribution is working** when that charge appears under a real channel — not `(no session)` — in the Revenue report's source, campaign, and country breakdowns. If it lands in `(no session)`, check the payment in Stripe: the charge, its payment intent, or its Checkout Session should carry a non-empty `ta_identified_user_id` in **Metadata**.
</Check>

## Related

<Columns cols={2}>
  <Card title="Identify users" icon="user-check" href="/docs/user-identification-analytics">
    Set the user ID that links Stripe payments to visits.
  </Card>

  <Card title="Revenue analytics" icon="dollar-sign" href="/docs/revenue-analytics">
    Compare client events and Stripe revenue.
  </Card>

  <Card title="Site settings" icon="gear" href="/docs/analytics-site-settings">
    Find the site's Integrations and Revenue settings.
  </Card>

  <Card title="SQL query builder" icon="database" href="/docs/sql-analytics-query-builder">
    Query raw, signed Stripe revenue rows.
  </Card>

  <Card title="How your data is handled" icon="server" href="/docs/resources/analytics-data-handling">
    See what the Stripe integration stores.
  </Card>
</Columns>


## Related topics

- [Revenue Analytics](/docs/revenue-analytics.md)
- [Analytics Site Settings](/docs/analytics-site-settings.md)
- [SQL Analytics API](/docs/api-reference/sql-analytics-api.md)
