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 session | What the dashboard itself uses after GitHub login. Browser-only, same-origin. |
| Bearer token | Authorization: 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
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
| repo | required — 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
| repo | required — owner/name |
| path | required — 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.
Search
| q | required — full-text query |
| repo | optional — scope to one owner/name |
| limit | optional — 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).
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.
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.