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.
| Name | What it means | Carries money |
|---|---|---|
signup | Someone created an account | No |
trial_started | A trial began | No |
subscription_started | A paid subscription began | No |
payment | Money was received | Yes, required |
refund | Money was given back | Yes, required |
churn | A subscription ended | No |
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" }
}| Field | Required | What it holds |
|---|---|---|
event_id | Yes | A 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. |
name | Yes | One of the six names above. |
email | No | A valid address. This is what ties the money to a person. |
amount_minor | For payment and refund | The amount in minor units, as a whole number. Never a decimal — see below. |
currency | Whenever amount_minor is present | A three-letter ISO 4217 code. Case does not matter; it is stored upper case. |
ts | No | ISO 8601 timestamp of when it happened. Left out, the time of arrival is used. |
anonymous_id, session_id | No | If 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. |
props | No | An 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.
| Amount | Send | Currency |
|---|---|---|
| 49.00 EUR | 4900 | EUR |
| 12.34 USD | 1234 | USD |
| 1000 JPY | 1000 | JPY — the yen has no minor unit, so the number is the same either way |
| 1.500 BHD | 1500 | BHD — 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:
| Event | What it does to the subscription |
|---|---|
trial_started | Sets it to trialing and records the trial start |
subscription_started | Sets it to active and records the start |
payment | Keeps it active |
churn | Sets 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
paymentfor 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
- Send events from your backend: create the key this route needs, and rotate it.
- Read the attribution reports: which sources earned this money.
- Feed conversions back to Google Ads: let your campaigns optimise on real revenue.
- Fix revenue that is not showing: unit, currency, person and conversion, in that order.