batchwatch

batchwatch › Keys

Keys

Self-service, no email, no confirmation, no password. You get a key by asking for one.

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/keys success response and the /v1/keys/current responses below were captured from a local wrangler dev instance 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. The 429 limit 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)

FieldTypeRequiredDefaultNotes
labelstringnounnamedYour 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.