Skip to content

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

  1. 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

  1. Build an events array with up to 500 events, keeping the whole request within 64 KiB. Copy the endpoint from Public access and 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" }
  ]
}
  1. Verify it works: read the response. accepted counts stored events, duplicates counts previously stored ids, and rejected gives 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 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.

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

Was this page helpful?
Esc

Start typing to search the docs.

↑↓navigate↵select