DynVoltDYNVOLT · developers

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 on message.
  • message — human-readable explanation. Wording may change; do not parse it.
  • request_id — the same value as the X-Request-Id response header.
  • details — optional structured context (e.g. field-level validation issues).

Error codes

CodeHTTPMeaning
unauthorized401Missing, malformed, revoked, or unknown API key.
forbidden_scope403The token does not carry the scope this endpoint requires. The message names the missing scope.
forbidden_module403The account does not have the module entitlement behind this endpoint. The message names the module.
not_found404Unknown route, unknown site_id, or an item ID that does not exist (or belongs to a site this key cannot see).
validation_error422The request body or query parameters failed validation. details describes the offending fields.
idempotency_conflict422An Idempotency-Key was reused with a different request body. See Idempotency.
rate_limited429Too many requests. Honor Retry-After — see Rate limits.
conflict_safe_mode409The site is in commissioning safe mode: writes are frozen until commissioning completes. Reads keep working. Retry the write after safe mode is lifted.
upstream_unavailable502/504The on-site edge gateway is offline or timed out. Live-data reads may fail while historical data still serves. Retry with backoff.
upstream_error502The edge responded with an error. Retry with backoff; if it persists, contact support with the request ID.
internal_error500Unexpected 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.