Base URL: https://batchwatch.dev (https://api.batchwatch.dev and https://www.batchwatch.dev are the same worker — no redirect).
Everything below was written by reading src/index.js and then calling the routes. Every example response in these documents is a real captured response, and each one says where it came from. Nothing here is invented.
Provenance of the examples
Two sources, and they are always labelled:
| Label | What it means |
|---|---|
Captured from https://batchwatch.dev | A real call against production on 2026-08-25. The numbers are whatever the live dataset held at that moment. |
Captured from a local wrangler dev instance | The same code, run locally against a local D1 seeded with synthetic measurements. Used only for routes that cannot be reached from an unauthenticated caller, or for response shapes production could not produce at the time. The numbers in those examples are not real queue times. |
Why some examples are local: creating an API key from the machine used for this work returned 429 too many keys from this address today, so the key-authenticated routes could not be exercised against production. Writing measurements into production to demonstrate the ingest routes was deliberately not done — the dataset is the product, and it must not contain data that was made up to illustrate documentation.
Routes
| Method | Path | Key required | Delayed by tier |
|---|---|---|---|
| GET | /health | no | n/a |
| GET | /v1/status | no | no |
| GET | /v1/probe | no | no |
| GET | /v1/outages | no | no, never |
| GET | /v1/outages.atom | no | no, never |
| GET | /v1/coverage | no | no |
| GET | /v1/wait | no | yes |
| GET | /v1/curve | no | yes |
| GET | /v1/should-i-batch | contribution or free trial | yes |
| GET | /v1/b2b/should-i-batch | contribution or free trial | yes |
| GET | /v1/estimate-batchtime | contribution or free trial | yes |
| GET | /v1/conditions | contribution or free trial | yes |
| GET | /v1/distribution | contribution or free trial | yes |
| POST | /v1/subscriptions | yes | n/a |
| GET | /v1/subscriptions | yes | n/a |
| DELETE | /v1/subscriptions/{id} | yes | n/a |
| POST | /v1/keys | no | n/a |
| GET | /v1/keys/current | yes | n/a |
| DELETE | /v1/keys/current | yes | n/a |
| POST | /v1/calls | no (optional) | n/a |
| PATCH | /v1/calls/{id} | no (optional) | n/a |
| POST | /v1/calls/complete | yes | n/a |
| GET | /v1/calls/mine | yes | n/a |
| DELETE | /v1/calls/mine | yes | n/a |
| POST | /v1/rollup/refresh | yes | n/a |
| OPTIONS | any | no | n/a |
Plus the non-API pages served by the same worker: / and /index.html (dashboard, HTML), /m/{provider}/{model} and /p/{provider} (public model and provider pages, HTML), /robots.txt, /sitemap.xml. See ops.md.
Routes that do not exist, despite being named in docs/KRAVSPEC.md: /v1/batches, /v1/batches/complete, /v1/forecast, /v1/tradeoff, /v1/anomalies, /v1/status.atom. All six return 404 (verified against production on 2026-08-25; /v1/coverage returned 200 in the same run as a positive control). KRAVSPEC is out of date; this reference describes the code.
Authentication
Authorization: Bearer bw_...
The token is hashed with SHA-256 and compared against api_key.token_hash. A revoked key (revoked_at set) does not authenticate.
An absent or unrecognised header is not an error on most routes — the caller is simply treated as anonymous. Only the routes marked "key required" above return 401.
Get a key with POST /v1/keys. No email, no confirmation.
Path and method handling
- Trailing slashes are stripped before matching:
/v1/coverage/behaves as/v1/coverage(verified: both returned200). - Method is part of the match.
POST /v1/waitreturns404 unknown route, not405(verified). /healthis matched without a method check, soPOST /healthalso returns200(verified).- Unmatched paths return
404with{"error":"unknown route","see":"/v1/coverage"}.
CORS
All JSON responses carry access-control-allow-origin: * and cache-control: no-store. OPTIONS on any path returns 204 with:
access-control-allow-origin: *
access-control-allow-methods: GET,POST,PATCH,OPTIONS
access-control-allow-headers: authorization,content-type
Note that DELETE is not in access-control-allow-methods, although DELETE /v1/calls/mine and DELETE /v1/keys/current exist. Browser preflight for those two routes will therefore fail; call them server-side. (Verified against production 2026-08-25 — see the discrepancy list at the bottom of ops.md.)
The rest of this reference
- query.md — the measurement routes.
- ingest.md — contributing measurements, exporting and excluding them.
- keys.md — creating, inspecting and revoking a key.
- ops.md — health, self-check, prober, outages, HTML pages.
- errors.md — every status code and what it means.
- tiers.md — tiers, delay, contribution, quota, free trial.
- interpreting.md —
no_coverage,confidence,basis,n,freshness, and why there is never a point ETA. - versioning.md — what
/v1promises, what counts as a breaking change, what does not, and how we would give notice. - sandbox.md —
?_force=for exercising every outcome without waiting for a real queue. Disabled on batchwatch.dev.
Quickstart
# open, no key, delayed 15 minutes
curl 'https://batchwatch.dev/v1/wait?provider=openai&model=gpt-5-nano'
# get a key
curl -X POST https://batchwatch.dev/v1/keys \
-H 'content-type: application/json' \
-d '{"label":"prod-pipeline"}'
# contribute: start a measurement when your batch job starts
curl -X POST https://batchwatch.dev/v1/calls \
-H "authorization: Bearer $BW_KEY" -H 'content-type: application/json' \
-d '{"provider":"openai","model":"gpt-5-nano","mode":"batch","requests":1200,"input_tokens":840000}'
# ...and close it when the job finishes
curl -X PATCH https://batchwatch.dev/v1/calls/c_xxxxxxxxxxxxxxxxxxxx \
-H "authorization: Bearer $BW_KEY" -H 'content-type: application/json' \
-d '{"status":"completed","output_tokens":250000}'
Five completed measurements in the last seven days makes you a contributor, which unlocks the four gated routes and cuts the delay from 900s to 300s.
Reference
Measurement routes
Seven routes answer questions about queue time. Three are open to anyone (/v1/wait, /v1/curve, /v1/coverage); four are gated behind a contribution or the…
Contributing measurements
There are two ways to get a measurement into the dataset, and they are not equally trusted.
Keys
Self-service, no email, no confirmation, no password. You get a key by asking for one.
Tiers, delay, quota and the free trial
Two things are sold separately, and the split is the whole design:
Error model
Every error is JSON with an error field holding a human-readable message, plus route-specific fields. There is no error code enumeration — the status code…
Reading an answer
Most responses carry the same four disclosure fields: confidence, basis, n and freshness. They exist because the alternative — a single number with no…
Operational and public routes
Health, self-check, prober state, outage feeds, the forced rollup, and the HTML pages the same worker serves.
Outage alerts
Batchwatch watches every model it measures and opens an outage when the queue for one of them degrades against its own baseline. This page is how you get…