API reference

Read-only JSON over the inventory snapshots, served by this Worker from src/api/router.ts. Every example uses https://kiwi-prod.aa-bts.workers.dev.

Conventions

The rules every KIWI endpoint follows, and the bar for any API added behind a Worker next:

  • Read by default. Everything is GET and safe to retry. The one mutation is a POST behind 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. null means “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.

sh
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/health" | jq .
ts
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 }

ok
true whenever the Worker answers
environment
dev or prod, from the Worker’s ENVIRONMENT variable

GET /api/snapshots

The 50 most recent snapshots, newest first. Use an id from here as ?snapshot= anywhere else.

sh
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/snapshots" | jq .
ts
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
complete
1 if 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 parameterMeaning
snapshotA snapshot id. Defaults to the latest snapshot.
sh
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/summary" | jq .
ts
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 parameterMeaning
platformpages or worker. Both when omitted.
findingA finding code; returns only projects with that finding.
snapshotA snapshot id. Defaults to the latest snapshot.
sh
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/projects?platform=pages&finding=stale_production" | jq .
ts
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, stale or wiped
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 project table

GET /api/findings

Every finding in a snapshot, ordered by code then name.

Query parameterMeaning
snapshotA snapshot id. Defaults to the latest snapshot.
sh
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/findings" | jq .
ts
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 parameterMeaning
fromThe older snapshot id.
toThe newer snapshot id.
sh
curl -s "https://kiwi-prod.aa-bts.workers.dev/api/diff" | jq .
ts
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 } counts
resolved, 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.

sh
curl -X POST "https://kiwi-prod.aa-bts.workers.dev/api/collect" \
  -H "Authorization: Bearer $COLLECT_TRIGGER_SECRET"
ts
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 }

ok
true when collection ran
snapshot
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-Id and CF-Access-Client-Secret headers, 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.
worker.ts
// wrangler.jsonc: "services": [{ "binding": "KIWI", "service": "kiwi-prod" }]
const res = await env.KIWI.fetch('https://kiwi/api/summary');
const { snapshot, findings } = await res.json();