Python 3.15 UTF-8 default: still verify Sume webhooks on raw bytes
Python 3.15 makes UTF-8 the default for open() without an encoding. It does not change that a Sume signature covers raw body bytes, so verify before any parse.

Python 3.15's UTF-8 default does not change how a Sume webhook is verified: Sume signs the raw JSON body with HMAC-SHA256 over <timestamp>.<raw_body>, so your Python handler must compute the digest over the exact bytes that arrived, before any JSON parse or re-serialize. What 3.15 does change is the hidden-encoding case around it, such as reading a stored secret file or writing a received event to disk with open() and no encoding.
The Python facts come from the What's New in Python 3.15 page, which was still labelled a release candidate and a draft when I read it on 2026-10-03; confirm the final release notes before you rely on it. The Sume facts come from Webhooks, Run webhooks and Verifying webhooks. I did not run a receiver on 3.15.
What exactly changes in Python 3.15?
Per the release notes (PEP 686), Python uses UTF-8 as the default encoding independent of the system environment, so I/O without an explicit encoding argument uses UTF-8. It applies only when no encoding is given, can be turned off with PYTHONUTF8=0 or -X utf8=0, and the notes still advise passing encoding explicitly for compatibility between versions. encoding='locale' selects the locale encoding and has been supported since 3.10.
| Step | Encoding matters? | What to do |
|---|---|---|
| Computing the HMAC | No, it is bytes | Use the raw request body bytes as received |
| Building the signed message | Only if you decode first | Join timestamp, a period and the raw bytes; do not decode and re-encode |
| Reading the signing secret from a file | Yes, if you call open() without encoding | Pass encoding='utf-8' explicitly |
| Writing the event to disk before replying | Yes, same reason | Write the raw bytes, or pass encoding='utf-8' |
| Parsing JSON after verification | No, parse the verified bytes | Parse once, after the signature passes |
What must the verifier still do?
Reject a timestamp outside your replay window, five minutes by default in Sume's docs, and accept the delivery when any sume-v1= entry in x-sume-webhook-signature matches: during a secret rotation the header carries one entry per live secret, newest first, comma separated. Compare with hmac.compare_digest.
Then return any 2xx after durably recording the event. A job webhook has a 10-second timeout per attempt and a fixed 30-second default gap; a run webhook has the same 10-second timeout with exponential backoff capped at one hour. Dedupe on job_id for job events and request_id for run events, and keep status_url polling as the backup.
Sources
Related posts
More in Developers
- Python handler for the Sume agent.run.terminal webhook
Verify the HMAC over timestamp.raw_body, refuse an empty secret, dedupe on request_id and branch on outcome. Runs offline with a self-test.
- Python: list Sume video models that accept a video input
A 20-line Python script reads GET /v1/videos/models and prints every model whose supported_input_references include video_url, with its duration range.
- Python match on a Sume run status: terminal is not success
A Format run can be terminal as completed, failed, canceled or skipped. A structural match that never treats done as success, with a null-output case.
- Poll a Sume job in Python with a deadline and next_poll_after_seconds
A Python wait loop for a Sume job: stop on terminal, sleep at least next_poll_after_seconds, and give up at a monotonic deadline. A timeout is not a failure.
Written by Sume