API reference

The read API under /api (used by the dashboard and by MCP tool calls) plus the write API under /ingest (used by each repo's CI to publish docs). Base URL is this site's origin, https://projects.kecker.co. Conversational access goes through MCP instead of these endpoints directly — this page is for scripting or debugging.

Authentication

The read endpoints (/api/*) accept either:

Cookie sessionWhat the dashboard itself uses after GitHub login. Browser-only, same-origin.
Bearer tokenAuthorization: Bearer <access_token> — the OAuth token an MCP client holds after the GitHub sign-in flow described on /connect.

Either way, access is gated by KeckerCo GitHub org membership at login time — there's no separate allowlist or API key for the read API. A request with neither credential returns 401 {"error":"unauthorized"}. There is deliberately no CORS handling: these endpoints are same-origin (the dashboard's own JS) or same-token (an MCP client), never called from another site's browser JS.

The write endpoints (/ingest/*) use a separate credential — Authorization: Bearer <INGEST_TOKEN>, a shared secret held by CI, not a per-user OAuth token. A session or MCP bearer token is not accepted there, and the ingest token is not accepted on /api/*.

Conventions

All request and response bodies are JSON (Content-Type: application/json). A repo is always the full owner/name form, e.g. KeckerCo/miller — matching the GitHub repo it came from.

Validation errors (missing required field, unknown repo, etc.) return 400 with {"error": "<message>"}. A doc or repo that doesn't exist returns 404 with the same shape.

List repos

GET/api/repossession or bearer

Every repo with at least one indexed doc, with rollup counts.

{
  "repos": [
    {
      "repo": "KeckerCo/miller",
      "docs": 12,
      "chunks": 84,
      "bytes": 41823,
      "updated_at": "2026-09-10T14:22:01Z",
      "last_commit_sha": "a1b2c3d4e5f6"
    }
  ]
}

List docs

GET/api/docssession or bearer
reporequired — owner/name
{
  "docs": [
    {
      "id": 42,
      "repo": "KeckerCo/miller",
      "path": "docs/architecture/data-layer.md",
      "title": "Data Layer",
      "bytes": 5310,
      "updated_at": "2026-09-10T14:22:01Z"
    }
  ]
}

Read a doc

GET/api/docsession or bearer
reporequired — owner/name
pathrequired — exact path as returned by /api/docs
{
  "doc": {
    "repo": "KeckerCo/miller",
    "path": "docs/architecture/data-layer.md",
    "title": "Data Layer",
    "content": "# Data Layer\n\nMiller stores...",
    "updated_at": "2026-09-10T14:22:01Z"
  }
}

content is the full raw markdown, exactly as ingested — no rendering or truncation.

GET/api/searchsession or bearer
qrequired — full-text query
repooptional — scope to one owner/name
limitoptional — max results
{
  "results": [
    {
      "repo": "KeckerCo/miller",
      "path": "docs/architecture/data-layer.md",
      "heading_path": "Architecture > Data Layer > Caching",
      "snippet": "...falls back to the origin [D1] instance when the edge...",
      "score": 4.2
    }
  ]
}

Matched terms in snippet are delimited with [brackets] rather than HTML, so a caller renders them however it wants — the dashboard converts them to <mark> client-side, after escaping.

Ingest (write API)

Called by each repo's CI on merge to main, never by the dashboard or an MCP tool. Requires Authorization: Bearer <INGEST_TOKEN> (see Authentication above).

POST/ingest/batchingest token

Upserts one repo's docs. Content-hash comparison means re-sending an unchanged doc is a no-op — safe to call on every merge, even docs-untouched ones.

{
  "repo": "KeckerCo/miller",
  "commitSha": "a1b2c3d4e5f6",
  "docs": [
    { "path": "docs/architecture/data-layer.md", "content": "# Data Layer\n\n..." }
  ]
}
{
  "repo": "KeckerCo/miller",
  "docsSeen": 8,
  "docsWritten": 3,
  "errors": 0,
  "results": [
    { "status": "updated",   "repo": "KeckerCo/miller", "path": "docs/architecture/data-layer.md", "chunks": 6 },
    { "status": "unchanged", "repo": "KeckerCo/miller", "path": "docs/overview.md", "chunks": 4 }
  ]
}

Returns 207 instead of 200 if any individual document failed, with that document's entry carrying "status": "error". CI treats 207 as a failure — a partially indexed doc set should be loud, not silently accepted.

POST/ingest/completeingest token

Call once after the batch(es) for a commit are sent, with every path that should still exist. Any previously-indexed doc for the repo not in paths is deleted — this is how a doc removed from the source repo gets removed here too.

{
  "repo": "KeckerCo/miller",
  "paths": ["docs/architecture/data-layer.md"],
  "commitSha": "a1b2c3d4e5f6"
}
{
  "repo": "KeckerCo/miller",
  "docsSeen": 8,
  "docsDeleted": 1,
  "deleted": ["docs/architecture/old-cache-design.md"]
}

An empty paths array is rejected with 400 rather than honoured. It would delete every document for the repo, and in practice that means the generator produced nothing — not that the docs were genuinely all removed.