Skip to content
Documentation menu

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": "..." } }.

StatuscodeMeaning
401unauthorizedMissing, malformed, unknown or revoked key
403forbiddenThe key doesn't carry the scope the endpoint needs
404not_foundNo such report, or it belongs to another agency
429rate_limitedToo 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.