API reference
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://app.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
Roles go viewer → analyst → owner, each able to do
everything the one before can. deliverer stands apart: a key for an
upload job, which can deliver log files and read what became of them, and
cannot read a single report. Analysts and owners can deliver too.
| 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 |
GET /v1/measured | What the tag measured, with or without a billing log: impressions, measurable, viewable and invalid traffic per day, campaign and website. CSV on the same path. | viewer |
POST /v1/org/logs/uploads | Declare an hourly log file and get a 15-minute upload link for it. See the integration guide. | deliverer |
GET /v1/org/logs | What became of each delivered log file in the last 14 days: loaded, or rejected and why. | deliverer |
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=5000 Errors
| 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 the
X-Data-Status header, so a spreadsheet does not silently lose it.