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
- How to test a webhook URL before a Sume Format run uses it
POST /v1/webhooks/test-deliveries sends a signed webhook.test event to your URL. See the scope, the response fields, and the secret check, with no paid run.
- Transactional outbox for paid API calls in Python (Sume)
Write the Sume request and its Idempotency-Key in the order's transaction, drain later: a tested Python outbox that survives crashes, 429s and 409s.
- Transcribe a three-hour recording when the API caps at ten minutes
Streaming sessions end at an hour; Sume STT jobs take up to 600 seconds. Split with Timeline audio, transcribe 18 chunks, and stitch word times back together.
- Translate an SRT and burn it in: Sume caption cues, limits, Python
Sume takes no SRT upload, but caption cues take the same text and times. A Python converter, the 200-cue and 60-second limits, and which fonts apply.
Written by Sume