Docs/Track revenue
Track revenue
How a sale reaches Kipstats, how it is counted once and only once, and what is not handled yet.
Two ways in, and one is better
A sale can be reported by the browser, on your success page, or by your server, when your payment provider confirms it. Prefer the server. A browser can be closed before it ever reaches the success page, lose the network, or be a phone that never comes back — and the sale then exists nowhere.
| Browser | Server | |
|---|---|---|
| How | window.kipstats.event('purchase', …) | POST /api/collect/server |
| Needs an open tab | yes | no |
| Attached to a session | yes — you keep the acquisition source | no |
| Replay-safe | only with your own guard | yes, through ref |
Sending both is allowed: when a browser sale and a server sale describe the same payment within the same window, the server one wins and the pair counts once.
The payload
POST https://kipstats.com/api/collect/server
Authorization: Bearer kpi_your_ingest_key
{
"name": "purchase",
"ref": "ch_3U9abcdef",
"occurredAt": "2026-09-17T10:12:00Z",
"data": { "amount": 4900, "currency": "EUR", "plan": "pro" }
}amountin the smallest unit — 900 means 9,00 €, never 9.currencyalways, unless you only ever charge in euros.{ amount: 9900 }on a site selling in złoty is read as 99 €, not 99 zł.refon server calls: the payment's own ID. It is what makes a retry harmless.plan,product, anything else you want: free-form, stored as JSON, visible in the event.
How a sale is counted once
Three rules, in this order:
- Same
ref, same site → recorded once. The second call answersduplicate. - Same session, same event name, same amount and currency → counted once. This is the success page reopened, the back button, the receipt link.
- Server beats browser for the same payment in the same window: the reconciled row is the one that counts.
The dashboard shows both the number of sales and the number of raw purchase events. When the second is higher, a site is replaying its event — the gap is displayed rather than hidden.
Currencies
Each sale keeps the amount you sent, in the currency you sent. Totals are converted to euros at the European Central Bank's daily reference rate, and the per-currency detail stays visible next to the total, so any figure can be recomputed by hand.
Subscriptions and renewals
A subscription is not one sale: it is one sale per paid invoice. Send a purchase from your billing webhook each time an invoice is paid, with the invoice ID as ref — the first payment and every renewal then appear on the day they were actually collected.
if (event.type === 'invoice.payment_succeeded') {
const invoice = event.data.object
await fetch('https://kipstats.com/api/collect/server', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KIPSTATS_INGEST_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'purchase',
ref: invoice.id, // one sale per invoice, replays are safe
occurredAt: new Date(invoice.created * 1000).toISOString(),
data: {
amount: invoice.amount_paid,
currency: invoice.currency.toUpperCase(),
plan: invoice.lines.data[0]?.price?.lookup_key,
renewal: invoice.billing_reason === 'subscription_cycle',
},
}),
})
} Use occurredAt when you replay history: a payment from last Tuesday must stay on last Tuesday, otherwise a catch-up run moves a month of revenue to today.
Refunds — what is not handled yet
A refund is not deducted from your revenue today. You can send a refund event and it will be stored and listed like any other event, but revenue totals only add up purchase amounts. If refunds matter to your numbers, subtract them on your side for now. This is a product gap, not a recommendation — it is written here so nobody discovers it from a wrong total.
Where the sale came from
Acquisition lives on the session: channel, referrer, UTM parameters, landing page. A browser purchase inherits it. A server sale has no session by design — recording one would invent a visit and inflate your traffic.
Two honest ways to keep attribution on server-recorded sales:
- Emit
checkout_startedfrom the browser: the session that intended to buy is then visible in the funnel, even if the payment is confirmed elsewhere. - Carry your own attribution into the payment: store the UTM parameters on your order, and send them back in
data— they stay queryable in the event payload.
Where it shows up
- Today, at the top of your dashboard: sales, revenue collected, signups, and the gap between sales and raw events.
- Events: every
purchase, with its payload. - Conversion panel: the funnel from arrival to purchase, split by intent and by traffic quality.
- API:
GET /api/v1/sites/{site}/breakdown/eventsand…/timeseries?event=purchase.
Checklist for a site that sells
- Tracker on every page, including the success page.
checkout_startedwhen the payment session actually opens — after your validations, not on the click.purchasefrom your webhook, withref,amountin cents andcurrency.- Never emit a sale for a test payment: check your provider's live flag first.
- One real purchase, end to end, before you trust the numbers.