Skip to content

Tracking

Send revenue from your backend

Post the six lifecycle events with amounts in minor units, and get lifetime value and customer status against a person.

Money never comes from the browser. You send it from your own backend, over HTTP, authenticated with a tracking API key in the X-Api-Key header. This page is about the money itself: which events carry it, what the fields mean, and what Tracking does with them.

Pick the event

The backend route accepts these six names and no others.

NameWhat it meansCarries money
signupSomeone created an accountNo
trial_startedA trial beganNo
subscription_startedA paid subscription beganNo
paymentMoney was receivedYes, required
refundMoney was given backYes, required
churnA subscription endedNo

payment and refund are rejected without an amount. The other four are accepted with or without one.

Send it

One request carries up to 500 events under the events key, and the whole body is capped at 64 KiB. The finished endpoint for your product, with a button to copy it, is on the API keys page.

{
  "event_id": "inv_20260802_8891",
  "name": "payment",
  "email": "ada@example.com",
  "amount_minor": 4900,
  "currency": "EUR",
  "ts": "2026-08-02T09:15:00Z",
  "props": { "subscription_ref": "sub_5512", "plan": "pro" }
}
FieldRequiredWhat it holds
event_idYesA non-empty string you choose. It is the idempotency key: an id already seen for your product is counted as a duplicate and stored once, so retrying a batch after a timeout is safe.
nameYesOne of the six names above.
emailNoA valid address. This is what ties the money to a person.
amount_minorFor payment and refundThe amount in minor units, as a whole number. Never a decimal — see below.
currencyWhenever amount_minor is presentA three-letter ISO 4217 code. Case does not matter; it is stored upper case.
tsNoISO 8601 timestamp of when it happened. Left out, the time of arrival is used.
anonymous_id, session_idNoIf your site captured the visitor’s anonymous id in the browser and your backend has it, pass it and the payment joins that browsing journey.
propsNoAn object of your own fields. One key is read by Tracking — subscription_ref. Everything else is stored as sent.

The route, its status codes and its full schema are on the Tracking API reference.

Amounts are in minor units

amount_minor is a whole number of the currency’s smallest unit. There is no float maths on it anywhere.

AmountSendCurrency
49.00 EUR4900EUR
12.34 USD1234USD
1000 JPY1000JPY — the yen has no minor unit, so the number is the same either way
1.500 BHD1500BHD — the dinar has three decimal places

Get this wrong and every number downstream is wrong by a factor of a hundred — lifetime value, ROAS, and the conversion values sent to Google Ads. When the amount is displayed it is divided by the divisor the currency itself implies, so a wrong unit is caught nowhere. It just reads as a plausible number.

A refund amount is positive, like a payment. refund says which direction the money went; the sign is not yours to set. amount_minor is rejected if it is negative or not a whole number.

Timestamps

A ts you send must be parseable and must fall in the writable window: no more than 24 hours ahead of now, and no more than about 90 days behind. Outside that the event is rejected with a reason and nothing is stored. This route is for money as it happens, not for importing years of history.

Read the answer

{
  "accepted": 2,
  "duplicates": 1,
  "rejected": [
    { "index": 3, "event_id": "inv_77", "reason": "amount_minor is required for payment" }
  ]
}

rejected names the position in your batch, the id if you sent one, and why. Every event is validated on its own — one bad event does not throw away the rest of the batch. The money itself is written in a single transaction, so a batch that is acknowledged is a batch that is durably stored.

Events sent with an API key are not run through the bot filter that applies to browser events: the key already proves who is calling.

Follow a subscription

Put a subscription_ref of your own in props and a subscription is kept for it, keyed on that reference:

EventWhat it does to the subscription
trial_startedSets it to trialing and records the trial start
subscription_startedSets it to active and records the start
paymentKeeps it active
churnSets it to canceled and records the cancellation

Without a subscription_ref the event is still stored and the money still counts — there is simply no subscription to follow. It also matters for the reports: Revenue by source counts people by their first paid conversion, and a conversion only exists when you send subscription_started with a subscription_ref.

What makes a person a customer

A person becomes a Customer when a subscription_started or a payment is tied to them. Nothing else promotes: signup, trial_started, churn and refund leave the status as it was, and a refund never takes the status back.

The tie is what matters. A revenue event reaches a person through its email or its anonymous_id; with neither, the money is stored but belongs to nobody, there is no Customer to show, and it lands in Unattributed in the reports. Send the email you know.

The status is sticky: when two records turn out to be the same person and are merged, Customer survives, and the merged person keeps all of the money from both records.

Lifetime value

Lifetime value is the plain sum of what a person paid you, less what you gave back, held per currency:

  • Gross — every payment for that person in that currency.
  • Refunded — every refund.
  • Net — gross minus refunded. This is the headline number.
  • First payment and last payment — the earliest and latest payment. Refunds do not move them, so a person with refunds only has neither.

Someone who paid you in two currencies gets two blocks, side by side. They are never added together; there is no exchange rate anywhere in Tracking.

It appears as the Lifetime value tile on a person’s page, opened from People, and reads No revenue yet before any money arrives. The figures are recomputed on a schedule rather than on every payment, so a payment you have just sent shows in the person’s journey immediately but can take up to an hour to move the tile. Money that belongs to nobody is not in lifetime value at all.

Money in the journey

A person’s journey shows the lifecycle events by name — Payment, Refund, Trial started, Subscription started, Churn — with the amount alongside when the event carried amount_minor and currency. A refund is shown as a negative amount. signup carries no money and shows none.

payment and refund are the only two events that add to the cash record. Each is stored with its amount, its currency, the moment it happened and the person it belongs to. Nothing is ever overwritten and nothing is summed across currencies — EUR and USD stay separate all the way through.

Next steps

Was this page helpful?
Esc

Start typing to search the docs.

navigateselect