# Send revenue from your backend

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.

```json
{
  "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](/reference/openapi/tracking).

### 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

```json
{
  "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](/acquire/tracking/reports) 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

- [Send events from your backend](/acquire/tracking/api-keys): create the key this route needs, and rotate it.
- [Read the attribution reports](/acquire/tracking/reports): which sources earned this money.
- [Feed conversions back to Google Ads](/acquire/tracking/google-ads): let your campaigns optimise on real revenue.
- [Fix revenue that is not showing](/acquire/tracking/troubleshooting): unit, currency, person and conversion, in that order.
