A Python SumeError class: code, retryable, next_action, request id
Turn a Sume error envelope into one Python exception that carries code, retryable, retry_after_seconds, next_action and the request id. Standard library only.

urllib raises HTTPError for any status of 400 or above, and its message is just HTTP Error 402: Payment Required. The useful part is in the body. The Sume API wraps failures in an error object, and the fields your code should act on sit inside it. Wrapping them once, at the edge of your client, keeps the rest of the program from re-parsing JSON in every except block.
The envelope carries code, message, request_id, retryable, retry_after_seconds and next_action. The same request id is sent in the x-sume-request-id response header, which is the better source when the body is empty or is not JSON, such as a gateway error page.
What to copy out of the envelope
| Field | Use it for |
|---|---|
| code | Branching, for example insufficient_credits or rate_limited |
| retryable | Whether a retry can ever succeed without a change from you |
| retry_after_seconds | How long to wait first, when the API names a window |
| next_action | A hint such as add_funds that a human or a router can act on |
| request_id | The value to quote to support, also in x-sume-request-id |
One exception, built at the edge
The class below reads the fields defensively with .get, because a missing field is normal. A 402 carries no retry hint, and a proxy error has no envelope at all. from None hides the original HTTPError so a log shows one clean line. The final lines run against a job id that returns the wallet error and print the fields.
import json, os, urllib.error, urllib.request
class SumeError(Exception):
def __init__(self, status: int, body: dict, header_request_id: str | None):
e = body.get("error") or {}
super().__init__(f"{status} {e.get('code')}: {e.get('message')}")
self.status, self.code = status, e.get("code")
self.retryable = bool(e.get("retryable"))
self.retry_after = e.get("retry_after_seconds")
self.next_action = e.get("next_action")
self.request_id = header_request_id or e.get("request_id")
def get(path: str) -> dict:
h = {"x-api-key": os.environ["SUME_API_KEY"]}
req = urllib.request.Request("https://api.sume.com/v1" + path, headers=h)
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
except urllib.error.HTTPError as e:
try:
body = json.load(e)
except ValueError:
body = {}
raise SumeError(e.code, body, e.headers.get("x-sume-request-id")) from None
try:
get("/jobs/poor")
except SumeError as err:
print(err, "| retryable:", err.retryable, "| next:", err.next_action, "| id:", err.request_id)How to use it
- Branch on
statusandcode, as the docs advise. Treatcategoryandstageas coarse labels for dashboards, not for control flow. - Retry only when
retryableis true, and waitretry_afterfirst when it is set. A402 insufficient_creditscarriesretryable: falseandnext_action: add_funds, so a loop should stop and surface it. - Log
request_idon every raised error. It is the one field that lets support find the exact request. - Keep the status in the exception. Some codes, such as
idempotency_conflict, are a 409 that has its own recovery path, and a bare message would hide that.
The error fields are described in the errors reference, and the job routes that return them are in the jobs guide.
Sources
Related posts
More in Developers
- Python webhook receiver: read artifacts[].url and error.next_action
A standard-library Python receiver for Sume job webhooks: verify, then branch on status OK or ERROR to get the artifact URL or error.retryable and next_action.
- Does a queue_full 429 charge me? Sume's reservation rules
A Sume 429 queue_full means the workspace has no accepted-job capacity left. The failed admission releases its reservation; retry with the same Idempotency-Key.
- IN_QUEUE or queued? Two status fields on a Sume job, do not mix
GET /v1/jobs/{id}/status returns sume_status and a queue-shaped status that map one to one. Which to poll, and how /v1/videos values differ.
- Quota job error vs 402 insufficient_credits: where each appears
A 402 insufficient_credits means the submit was refused; a quota job category means an accepted job later failed. How to tell them apart and what to do next.
Written by Sume