Send revenue from your backend
Send lifecycle events in minor units and verify subscriptions, customer status, and lifetime value.
Send payment and refund events from your backend to record money. Use integer minor units and include the person’s email or anonymous id when the amount belongs to a customer.
Before you start: obtain a public access key scoped to the Events API and copy the product endpoint from Public access. This guide uses an Acme subscription; replace Ada’s address, invoice id, subscription reference, plan, amount, currency, and timestamp with your own values.
Pick the event
- Choose the lifecycle name that matches what happened. The route accepts other names, but these six have built-in lifecycle or revenue meaning.
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 |
An amount is mandatory for payment and refund. The other four lifecycle events accept one but do not require it.
For an ordinary event outside these names, only event_id and name are required.
Money outside the six names
You can attach amount_minor to any name, with currency required whenever an amount is present. An ordinary event retains the amount for reading back, but does not add it to lifetime value or revenue reports. There is no screen or report for those per-event amounts. Use payment or refund when the amount should count as cash.
A Google Ads conversion rule can map any catalog name to a fixed value. Reading a value from the revenue record requires a lifecycle event; ordinary event amounts do not create that record. Google Ads conversion feedback documents the dynamic sources.
Send it
- Build an
eventsarray with up to 500 events, keeping the whole request within 64 KiB. Copy the endpoint fromPublic accessand send the authenticated request from your backend. The object below is one event to place in that array:
{
"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 | Any non-empty string. The six names above are the ones Tracking reads as money. |
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. Any event may carry one. |
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. |
See Public HTTP routes and SDK for the request, response codes, and errors. The Tracking API reference provides the generated management schema.
Amounts are in minor units
Use an integer in the currency’s smallest unit for amount_minor. Tracking does not calculate money with floating-point arithmetic.
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 |
Check the currency’s divisor before sending. A major-unit amount sent as minor units makes lifetime value, ROAS, and Google Ads values wrong: EUR uses a factor of 100, while JPY and BHD use different divisors. Display formatting cannot detect the mistake; it produces a plausible but incorrect amount.
Send refunds as positive amounts. The name refund supplies the direction. Negative or fractional amount_minor values are rejected.
Timestamps
Set ts to a parseable time no more than 24 hours ahead or about 90 days behind now. Events outside that window are rejected with a reason. Omit it to use arrival time; this route cannot import years of history.
Read the answer
{
"accepted": 2,
"duplicates": 1,
"rejected": [
{ "index": 3, "event_id": "inv_77", "reason": "amount_minor is required for payment" }
]
}- Verify it works: read the response.
acceptedcounts stored events,duplicatescounts previously stored ids, andrejectedgives invalid entries by index, optional id, and reason. Correct any rejection; one invalid event does not discard valid events. Accepted money is written transactionally before acknowledgement.
A 402 instead refuses the entire request without storage. Resolve its plan or billing condition before resending retained events; see HTTP errors.
Authenticated backend events bypass the browser user-agent bot filter.
Follow a subscription
Include your own subscription_ref in props to maintain subscription state under 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 subscription_ref, the event is stored and its money counts, but there is no subscription to follow. Revenue by source selects people by their first paid conversion, which requires subscription_started with a subscription_ref.
What makes a person a customer
Only subscription_started or payment tied to a person promotes them to Customer. signup, trial_started, churn, and refund leave status unchanged; refunds do not remove customer status.
Attach email or anonymous_id to associate revenue with a person. With neither, the money has no owner: it cannot produce a Customer record or lifetime value and appears as Unattributed in reports.
Merging two person records combines their money and retains Customer status.
Lifetime value
Lifetime value totals a person’s payments minus refunds separately for each 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.
A person who pays in two currencies has two value blocks. Tracking stores no exchange rates and does not combine the amounts.
Open a person from People to find Lifetime value. Before revenue arrives the tile reads No revenue yet. Payments appear in the journey immediately but can take up to an hour to update the scheduled lifetime-value calculation. Money with no person is excluded.
Money in the journey
The journey labels lifecycle activity as Payment, Refund, Trial started, Subscription started, or Churn. Amounts appear when the event has amount_minor and currency; refunds display as negative. signup has no displayed money.
Only payment and refund add cash records. Each retains amount, currency, timestamp, and person association. Records are not overwritten and currencies remain separate.
Next steps
- Send events from your backend: get the key this route needs, and replace it without a gap.
- Read the attribution reports: which sources earned this money.
- Feed conversions back to Google Ads: let your campaigns optimize on real revenue.
- Trigger a workflow from a tracked event: the conversion trigger carries these amounts, in minor units.
- Fix revenue that is not showing: unit, currency, person and conversion, in that order.