Revenue attribution
Connect Stripe or Polar and see which pages, channels and campaigns actually earn.
What revenue attribution does
Connect Stripe or Polar and your payments, refunds and fees flow into a Revenue tab, joined to the visits that produced them where a signal exists. You get two things: totals you can trust (net revenue with its parts shown) and, when you opt in, the journey behind each payment, which page, channel and campaign actually earned it. Each site connects its own account, from its Settings → Integrations screen, and the screen walks you through the provider you pick.
Connect Stripe
Open your site's integrations
DashboardSiteSettingsIntegrationsEach site connects its own Stripe account, so open the site you want to measure and go to its Settings.
Connect with a restricted, read-only key
Create a restricted key in Stripe with read access to charges, payment intents, checkout sessions, refunds and disputes, and paste it in. A couple of minutes, no code changes. From then on the Revenue tab fills.
Choose the snapshot payload style
See the totals arrive
Net revenue and its parts appear in the Revenue tab. This much needs nothing from your visitors and no further setup. To tie a payment back to the visit that produced it, turn on attribution next.
Connect Polar
Create an Organization Access Token
PolarSettingsDevelopersNew TokenScope it to the organization whose revenue we should read, with exactly two permissions: Orders: Read and Refunds: Read. Anything else should stay off; we never write to Polar. Copy the token (polar_oat_…, shown once) and paste it into the site's Integrations screen. The connect probe reads Orders, so a token missing that scope is refused on the spot rather than half-working later.
Create the webhook endpoint
PolarSettingsWebhooksAdd endpointConnecting hands you back a webhook URL for this site. Create an endpoint at that address in Polar and select the order, refund, checkout and subscription events, or simply all of them: anything we do not consume is acknowledged and ignored, never an error.
Choose the Raw format
Paste the signing secret back
Reveal the endpoint's signing secret (whsec_…) and paste it into the Integrations screen. Totals already flow from the token alone; the webhook is the low-latency path that lands a payment within seconds instead of on the next sweep.
- Revenue is read pre-tax.Polar is the merchant of record: it collects tax and remits it, so an order's tax-inclusive total includes money you never receive. We record the post-discount, pre-tax figure, the same number Polar's own dashboard reports, so a comparison against a tax-inclusive total will read lower, correctly.
- There are no dispute events. Polar emits none as the merchant of record, so chargebacks stay empty for a Polar connection. That is the provider having nothing to report, not a gap in your data.
- Sandbox is a separate host.A sandbox token does not work against production and vice versa, so objects created in Polar's sandbox will not appear here.
Turn on attribution (opt-in, and it asks something of you)
Revenue totals need nothing from your visitors. Tying a payment back to the visit that produced it is different on both counts: you turn Attributed revenue on when connecting your provider, and the linking itself carries a consent obligation you meet as the controller of your site.
The switch decides whether the link happens at all, not just what the script sends. An order_id that reaches us while it is off, from a cached copy of the script or a request you wrote yourself, is dropped before the event is stored; and the matcher skips your site entirely, so a payment is not linked through your own payments integration either. Conversions are still counted and revenue totals are unaffected; what stops is the journey.
You wire the consent, because only you can see it
We do not hold the hint back on a consent state we cannot see, so wiring this is yours to do. The best place to ask is your own checkout page: consent collected inside Stripe Checkout is consent for taking payment, and the analytics link is a different purpose with a different controller, you.
oa.consent("granted"); // or "denied", which stops collection entirelyLink a payment to its visit
There are two recipes. The first is exact and is what you should reach for; the second is the fallback for when your success page cannot know the order.
oa.conversion("purchase", { order_id: "cs_live_a1B2c3" });order_idis the Stripe Checkout Session (cs_…), PaymentIntent (pi_…) or charge id (ch_…), whichever your success page has. The join is exact, and the transaction and its journey get confidence exact, the strongest the system issues.- The fallback passes the same stable user reference to both sides. Both are hashed with the same site-scoped derivation and joined without either value ever being stored raw, yielding confidence linked: it identifies the person rather than the order. See identify for the rules.
- On Polar the same reference travels as
customer.external_id,metadata.oa_external_idon the checkout, or thereference_ida Polar checkout link propagates, whichever is present, in that order. If you already set external ids for your own system, nothing more is needed. - A conversion is a billable custom event, one unit; nothing about either recipe bills twice.
What the reports show
- Net revenue with its parts printed: gross, refunds, disputes, fees. A flat month of quiet trading and a flat month of heavy refunds look different here, on purpose.
- Gross and net share one chart; the gap between the lines is the cost of doing business.
- Payments the join could not attribute still count in every total; they simply have no journey. There is no IP matching and no time-proximity guessing, by policy.
- Currencies that could not be converted are listed beside the totals rather than silently dropped.
Disconnecting
Disconnect the credential
SettingsIntegrationsDisconnecting stops the flow immediately: no new payments, refunds or disputes are read from that account.
What stays and what stops
The revenue you already collected stays in your history. Only new inflow stops. Reconnecting later picks up roughly the last month on the first pass, the same window attribution re-reads.