Errors
The error envelope, every error code, and request IDs.
Errors are returned with an appropriate HTTP status and a consistent JSON envelope:
{
"error": {
"code": "forbidden_scope",
"message": "This token lacks the om:write scope required for this endpoint.",
"request_id": "req_9f2c1b7e4a",
"details": null
}
}code— a stable machine-readable string. Branch on this, not onmessage.message— human-readable explanation. Wording may change; do not parse it.request_id— the same value as theX-Request-Idresponse header.details— optional structured context (e.g. field-level validation issues).
Error codes
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Missing, malformed, revoked, or unknown API key. |
forbidden_scope | 403 | The token does not carry the scope this endpoint requires. The message names the missing scope. |
forbidden_module | 403 | The account does not have the module entitlement behind this endpoint. The message names the module. |
not_found | 404 | Unknown route, unknown site_id, or an item ID that does not exist (or belongs to a site this key cannot see). |
validation_error | 422 | The request body or query parameters failed validation. details describes the offending fields. |
idempotency_conflict | 422 | An Idempotency-Key was reused with a different request body. See Idempotency. |
rate_limited | 429 | Too many requests. Honor Retry-After — see Rate limits. |
conflict_safe_mode | 409 | The site is in commissioning safe mode: writes are frozen until commissioning completes. Reads keep working. Retry the write after safe mode is lifted. |
upstream_unavailable | 502/504 | The on-site edge gateway is offline or timed out. Live-data reads may fail while historical data still serves. Retry with backoff. |
upstream_error | 502 | The edge responded with an error. Retry with backoff; if it persists, contact support with the request ID. |
internal_error | 500 | Unexpected server error on our side. Safe to retry with backoff; report persistent occurrences. |
Handling errors
resp = client.get("/v1/sites/pv-ljubas/energy/hourly")
if resp.status_code >= 400:
err = resp.json()["error"]
match err["code"]:
case "rate_limited":
retry_after = int(resp.headers.get("Retry-After", "5"))
case "upstream_unavailable" | "upstream_error" | "internal_error":
... # transient: retry with exponential backoff
case "forbidden_scope" | "forbidden_module":
... # permanent: fix the token scopes / account entitlement
case _:
raise RuntimeError(f"{err['code']}: {err['message']} ({err['request_id']})")Treat 4xx codes (except 429) as permanent for the given request — retrying the identical call will fail the same way. Treat 429, 5xx, and the upstream_* codes as transient.
Request IDs
Every response — success or error — carries an X-Request-Id header. Log it alongside your own request logs, and include it in any support request: it lets us trace the exact call through the platform, including the hop to the on-site edge.

