Parse the Sume error envelope into a Python dataclass and exception
Sume errors share one envelope: code, request_id, retryable, retry_after_seconds, next_action, category and stage. Turn it into a typed Python exception.

Every error from the Sume API has the same shape, so one parser can serve every endpoint. The body is { "error": { ... } } and the object holds code, message, request_id, category, stage, retryable, retry_after_seconds, public_reason, next_action and, when relevant, details. A typed exception that carries those fields lets the rest of your code branch on them and never on message text.
Fields worth branching on
| Field | Type | Use |
|---|---|---|
| code | string | Stable identifier such as rate_limited or idempotency_conflict |
| request_id | string | Quote it to support and in your logs |
| retryable | boolean | Whether sending again can help |
| retry_after_seconds | number or null | Seconds to wait before the retry |
| next_action | string | For example retry_later, fix_input or contact_support |
Why not the message
The message is for people and can change. The code and next_action are the contract. retryable is the field that keeps a 503 for a missing provider (do not retry) apart from a 503 for a redeploy (retry in 5 seconds).
Dataclass and exception
from_body accepts anything and still returns an object, so a proxy that replaces the body with HTML does not crash the handler. The status comes from the HTTP response and is kept alongside the envelope.
from dataclasses import dataclass, field
@dataclass
class SumeError(Exception):
status: int
code: str = "unknown"
message: str = ""
request_id: str | None = None
retryable: bool = False
retry_after_seconds: float | None = None
next_action: str | None = None
details: dict = field(default_factory=dict)
@classmethod
def from_body(cls, status, body):
err = body.get("error", {}) if isinstance(body, dict) else {}
return cls(status, err.get("code", "unknown"), err.get("message", ""), err.get("request_id"),
bool(err.get("retryable")), err.get("retry_after_seconds"),
err.get("next_action"), err.get("details") or {})
def __str__(self):
return f"{self.status} {self.code} (request {self.request_id}): {self.message}"
e = SumeError.from_body(503, {"error": {"code": "deploy_draining", "retryable": True, "retry_after_seconds": 5}})
print(e.retryable, e.retry_after_seconds) # True 5Use it
Raise it from your HTTP wrapper after any non-2xx response. Callers then write except SumeError as e: if e.retryable: .... Keep request_id in every log line, and the single value is what support needs to find the request.
Where the envelope appears
The envelope is not limited to one route. A 429 carries details with the limit and scope. A 400 unknown_parameter carries details.errors with a suggestion and details.accepted with the valid keys. A 503 carries the retry delay in retry_after_seconds. Because all of them share one shape, the details field is a plain dictionary in the dataclass and each caller reads only what it needs.
The response also carries an x-sume-request-id header, which the OpenAPI schema describes as the correlation id to include when you contact support. Record whichever is easier to reach in your stack, and log it next to the job id.
Sources
Related posts
More in Developers
- Get the Sume webhook secret with GET /v1/webhooks/signing-secret
Read the workspace webhook secret with an account:read key and load it as SUME_COM_WEBHOOK_SIGNING_SECRET without printing it. A Python deploy step.
- GET /v1/jobs 400 unknown_parameter: a typo'd filter no longer widens
A misspelled query key on GET /v1/jobs, such as state for status, now returns 400 with a suggestion instead of a full unfiltered page. Handle it in TypeScript.
- Sume job status headers: cache-control no-store and x-sume-poll-after
GET /v1/jobs/:id/status is never cacheable and sends x-sume-poll-after: 2. What each header means for CDNs, browsers and a TypeScript poll loop.
- Cold Sume API key burst: first requests get the Free 120-write floor
The API reads your plan after auth, so a first-seen key is judged at the Free 120 write floor. Warm a Pro or Scale key with GET /v1/me before a burst.
Written by Sume