API
The dashboard is a client of this API, not a privileged sibling. Anything a screen can show, you can pull — which also means the API cannot quietly fall behind the product, because the product would break first.
Authentication
A bearer token on every request. Two kinds, telling themselves apart by shape, and both resolving to the same internal principal — so there is one place a scoping bug can live rather than two.
# a machine, with an API key
curl https://api.pixtru.com/v1/recon/summary?from=2026-09-01&to=2026-09-30 \
-H "Authorization: Bearer pxk_live_..."
# a human, with a session token from the dashboard
curl ... -H "Authorization: Bearer eyJraWQiOi..."Scope is never a parameter. A request may say which campaign it wants; it may not say which advertiser it is allowed to see. That is derived from the credential on every request. Asking for an advertiser you do not hold returns an empty result, not a 403 — a refusal would confirm that the advertiser exists and belongs to somebody else.
Keys
Keys are shown once and stored only as a hash. Revocation takes effect on the next request — there is no cache to wait out, because a leaked key is the realistic incident. The first twelve characters are a readable prefix, so a key can be identified in a list or a support thread without ever being handled.
Endpoints
| Endpoint | Returns | Min role |
|---|---|---|
GET /v1/recon | The waterfall: impressions and spend per stage, reason and match tier. | viewer |
GET /v1/recon/summary | One row for the headline — billed, rendered, viewable, valid, and the spend attached to each. | viewer |
GET /v1/recon/impressions | Row level, one line per impression. Keyset paginated for export. | analyst |
GET /v1/sites/quality | Per site or app, sorted by wasted spend rather than volume. | viewer |
GET /v1/campaigns/{id}/live | The last two hours of beacons. Always preliminary — it reads raw events, not the reconciled table. | viewer |
The response envelope
Every response carries how fresh it is. A number that might still move and a number that is settled must never look identical, which is the mistake that turns a dashboard into a support ticket.
{
"data": [
{ "stage": "measurable", "reason": "below_threshold",
"match_tier": "exact", "impressions": 1496,
"spend": 2.71, "revenue": 1.95 }
],
"rows": 11,
"data_status": "preliminary",
"as_of": "2026-09-18T14:05:00Z"
}| Field | Meaning |
|---|---|
data_status | preliminary until the day is closed and the roll-up is final; final after. Show it — do not hide it behind a tooltip. |
as_of | When the underlying roll-up last ran, not when you asked. |
match_tier | exact, partner_id, fuzzy or none. Never sum across tiers without saying so. |
Pagination
Row-level exports are keyset paginated, not offset paginated. Pass the last imp_id you saw as after. Offsets drift when rows arrive mid-export, which silently skips or repeats impressions — precisely the failure a reconciliation product cannot have.
GET /v1/recon/impressions?day=2026-09-18&after=a3f9...&limit=5000Errors
| Status | When |
|---|---|
400 | A malformed or missing parameter. The message names it. |
401 | Missing, expired or revoked credential. Deliberately undetailed: “expired” and “no such key” are useful to someone enumerating credentials and identical to you. |
403 | Authenticated, but the role is too low for this endpoint. Never used for scope. |
429 | Rate limited. Retry after the header says. |
5xx | Ours. The response carries a request id; send it to us. |
CSV
Add format=csv to any endpoint for the same data with a header row and a Content-Disposition filename. The freshness label moves to theX-Data-Status header, so a spreadsheet does not silently lose it.