API
A read-only way to pull reports and brands into your own tools — a nightly sync, an internal dashboard, a client portal — without signing in through the browser. Everything below is scoped to one agency: a key can only ever read the data that agency's users can see.
Getting a key
Sign in and open Settings → Report API. Agency users create and revoke their own agency's keys; an operator can see and revoke every agency's keys. A new key looks like pl_live_ followed by a long random string, and it is shown exactly once, at creation — Mapford stores only a one-way hash of it, so if it's lost the only recovery is revoking it and creating another.
Authentication
Send the key as a bearer token on every request:
curl https://your-deploy.example/api/v1/brands \
-H "Authorization: Bearer pl_live_your_key_here"
A missing, malformed, revoked or unknown key returns 401. Requests are rate-limited per key — see Limits.
Endpoints
All three are GET, all three return JSON, and every response carries Cache-Control: private, no-store — this is per-agency data, never something a shared cache should hold.
GET /api/v1/brands
The requesting agency's brands.
curl https://your-deploy.example/api/v1/brands \
-H "Authorization: Bearer pl_live_your_key_here"
{
"data": [
{
"id": "b1a2c3d4-...",
"name": "LumenLedger",
"vertical": "Mortgage / loan-officer CRM",
"domains": ["lumenledger.com"],
"archived": false,
"createdAt": "2026-08-01T12:00:00.000Z"
}
]
}
Pass ?includeArchived=1 to include archived brands, matching the app's own list view.
GET /api/v1/reports
The requesting agency's reports, newest first, paginated.
curl "https://your-deploy.example/api/v1/reports?brandId=b1a2c3d4-...&limit=20" \
-H "Authorization: Bearer pl_live_your_key_here"
{
"data": [
{
"id": "r5f6e7d8-...",
"brandId": "b1a2c3d4-...",
"number": 3,
"title": "LumenLedger — Report #3",
"createdAt": "2026-09-01T09:00:00.000Z"
}
],
"pagination": { "limit": 20, "offset": 0, "hasMore": false }
}
Query parameters: brandId (optional, narrows to one brand), limit (default 20, maximum 100), offset (default 0).
GET /api/v1/reports/{id}
One report's full stored snapshot — the same analysis object the app renders the report from, including every rate's n and its interval, the funnel, sources, and the measurement block naming the engines and models that produced it. Cost never appears here: reports.snapshot is built without it at report-creation time, so there is nothing for this endpoint to strip.
curl https://your-deploy.example/api/v1/reports/r5f6e7d8-... \
-H "Authorization: Bearer pl_live_your_key_here"
{
"data": {
"id": "r5f6e7d8-...",
"brandId": "b1a2c3d4-...",
"number": 3,
"title": "LumenLedger — Report #3",
"createdAt": "2026-09-01T09:00:00.000Z",
"analysis": { "placeableShare": 0.38, "verdict": "MARGINAL", "…": "…" }
}
}
A report id belonging to a different agency, or one that does not exist, returns 404 either way — the two are indistinguishable by design, the same rule the signed-in app follows.
Errors
Every error is { "error": { "code": "...", "message": "..." } }.
| Status | code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, malformed, unknown or revoked key |
| 403 | forbidden | The key doesn't carry the scope the endpoint needs |
| 404 | not_found | No such report, or it belongs to another agency |
| 429 | rate_limited | Too many requests — see Retry-After |
Limits
600 requests per 10 minutes per key. Going over returns 429 with a Retry-After header (seconds). This is a read-only, per-agency-scoped key, so the limit exists to bound a leaked key's blast radius, not to ration normal use — an integration polling a report every few seconds stays well under it.
Scopes
Every key carries reports:read today — read access to that agency's own reports and brands. There is no write access yet; creating runs, prompt sets, or share links still requires signing in.