Authentication
Last verified: 2026-05-03 (Task #334 — docs accuracy pass).
All Weather Intelligence endpoints require an API key. Keys identify
both the developer account and the environment (test vs
production) that calls are billed against.
Sending the key
Use either header on every request:
| Header | Example |
|---|---|
Authorization: Bearer <key> | Authorization: Bearer tm_weather_abc123... |
X-API-Key: <key> | X-API-Key: tm_weather_abc123... |
If both are present, Authorization wins. If neither is present you
get api_key_missing (HTTP 401).
Key format
tm_weather_<32+ hex chars>
- The
tm_weather_prefix is fixed and always plaintext. - The
tm_weather_<prefix>...is logged in plaintext for debugging. Only the hash of the full key is persisted indeveloper_api_keys.key_hash. - The plaintext key is shown to you exactly once on creation. If you lose it, you have to mint a new key.
Scopes
Every endpoint requires at least one scope. The scope set is fixed — unknown scope values are ignored at provision time.
| Scope | Required by |
|---|---|
weather:read | /v1/weather/current, /v1/weather/forecast, /v1/weather/health |
weather:timeline | /v1/weather/timeline |
weather:route | /v1/weather/route |
weather:watch | /v1/weather/watch (POST/GET/DELETE) |
weather:webhooks | /v1/webhooks (POST/GET/DELETE) and /v1/webhooks/{id}/attempts |
weather:admin | Wildcard — satisfies any scope check. Issued to internal tooling only. |
/v1/usage is special: any valid key can read its own usage. No
extra scope is required.
A request whose key is missing the required scope gets
scope_required (HTTP 403) with a details.required field naming the
scope that was missing.
Environments
Each developer account has an environment field of test or
production. The environment a request hit is surfaced in
meta.environment on every response, so a misconfigured client can
notice it's calling the wrong account.
For the MVP there's no separate test base URL — calls hit the same
endpoints, but test accounts get isolated quotas, isolated webhook
endpoints, and a separate billing ledger.
Internal keys
Travelmode itself calls the public API for service-to-service
weather lookups. Those calls are signed by an internal key
(developer_accounts.is_internal = true), provisioned via
scripts/provision-internal-weather-key.ts.
Internal keys:
- Bypass scope enforcement. Every endpoint is callable.
- Bypass rate-limit enforcement. All requests are still logged via
the standard
api_usage_eventspipeline. - Are still subject to webhook signing rules. Internal keys do not get to skip HMAC verification on the receiving end.
Internal keys are not minted for external developers.
Key rotation
Mint a fresh key with the same scopes and revoke the old one once your client has rolled over. The hashed-only storage means there's no "reveal" path for an existing key — rotate and re-deploy.
Errors at a glance
| Code | HTTP | Cause |
|---|---|---|
api_key_missing | 401 | No Authorization and no X-API-Key header. |
api_key_invalid | 401 | Header present but the key prefix doesn't resolve, or the hash doesn't match. |
api_key_revoked | 401 | Key was explicitly revoked by an admin. |
api_key_expired | 401 | Key has an expires_at and it's in the past. |
scope_required | 403 | Key valid but missing the scope this endpoint requires. |
account_suspended | 403 | Developer account has been suspended. |
See errors.md for the full table and example
envelopes.