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: 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.
- 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.
- 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.
Written by Sume