If your goals are worth money — a lead that closes, a quote accepted, a booking taken — send what each one was worth and the dashboard reports it beside your traffic.
POST https://truestat.io/api/v1/goals
Authorization: Bearer <your API key>
Content-Type: application/json
{
"name": "lead_won",
"value_cents": 250000,
"currency": "usd",
"visitor_id": "<the ts_vid you stored>"
}value_cents is in the currency's smallest unit, so 250000 is $2,500.00.
Why this one needs an API key
Your site key ships in every page's HTML. A figure the browser could send is a figure anyone reading your source could send, so no amount is accepted from the script or from the server route.
The key needs the goals:write scope. That is deliberately separate from
reading: a key wired into a reporting dashboard should not be able to write
your conversion numbers. Create one under Settings → API keys.
The two-event pattern
A lead and a closed deal are two different events, and measuring both is what makes the conversion rate real:
window.truestat("lead"); // browser, immediatelyPOST /api/v1/goals // server, a week later
{ "name": "lead_won", "value_cents": 250000, "currency": "usd",
"visitor_id": "<stored>" }Someone who counts both is measuring the conversion rate. Someone who writes an estimated value onto the single first event is assuming it, and can never see that they were wrong.
Keeping the visitor id
Write the ts_vid cookie into your own database when you record the lead, and
hand it back here when the deal closes. The cookie does not live long enough to
still be there a week later — this is the same job subscription_data.metadata
does in Stripe, for the same reason.
The fields
| Field | Required | Notes |
name | yes | Lowercase letters, numbers, _ - :, up to 64 characters |
value_cents | no | Integer, smallest currency unit, up to 10,000,000 ($100,000) |
currency | with a value | ISO 4217, three letters — usd, eur, gbp |
visitor_id | no | The ts_vid you stored; any string up to 64 characters |
site_id | when the key covers several sites | UUID |
occurred_at | no | ISO 8601; the deal's real date rather than now |
metadata | no | Properties to break the goal down by |
An amount always travels with its unit. value_cents without currency is
refused: 250000 in an unknown currency is a number nobody can read, and
guessing afterwards is worse than refusing now.
occurred_at is worth using when you post a deal that closed yesterday —
without it the goal lands on today and your daily chart is wrong.
What comes back
{
"data": {
"name": "lead_won",
"site_id": "…",
"attributed": true
}
}A 202, and attributed tells you whether the goal was tied to a visit —
false means no visitor_id was given, so the goal counts on the card but
contributes to no breakdown.
When it does not work
| Status | Meaning |
400 | The body is wrong; the message names the field |
401 | The key is missing, unknown or revoked |
403 | The key lacks goals:write, or the site is not yours |
402 | The subscription has lapsed — the key is fine, the plan is not |
502 | We could not record it; retry |
A 402 is not a permission problem, so do not rotate the key when you see one.
Where it appears
The total sits on the Revenue tile, and each goal shows its own value beneath the name. Because goals filter the whole dashboard, clicking one narrows every other card to the people who converted — so "which campaign produced the leads that actually closed" is a question the panel answers.
You can also make these goals the site's revenue figure outright: see what counts as revenue.
You do not need Stripe for this. Revenue reporting and goal values are separate: one is what your payment provider settled, the other is what you told us a conversion was worth. A site that sells nothing online can still measure which channel brings the deals that close.
Goal value and provider revenue are never added together. If you connect a payment provider, the tile reports what the provider settled, so it keeps matching their dashboard exactly. Mixing in figures you supplied would break that match, and a total that disagrees with your payment provider is one you cannot use.
Reserved names
subscription_started, subscription_renewed and the other names the billing
integration writes are refused here. Sending one yourself would merge two
sources into one row — the card would say "forty churns" with no way to tell
which twenty came from where. See
reserved names.