Revenue
Stripe, Lemon Squeezy, Polar, Paddle and Dodo. Paste one key, and every sale is credited to the visit that earned it — renewals included.
Revenue is the thing trckable has that the other free, self-hostable analytics do not. It is not a "revenue event" you send from the browser: it is the provider's own signed webhooks, an idempotent ledger, and reconciliation against the provider's API every six hours.
Setting it up
Settings → Payments → choose a provider → paste a restricted API key (read access, plus webhook endpoints). trckable then creates the webhook endpoint itself, with a fixed event list (and a pinned API version where the provider versions its API), and stores the signing secret encrypted.
Manual webhook setup is still there if you would rather not hand over a key: trckable gives you the URL and you paste the signing secret back.

Any other checkout
Not on Stripe, Lemon Squeezy, Polar, Paddle or Dodo? Settings → Payments → Anything else gives you a webhook URL and a signing secret, and your own code posts each sale to it, whatever took the money: a shop, another gateway, a checkout you wrote.
POST /webhooks/custom/<connection>
Trckable-Timestamp: 1758579600
Trckable-Signature: v1=<hex HMAC-SHA256 of "<timestamp>.<body>", keyed with the secret>
{"id": "evt_1", "type": "payment", "at": "2026-09-22T10:00:00Z",
"payment": {"id": "ord_1", "amount": 4900, "tax": 800, "currency": "EUR",
"kind": "one_time", "email": "them@company.com",
"visitor": "<the trckable_vid cookie's value>"}}typeispayment,refundordispute, with a matchingpayment,refund(id,payment_id,amount,currency) ordisputeobject (the same, plusstatus:open,wonorlost).- Amounts are in minor units (cents), tax included; revenue is amount minus
tax, as for every provider.
kindisone_time,subscription(a first payment) orrenewal, withcustomer_idandsubscription_idso renewals follow the first visit. - Times may be RFC 3339, unix seconds or milliseconds. The timestamp header must be within five minutes of the server's clock.
- The same
idsent twice is one event, and events may arrive in any order."test": truekeeps a payment out of the real numbers.
Connecting a sale to a visit
In order of preference:
- Checkout metadata. The browser script adds the visitor id to hosted checkout links when they are clicked — Stripe Payment Links, Lemon Squeezy, Polar and Dodo links need nothing at all.
- Checkouts you create on your server. Pass the visitor through:
import { getIds, checkoutFields } from 'trckable/server'
const ids = getIds(request.headers.get('cookie'))
await stripe.checkout.sessions.create({ ...params, ...checkoutFields('stripe', ids, 'subscription') })Paddle runs in the browser: Paddle.Checkout.open({ items, customData: { trckable_vid } })
with the value of the trckable_vid cookie.
- The customer graph. Once a customer is linked, their renewals follow the visit that won them, forever.
Attribution
A sale belongs to the visitor's last non-direct visit within 90 days before it. A renewal belongs to the visit that started the subscription, not to whatever they happened to be doing the day the card was charged.
First-touch is a switch in the dashboard: the visit that found them rather than the one that closed them. It changes who gets the credit, never the total.
Attribution is resolved when you ask, not when the payment arrives. That is why a refund that arrives before its payment, or a webhook replayed out of order, still ends up right.
What "revenue" means here
Amount paid, excluding tax, before fees, converted at the European Central Bank's rate on the day of the payment.
- Refunds are recorded per refund id. A cumulative
amount_refundedfield is never summed, so a partial refund followed by another is not counted twice. - Disputes are recorded as opened, won or lost.
- Test and sandbox payments are kept separate and excluded from your real numbers. There is a toggle to look at them.
Reconciliation
Webhooks get lost. Lemon Squeezy, for instance, retries three times over about two and a half minutes and then gives up. Every six hours trckable pulls recent payments from each provider's API and fills anything missing, through the same inbox and the same idempotent path — so a dropped webhook is a delay, not a hole.
trckabled payments list # connections and their health
trckabled payments sync [site] [days] # pull now
trckabled payments reprocess <site> # rebuild the ledger from stored webhooksThe ledger can always be rebuilt, because every webhook is stored raw and fsynced before it is acknowledged.
What you see
Revenue, conversion and revenue-per-visitor next to your traffic; a revenue strip under the chart; revenue and conversion columns on every breakdown row; and Top earners — the sources, pages and campaigns that actually pay.
Hover a source and the money trail highlights where those visitors went and what they paid.
The webhook inbox holds personal data
Raw provider payloads contain your customers' emails and addresses. They are on your own server and they make the rebuild possible, so they are kept: the retention setting deletes old visits, not these. Erasing a person (Data & privacy → Answer a data request) removes their payloads, and deleting the site removes all of them. You should know they are there, which is why the privacy paragraph mentions them.