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.

5 min readSume
All posts

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.

What each logged field answers, from Sume's docs, read 2026-10-03
FieldWhere it comes fromWhat it answers
sume_request_idx-sume-request-id response headerWhich call to quote to support
idempotency_keyThe request header you sentWhether a retry reused the key
error_codeerror.code in the bodyA stable token to switch on, never the message
error_request_iderror.request_id in the bodyThe id on the error itself, kept for comparison
ratelimit_remainingratelimit-remaining header, when presentHow close the workspace is to the request limit
retry_afterretry-after header, when presentHow 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

All Developers posts

Written by Sume