title: "Authentication" description: "How to authenticate API requests — bearer tokens, key format, rotation, and errors." last_updated: "2026-05-24"
Authentication
All endpoints under https://api.boxwatch.app (except public routes) require a bearer token. There is one auth scheme for the API: send your key as Authorization: Bearer <key>.
Getting a key
Generate a key from the dashboard — see API keys for the full walkthrough.
The dashboard shows the key exactly once when created. Copy it then; only the prefix is shown after that.
Using the key
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.boxwatch.app/serversThe Authorization header is the only place the API reads the token from. Query-string tokens are not accepted on authenticated endpoints.
fetch('https://api.boxwatch.app/cron-checks', {
headers: { 'Authorization': 'Bearer ' + process.env.BOXWATCH_API_KEY },
});Token format
Full-scope keys are 67 characters:
bw_4f3a91e8c7d2b15a8e0f47c93b6d28a05c1f9e7b3a4d68f72c5e93a17b8d04e6f
The bw_ prefix is followed by 64 hex characters generated from crypto.randomBytes(32) — 256 bits of entropy.
Two other key prefixes exist for narrower use:
bw_api_— bound to a single custom API endpoint, sent viaX-API-Keyheader or?key=query param.bw_tv_— bound to a single TV dashboard, sent the same way.
Endpoint and TV keys do not authenticate the main resource API — they only unlock their bound public route.
Token rotation
There is no rotation endpoint. To rotate:
DELETE /api-keys/:idthe existing key (or revoke it in the dashboard).POST /api-keysto mint a new one.- Roll consumers over to the new key.
Only one full-scope key per account is allowed at any time — you must delete the existing one before creating a replacement.
Revocation is immediate. The instant you delete a key, the next request that uses it returns 401. Plan rotations so consumers can re-read the new key before the old one disappears.
Errors
| Status | Body | When |
|---|---|---|
401 | { "error": "No token provided" } | Missing Authorization header. |
401 | { "error": "Invalid or expired token" } | Token doesn't validate (wrong format, deleted, or expired). |
401 | { "error": "User not found" } | Token's account was deleted. |
500 | { "error": "Authentication error" } | Internal error during auth. Safe to retry. |
Every error body also includes a timestamp field with the server's UTC time.
Rate limiting
Rate limits are applied per IP, not per key, and trip on aggressive bursts.
| Scope | Limit | Notes |
|---|---|---|
| Global (all requests) | 100 / minute / IP | 429 Too many requests, please slow down. Agent heartbeats are excluded. |
/auth/login, /auth/register, /auth/forgot-password | 5 / 15 minutes / IP | Returns 429 Too many attempts, please try again later. |
/admin/* | 30 / minute / IP | Admin routes only. |
/ping/:slug cron heartbeats | 60 / minute / slug | Excess pings return 200 OK but aren't recorded. |
A 429 response includes the standard RateLimit-* headers. Back off and retry.
CORS
CORS allowlist is set from the CORS_ORIGINS environment variable, comma-separated. The production deployment allows requests from the BoxWatch dashboard origin only. Credentials (Access-Control-Allow-Credentials: true) are enabled for cookie-based browser sessions.
If you're calling the API from a server (curl, Node, Python, Go, etc.) CORS does not apply. If you need cross-origin browser calls from a domain you control, contact support to request an allowlist addition.
Browser cookies
The dashboard at app.boxwatch.app authenticates with an httpOnly cookie named boxwatch_token, also accepted by the same auth middleware. This cookie is not part of the public API. Don't try to obtain or use it from scripts — your bearer token works everywhere the cookie does, with no expiry quirks.
See also
- API overview
- API keys — creating and revoking
- Public routes — endpoints that don't require a token