The desk’s wire — reference

v1 API

Everything the web desk presents is served over versioned JSON endpoints under /api/v1 — the version is explicit in the path, and it is the contract clients (an iOS app included) build against. Reading is anonymous; account-scoped calls carry a personal token.

Authentication

Read endpoints — the reports surface and docket status — need no authentication: reading is free product-wide. Account-scoped endpoints (/api/v1/my/* and POST /api/v1/research) require a personal API token, minted at your account’s tokens page, sent as a Bearer header:

Authorization: Bearer dd_5e884898da28047151d0e56f8dc6292773603d0d

Tokens are the only credential v1 accepts — browser session cookies never authorize these endpoints, so a v1 call is immune to cross-site request forgery by construction. A missing, malformed, unknown, or revoked token yields one identical 401 body. Revocation is immediate. Keep tokens out of URLs; send them only in the header.

Endpoints

GET/api/v1/reportsNo auth — public read

The library list: every published report's ticker, latest version date, and generated_at — the same content and order as the library page. The envelope carries the disclaimer field.

Example response

{
  "disclaimer": "Research tooling output; may contain data or reasoning errors; not investment advice",
  "reports": [
    {
      "ticker": "KO",
      "version": "2026-07-03",
      "generated_at": "2026-07-03T06:33:23+00:00"
    }
  ]
}
GET/api/v1/reports/<ticker>No auth — public read

The latest published report payload. A caller holding an unlock for the ticker (a personal token of an account that unlocked it in the app) gets the engine's full report.json shape (ticker, version, generated_at, data_as_of, sections, figures, scores) plus exactly one product-added field: disclaimer. Every other caller — anonymous, or a token without the unlock — gets the teaser slice: the TL;DR section only, locked: true, and the locked remainder as heading names under locked_sections. API reads never consume an unlock. Ticker is case-insensitive.

ticker
case-insensitive ticker symbol, e.g. KO or ko

Example response

{
  "ticker": "KO",
  "version": "2026-07-03",
  "generated_at": "2026-07-03T06:33:23+00:00",
  "data_as_of": {
    "companyfacts.json": {
      "sha256": "b903ffdb93bd0cacedcf17c30aed679f60e52ebcfef85619197ae4ace1b3fbb6",
      "as_of": "2026-07-03T04:54:31.085294+00:00"
    }
  },
  "sections": [
    {
      "id": "tldr",
      "title": "TL;DR",
      "markdown": "KO closed 2026-07-02 at $84.14 — the 52-week high (the maximum of all 261 closes…"
    }
  ],
  "figures": [
    {
      "value": "$5,296M",
      "label": "stated FY2025 free cash flow (OCF $7,408M − capex $2,112M)",
      "source": "metrics.json free_cash_flow (identity script-checked)",
      "section": "fundamentals"
    }
  ],
  "scores": null,
  "disclaimer": "Research tooling output; may contain data or reasoning errors; not investment advice"
}

Arrays truncated for display — the seeded exemplar carries eight sections, 17 figures, and five data_as_of sources. Shapes are exact.

GET/api/v1/reports/<ticker>/versionsNo auth — public read

The dated version list the history register shows, newest first. Every version is immutable and individually addressable.

ticker
case-insensitive ticker symbol

Example response

{
  "disclaimer": "Research tooling output; may contain data or reasoning errors; not investment advice",
  "ticker": "KO",
  "versions": [
    {
      "version": "2026-07-03",
      "generated_at": "2026-07-03T06:33:23+00:00"
    },
    {
      "version": "2026-06-19",
      "generated_at": "2026-06-19T06:12:47+00:00"
    }
  ]
}
GET/api/v1/reports/<ticker>/<version>No auth — public read

One immutable dated version's payload, exactly as published — same shape and same read gate as the latest fetch, pinned to that version (an unlock is ticker-keyed, so it opens every dated version; locked callers get this version's teaser slice with locked: true). An unknown version is a JSON 404.

ticker
case-insensitive ticker symbol
version
a dated version from the versions list, YYYY-MM-DD

Example response

{
  "ticker": "KO",
  "version": "2026-06-19",
  "generated_at": "2026-06-19T06:33:23+00:00",
  "data_as_of": {
    "companyfacts.json": {
      "sha256": "b903ffdb93bd0cacedcf17c30aed679f60e52ebcfef85619197ae4ace1b3fbb6",
      "as_of": "2026-06-19T04:54:31.085294+00:00"
    }
  },
  "sections": [
    {
      "id": "tldr",
      "title": "TL;DR",
      "markdown": "KO closed 2026-07-02 at $84.14 — the 52-week high (the maximum of all 261 closes…"
    }
  ],
  "figures": [
    {
      "value": "$5,296M",
      "label": "stated FY2025 free cash flow (OCF $7,408M − capex $2,112M)",
      "source": "metrics.json free_cash_flow (identity script-checked)",
      "section": "fundamentals"
    }
  ],
  "scores": null,
  "disclaimer": "Research tooling output; may contain data or reasoning errors; not investment advice"
}

Arrays truncated for display — shapes are exact; the payload is the version's, byte-for-byte as published.

GET/api/v1/jobs/<id>No auth — public read

Docket status: queued → running → ready, or failed with its category in detail. On ready, published_version names the dated version the run published. Resumable runs carry session detail ({"session": 2, "paused": true}) while running. The payload is impersonal — no account material ever rides it.

id
the docket id from POST /api/v1/research

Example response

{
  "id": "1f0c2f6a-8f4e-4c1b-9a52-3d8e7b6a41d0",
  "ticker": "ZZQX",
  "kind": "research",
  "status": "running",
  "detail": "Analyst session in progress",
  "requested_at": "2026-07-06T14:02:11Z",
  "updated_at": "2026-07-06T14:02:15Z",
  "published_version": null,
  "session": null
}
POST/api/v1/researchPersonal token

Request research for a ticker. Runs the desk's one consumption rule: a research unit is consumed exactly when a new engine docket is created on your account's behalf — and at no other moment.

Request body — JSON

{"ticker": "ZZQX"}

Outcomes & quota semantics

  • 201 {"outcome": "docketed", "job": {…}}: a fresh docket, attributed to your account, consuming one research unit. It is the same docket a web request creates — it appears on the web surfaces and in /api/v1/my/research.
  • 200 {"outcome": "joined", "job": {…}}: the ticker already has a live run; you join it. Joining consumes nothing — one run per ticker is engine law.
  • 200 {"outcome": "published", "ticker", "version"}: the ticker is already on the library — fetch it; no docket, no unit.
  • 403 quota_wall — the account is on Free; nothing dockets. 403 quota_exhausted — the period pool is at zero; the message names the allowance and reset date.
  • A docket that terminally fails (engine halt) refunds its unit to the period it was consumed from. Resumable runs charge once, at docket creation.
  • 503 engine_unavailable — the engine is unreachable: nothing docketed, no unit consumed; try again later.

Example response

{
  "outcome": "docketed",
  "job": {
    "id": "1f0c2f6a-8f4e-4c1b-9a52-3d8e7b6a41d0",
    "ticker": "ZZQX",
    "kind": "research",
    "status": "queued",
    "detail": "Awaiting an engine slot",
    "requested_at": "2026-07-06T14:02:11Z",
    "updated_at": "2026-07-06T14:02:11Z",
    "published_version": null,
    "session": null
  }
}
GET/api/v1/my/researchPersonal token

The token account's own dockets, newest first — the same rows the My research page presents. Reading a live docket advances it, so polling this endpoint tracks runs to their terminal state.

Example response

{
  "jobs": [
    {
      "id": "1f0c2f6a-8f4e-4c1b-9a52-3d8e7b6a41d0",
      "ticker": "ZZQX",
      "kind": "research",
      "status": "ready",
      "detail": "Report cleared audit",
      "requested_at": "2026-07-06T14:02:11Z",
      "updated_at": "2026-07-06T14:02:24Z",
      "published_version": "2026-07-06"
    }
  ]
}
GET/api/v1/my/watchlistPersonal token

The token account's followed tickers, newest follow first, each with its latest published version and any live docket (live is {id, status, kind} while a run is underway, null once settled).

Example response

{
  "rows": [
    {
      "ticker": "KO",
      "version": "2026-07-03",
      "generated_at": "2026-07-03T06:33:23+00:00",
      "live": null
    }
  ]
}

Errors

Every v1 error is JSON of one shape, with an honest HTTP status — never an HTML page under the v1 prefix:

{
  "error": {
    "code": "not_found",
    "message": "No published report for 'ZZQX' — request research via POST /api/v1/research"
  }
}
CodeStatusMeaning
invalid_request400Malformed body or ticker — hostile input never crashes, it lands here.
unauthorized401Missing, malformed, unknown, or revoked token — one identical body for all four; the API never redirects to sign-in.
quota_wall403The token's account is on Free — research generation comes with Pro.
quota_exhausted403The account's period allowance is at zero; the message names the allowance and reset date.
not_found404Unknown ticker, version, docket id, or v1 path — designed JSON, never HTML.
engine_unavailable503The research engine is unreachable at docket creation — nothing dockets and no unit is consumed; try again later.

The v1 surface is additive and stable: report payloads serve the engine’s report.json exactly as published, dated versions are immutable, and breaking changes would arrive as a new version prefix — never as a change under /api/v1.