Log x-sume-request-id and Idempotency-Key on every call (Python)
A requests response hook that writes one JSON log line per Sume call: x-sume-request-id, idempotency key, error code and rate-limit headers. Tested.

Log one JSON line per Sume call from a requests response hook, so every request records the x-sume-request-id header, the Idempotency-Key you sent, the status, the error.code from the body and the ratelimit-remaining header. When a customer says an image never arrived, that line tells you which call to quote to support and whether a retry reused the same key.
The hook below sits on a Session, so no call site has to remember to log. I ran it against a local stand-in for the API and checked the lines for a 429 and for a 202.
Which fields are worth a log line?
Sume's docs name several identifiers, and they are not interchangeable. The OpenAPI describes x-sume-request-id as a Sume-owned HTTP correlation id that matches ^req_[a-f0-9]{32}$, says to include it when contacting support, and says it is separate from the request_id on a generation job. The Formats error docs add that an error body's request_id is also sent as that header. So the header is the one to log on every call, whether it succeeded or not.
The rest answer different questions: did this retry carry the same key, why did it fail, and how close am I to the limit. The headers ratelimit-limit, ratelimit-remaining, ratelimit-reset and retry-after can appear on a response, per the docs, so every one is read with get and may be empty.
| Field | Where it comes from | What it answers |
|---|---|---|
sume_request_id | x-sume-request-id response header | Which call to quote to support |
idempotency_key | The request header you sent | Whether a retry reused the key |
error_code | error.code in the body | A stable token to switch on, never the message |
error_request_id | error.request_id in the body | The id on the error itself, kept for comparison |
ratelimit_remaining | ratelimit-remaining header, when present | How close the workspace is to the request limit |
retry_after | retry-after header, when present | How long Sume asked you to wait |
What does the hook look like?
log_call runs after every response on the session. It tolerates a non-JSON body, because a proxy in front of you can return an HTML error page, and it never touches the Authorization header: the key is set on the session and not logged. A context variable carries your own order id, so every line from one request handler is tagged without passing it through each function.
import contextvars, json, logging, os, requests
order_id = contextvars.ContextVar("order_id", default=None)
log = logging.getLogger("sume")
def log_call(r: requests.Response, *args, **kwargs) -> None:
try:
err = r.json().get("error") or {}
except ValueError: # an HTML error page from a proxy, not Sume
err = {}
log.info(json.dumps({
"order": order_id.get(), "method": r.request.method,
"path": r.request.path_url.split("?")[0], "status": r.status_code,
"ms": round(r.elapsed.total_seconds() * 1000),
"sume_request_id": r.headers.get("x-sume-request-id"), # quote this to support
"idempotency_key": r.request.headers.get("Idempotency-Key"),
"error_code": err.get("code"), "error_request_id": err.get("request_id"),
"ratelimit_remaining": r.headers.get("ratelimit-remaining"),
"retry_after": r.headers.get("retry-after")}))
def sume_session() -> requests.Session:
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['SUME_API_KEY']}" # never logged
s.hooks["response"].append(log_call)
return s
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(message)s")
order_id.set("8823")
api = sume_session()
api.post("https://api.sume.com/v1/image-1.0/generate", timeout=30,
json={"prompt": "Matte black bottle", "mode": "async"},
headers={"Idempotency-Key": "order-8823-hero-v1"})What should stay out of the log?
Sume's docs say not to send API keys, signing secrets or raw media URLs when you write in, and the same caution applies to your own logs. The sample records a path, a status and ids, and deliberately leaves out the prompt, the request body and the response body. Artifact URLs are the other thing to leave out, since they point at the media itself.
One limit to know: a requests response hook fires once per response your code receives. If you let a urllib3 Retry adapter resend a request behind the scenes, the intermediate responses are never seen by the hook, and only the last one is logged. Retry in your own loop, or in a task runner, when you want one line per attempt, and keep the same Idempotency-Key on every attempt so the lines can be joined.
The path does contain the job id on status and result calls. That is an identifier you generated work under, not a secret, but treat it with the same access controls as your order ids.
- Log the header on success too; a support case often starts from a call that returned
202. - Log your own order id beside it, so one search finds everything.
- Switch on
error_code, never on the message, which the docs say may change. - Keep the line single-JSON so any log tool can index the fields.
- Never log the bearer token, a webhook signing secret or signed media links.
How do you use these lines when something fails?
Search by order id, read the calls in order, and look at three things. Did every attempt carry the same idempotency_key? Did a 429 carry a retry_after that your code honoured? And did a 5xx get followed by a replay with the same key rather than a new one? If all three are yes, the system did what it should, and the remaining question is the job itself, which the status call answers.
When you do need Sume's help, quote the sume_request_id of the failing call, the job or run id you hold, and the error_code. Those three give support everything the docs ask for without exposing a secret.
Sources
Related posts
More in Developers
- 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.
- 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.
Written by Sume