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/recommend | 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), /badge/{provider}/{model}.svg (an embeddable live status badge — see Status badges below), /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.)
Status badges
Drop a live batchwatch median into any README, blog or status page. It is a plain SVG you <img>-embed, it updates itself, and it links back to the model's page. The number is the same measured p50 median /v1/coverage and /v1/wait report — never invented.
https://batchwatch.dev/badge/{provider}/{model}.svg
Copy-paste, Markdown:
[](https://batchwatch.dev/m/openai/gpt-5-nano)
Copy-paste, HTML:
<a href="https://batchwatch.dev/m/openai/gpt-5-nano">
<img src="https://batchwatch.dev/badge/openai/gpt-5-nano.svg"
alt="OpenAI gpt-5-nano batch queue time">
</a>
The provider and model are separate path segments — the model name keeps every hyphen and dot it has (gpt-5.6-sol, claude-haiku-4-5, gemini-3.7-flash). Use the same provider/model you would pass to /v1/wait.
The badge draws one of three states, and they look different on purpose:
- green — a measured median we can stand behind (a real number).
- grey — "measuring" — a model we track whose data is not yet thick enough to publish a percentile. Give it time; the badge fills in on its own.
- red — "unknown model" — a model name we have no measurements for at all (a typo, or one we do not track). This is not the same as "measuring".
A shorter /badge/{model}.svg form also works when the model name is unique across providers; otherwise use the {provider}/{model} form so it is unambiguous.
Let an agent call these routes for you — the MCP server
If your caller is an AI agent (Claude, Cursor, Cline, or anything that speaks MCP), it does not have to scrape a page about batchwatch — it can call batchwatch directly. The batchwatch MCP server (batchwatch-mcp, in the mcp/ folder of the client repo) wraps these read routes as six MCP tools: should_i_batch, estimate_batchtime, conditions, distribution, coverage and wait. When a model is deciding "batch this or run it synchronously?", it invokes the tool and gets the real measured queue evidence — the wait distribution, the current conditions, a graded confidence and how fresh the data is — passed through verbatim, never a fabricated number.
It runs on the same tier system as the rest of the API: without a key the tools read the public data delayed 15 minutes; a key earns that key's live figures, which is exactly what an agent needs to route real traffic. See clients for how to wire it into an MCP client. (Today it runs from source with node mcp/index.js; publication to npm as batchwatch-mcp is pending.)
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
Nine routes answer questions about queue time. Five are open to anyone (/v1/wait, /v1/recommend, /v1/curve, /v1/coverage, /v1/accuracy); four are gated…
Contributing measurements
There are two ways to get a measurement into the dataset, and they are not equally trusted.
Keys
A key is self-service: one request, no password, and no email needed to get started. You get a key by asking for one — and everything the free and…
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…