Self-service, no email, no confirmation, no password. You get a key by asking for one.
That is safe because a key does not by itself carry any weight in the statistics: a vote has to be earned by measuring on at least three separate days (see interpreting.md). Ten fresh keys count for exactly as much as one — nothing.
Provenance. The
POST /v1/keyssuccess response and the/v1/keys/currentresponses below were captured from a localwrangler devinstance running this code. Key creation from the machine used to write these docs hit the per-IP daily limit on production, so the production success path could not be exercised. The429limit response was captured from production and is marked as such.
POST /v1/keys
No authentication. Runs before the authentication step in the router, for the obvious reason.
Body (JSON, optional)
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
label | string | no | unnamed | Your own note, e.g. prod-pipeline. Control characters are stripped, then trimmed to 60 characters. Never shown to anyone else. |
A body that is absent or unparseable is treated as {}.
Response
201. The token is shown once; only a SHA-256 hash is stored.
curl -X POST https://batchwatch.dev/v1/keys \
-H 'content-type: application/json' \
-d '{"label":"prod-pipeline"}'
Captured from a local wrangler dev instance (the token below is from a local throwaway key and does not work anywhere):
{
"token": "bw_Tp28Sh0rR9d4INTka1X1St7LXw_7DY3knYrtr3TTtK4",
"label": "prod-pipeline",
"tier": "free",
"warning": "This is the only time you will see this token. We store a hash, not the token, so we cannot recover it for you.",
"next": {
"contribute": "POST /v1/calls when a job starts, PATCH it when it finishes. That is all it takes to be a contributor.",
"unlocks": "5 measurements in the last 7 days unlocks the full API.",
"voting": "Measuring on 3 separate days makes your figures count towards the published percentiles. Until then your data is stored and shown, but pooled with everyone else's so that a new account cannot move the numbers."
}
}
Token format: bw_ followed by base64url of 32 random bytes.
Rate limit
At most KEYS_PER_IP_PER_DAY keys per IP per day — 5 by default. This is a noise limit, not a security boundary, and the response says so.
The limit is only enforced when the deployment has a TRIAL_SALT and Cloudflare supplied a cf-connecting-ip. Without either, keys are still issued: not being able to get started is considered worse than a runaway loop.
Captured from https://batchwatch.dev, 2026-08-25 14:06 UTC — 429:
{
"error": "too many keys from this address today",
"made_today": 5,
"limit": 5,
"note": "This is a noise limit, not a security one - a key does not by itself carry any weight in the statistics. If you genuinely need more, one key per service is plenty; keys are not per-machine."
}
GET /v1/keys/current
Your tier, contribution status and quota. Requires a key — 401 {"error":"no key given"} without one.
Captured from a local wrangler dev instance, immediately after creating the key:
{
"label": "prod-pipeline",
"tier": "free",
"contributing": false,
"recent_measurements": 0,
"required": 5,
"window_days": 7,
"delayed_by_s": 900,
"live": false,
"quota": { "calls_used": 0, "calls_limit": 0, "calls_left": 0, "window": "7 days" }
}
And after the same key had sent 7 completed measurements:
{
"label": "prod-pipeline",
"tier": "contributor",
"contributing": true,
"recent_measurements": 7,
"required": 5,
"window_days": 7,
"delayed_by_s": 300,
"live": false,
"quota": { "calls_used": 0, "calls_limit": 10000, "calls_left": 10000, "window": "7 days" }
}
Note that tier here is the effective tier, derived from your measurements, not the tier column stored on the key. Only paid is assigned by the operator; free and contributor are computed from data. See tiers.md.
quota.calls_limit: 0 on the free tier is not a bug — the four gated routes are not on quota for a non-contributor, they are on the free trial instead.
DELETE /v1/keys/current
Revokes the key you authenticate with. There is deliberately no /v1/keys/{id}: without accounts there is no answer to "who may revoke what", and being able to kill your own key is enough to make a leaked one harmless.
Requires a key — 401 {"error":"authenticate with the key you want to revoke"} without one.
Captured from a local wrangler dev instance:
{
"revoked": true,
"label": "prod-pipeline",
"note": "The key no longer authenticates. Measurements you already sent stay in the dataset - they were true when they were taken, and a record that can be deleted backwards is not a record."
}
Measurements are not removed. If you want them out of the aggregates, call DELETE /v1/calls/mine before revoking — after revocation the key can no longer authenticate, so that route becomes unreachable for it.
A revoked token no longer authenticates, and the caller is treated as anonymous from then on. Verified on a local instance: reusing the revoked token against GET /v1/keys/current returned 401 {"error":"no key given"} — the same message an unauthenticated caller gets, which is slightly misleading when a token was supplied. See ops.md#known-discrepancies.