API reference
Conventions
The rules every KIWI endpoint follows, and the bar for any API added behind a Worker next:
- Read by default. Everything is
GETand safe to retry. The one mutation is aPOSTbehind a bearer secret. - History, not state. Reads take
?snapshot=<id>and default to the latest, so two points in time can always be compared. - Errors are JSON too.
{ "error": "…" }with a real status: 404 lists the endpoints, 405 for a wrong method, 503 before the first snapshot exists. - No guessed data.
nullmeans “not determined”, never “no”, so a collection failure can’t read as a finding.
GET /api/health
Liveness, and which environment answered. Needs no snapshot.
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/health" | jq .const res = await fetch('https://kiwi-prod.aa-bts.workers.dev/api/health');
if (!res.ok) throw new Error(`KIWI ${res.status}`);
const data = await res.json();Returns { ok, environment }
oktruewhenever the Worker answersenvironmentdevorprod, from the Worker’sENVIRONMENTvariable
GET /api/snapshots
The 50 most recent snapshots, newest first. Use an id from here as ?snapshot= anywhere else.
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/snapshots" | jq .const res = await fetch('https://kiwi-prod.aa-bts.workers.dev/api/snapshots');
if (!res.ok) throw new Error(`KIWI ${res.status}`);
const data = await res.json();Returns { snapshots: [...] }
id- Snapshot id
taken_at- ISO 8601 UTC time the collection ran
n_pages, n_workers, n_repos- Counts collected
complete1if collection finished without a provider error. An incomplete snapshot is a floor, not a total
GET /api/summary
One snapshot’s header and its finding counts by code.
| Query parameter | Meaning |
|---|---|
snapshot | A snapshot id. Defaults to the latest snapshot. |
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/summary" | jq .const res = await fetch('https://kiwi-prod.aa-bts.workers.dev/api/summary');
if (!res.ok) throw new Error(`KIWI ${res.status}`);
const data = await res.json();Returns { snapshot, findings: [{ code, n }] }
snapshot- The snapshot row: id, taken_at, github_org, cf_account, counts, complete, notes
findings- One entry per finding code with its count, most frequent first
GET /api/projects
Every Pages project and Worker in a snapshot, with everything the catalog shows.
| Query parameter | Meaning |
|---|---|
platform | pages or worker. Both when omitted. |
finding | A finding code; returns only projects with that finding. |
snapshot | A snapshot id. Defaults to the latest snapshot. |
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/projects?platform=pages&finding=stale_production" | jq .const res = await fetch('https://kiwi-prod.aa-bts.workers.dev/api/projects?platform=pages&finding=stale_production');
if (!res.ok) throw new Error(`KIWI ${res.status}`);
const data = await res.json();Returns { snapshot_id, projects: [...] }
platform, name- The Cloudflare project or Worker name
git_owner, git_repo, binding_state- The git binding as Cloudflare reports it:
bound,staleorwiped deploy_method, prod_deploy_method- How it last deployed, and how production deploys:
direct_upload,cf_git,wrangler,dashboard last_deploy, last_prod_deploy- ISO 8601 times
has_workflow, template_ref, has_deploy_secret, last_ci_status- The repo side of the shared deploy workflow
custom_domains- JSON array of custom hostnames, or null before migration 0007
…- Build shape and the rest: the full row of the
projecttable
GET /api/findings
Every finding in a snapshot, ordered by code then name.
| Query parameter | Meaning |
|---|---|
snapshot | A snapshot id. Defaults to the latest snapshot. |
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/findings" | jq .const res = await fetch('https://kiwi-prod.aa-bts.workers.dev/api/findings');
if (!res.ok) throw new Error(`KIWI ${res.status}`);
const data = await res.json();Returns { snapshot_id, findings: [{ platform, name, code, detail }] }
code- A stable finding code, e.g.
cannot_deploy,stale_production detail- A one-line explanation for this project
GET /api/diff
What changed between two complete snapshots: findings resolved and introduced. Defaults to the latest two.
| Query parameter | Meaning |
|---|---|
from | The older snapshot id. |
to | The newer snapshot id. |
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/diff" | jq .const res = await fetch('https://kiwi-prod.aa-bts.workers.dev/api/diff');
if (!res.ok) throw new Error(`KIWI ${res.status}`);
const data = await res.json();Returns { from, to, snapshots, summary, resolved, introduced }
summary{ resolved, introduced, unchanged }countsresolved, introduced- Findings present in only one of the two snapshots
POST /api/collect
Run collection now and return the snapshot it wrote. Disabled unless the Worker has COLLECT_TRIGGER_SECRET.
Mutation
Bearer token only, never a query parameter, which would land in request logs and history. Without the secret set on the Worker it answers 503; a wrong token answers 401.
curl -X POST "https://kiwi-prod.aa-bts.workers.dev/api/collect" \
-H "Authorization: Bearer $COLLECT_TRIGGER_SECRET"const res = await fetch('https://kiwi-prod.aa-bts.workers.dev/api/collect', {
method: 'POST',
headers: { authorization: `Bearer ${env.COLLECT_TRIGGER_SECRET}` },
});
const { snapshot } = await res.json();Returns { ok, snapshot }
oktruewhen collection ransnapshot- The new snapshot’s header
Calling KIWI from a Worker
Any Worker can call these endpoints with fetch, as in the examples. Two things change how:
- Cloudflare Access. Where a KIWI environment sits behind Access, a script needs an Access service token: send its
CF-Access-Client-IdandCF-Access-Client-Secretheaders, stored as Worker secrets. - Service bindings. A Worker on the same Cloudflare account can bind to KIWI and call it without a public network hop, and without Access in the way.
// wrangler.jsonc: "services": [{ "binding": "KIWI", "service": "kiwi-prod" }]
const res = await env.KIWI.fetch('https://kiwi/api/summary');
const { snapshot, findings } = await res.json();