API reference
Hand-written from the Worker routes. Read-only, stateless, open data only. Every error uses one typed envelope: {"error":{"code":"...","message":"..."}}.
GET /healthz
Liveness plus the build stage marker.
GET /healthz
{"ok":true,"stage":8}
GET /api/v1/sites
Search servable sites. Filters: bbox (minLon,minLat,maxLon,maxLat), connector (case-insensitive substring, max 64 chars), minPower (kW floor), openOnly (true keeps AVAILABLE rows only). Pagination: limit (1–100, default 20), offset (0–100000, default 0).
GET /api/v1/sites?bbox=4.0,52.0,5.0,53.0&connector=CCS2&minPower=50&openOnly=true&limit=20&offset=0
{
"data": [{ "id": "OCM:900000", "source": "ocm", "licence": "CC-BY-4.0" }],
"page": { "limit": 20, "offset": 0, "total": 35 },
"attribution": [{ "text": "EnBW (via Open Charge Map)", "url": "https://openchargemap.io" }]
}
GET /api/v1/sites/:id
One servable site. IDs are source-prefixed (OCM:900000, OSM:node/22000101); encode the colon as %3A. Closed or unknown ids answer NOT_FOUND.
GET /api/v1/sites/OCM%3A900000
{"data": {"id": "OCM:900000", "source": "ocm", "licence": "CC-BY-4.0"}}
GET /api/v1/status/:id
Live status with a stale label against the 60-minute refresh SLO. Fixture rows predate the SLO, so expect stale:true with a staleReason (serving last-good static cut).
GET /api/v1/status/OCM%3A900000
{"data": {"id": "OCM:900000", "status": "AVAILABLE",
"retrievedAt": "2026-09-14T06:15:00.000Z", "source": "ocm",
"stale": true, "staleReason": "retrievedAt 2026-09-14T06:15:00.000Z is older than the 60min refresh SLO (serving last-good static cut)",
"attribution": {"text": "EnBW (via Open Charge Map)", "url": "https://openchargemap.io"}}}
Errors
| Code | Status | When |
|---|---|---|
| INVALID_ARGUMENTS | 400 | Bad bbox, connector, minPower, limit or offset |
| UNAUTHORIZED | 401 | Missing or invalid API key when the free-key gate is set |
| RATE_LIMITED | 429 | More than 60 requests per minute per IP (retry-after header set) |
| NOT_FOUND | 404 | Unknown route, or unknown/closed site id (never a closed signal) |
| UPSTREAM_BLOCKED | 403 | Non-allowlisted or private-origin request refused before any outbound call |
| PAYLOAD_TOO_LARGE | 413 | Request body over the cap, rejected before parsing |
{"error": {"code": "NOT_FOUND", "message": "unknown site id"}}
MCP transport
The same four tools are served over Streamable HTTP at POST /mcp (use tools/list then tools/call). Non-POST methods answer the typed JSON-RPC error with status 405 — there is no session to hold. See MCP onboarding.
POST /mcp # JSON-RPC tools/list, tools/call — 200
GET /mcp # 405 Method not allowed: use POST /mcp for Streamable HTTP (stateless)