Python 3.10 is end of life: a stdlib Sume webhook verifier
Python 3.10 has reached end of life. A standard-library verifier for Sume's signed webhooks that refuses an empty secret and accepts rotated signatures.

Python 3.10 has reached end of life according to the Python downloads page, so plan to run webhook receivers on a supported release. The code below is a Sume webhook verifier that uses only the standard library, so it runs unchanged on 3.11 and later and has nothing to upgrade except the interpreter.
Sume does not document a Python SDK; the official client is TypeScript. In Python you call the HTTP API directly and verify webhooks yourself.
What the Python page lists
Relevant items only.
| Item | What the page says |
|---|---|
| 3.14.8 | Current 3.14 release |
| 3.10 | End of life reached |
| 3.15 | Pre-release |
What the signature covers
Sume signs the raw body with HMAC SHA 256 over <timestamp>.<raw_body> and sends x-sume-webhook-timestamp plus x-sume-webhook-signature: sume-v1=<hex>. During a rotation the header carries one entry per live secret, newest first, comma separated, and you accept the delivery if any entry matches. Reject callbacks outside a replay window; five minutes is the documented default.
The verifier
Pass the raw request bytes, not a parsed and re-serialized object. The function refuses an empty secret, which would otherwise make every signature trivially forgeable, and it compares every entry so timing does not reveal which one matched.
import hashlib
import hmac
import time
def verify_sume_webhook(raw_body: bytes, timestamp: str, signature_header: str,
secret: str, tolerance: int = 300) -> bool:
if not secret or not signature_header:
return False
try:
ts = int(timestamp)
except (TypeError, ValueError):
return False
if abs(int(time.time()) - ts) > tolerance:
return False
signed = str(ts).encode() + b"." + raw_body
digest = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
expected = f"sume-v1={digest}".encode()
matched = False
for entry in signature_header.split(","):
if hmac.compare_digest(entry.strip().encode(), expected):
matched = True
return matchedUsing it in a handler
Whatever framework you use, the order is the same.
- Read the secret from
SUME_COM_WEBHOOK_SIGNING_SECRET, the name the delivery worker signs with. You can reveal it on the Webhooks tab of the dashboard. - Call the verifier with the raw bytes and both headers. On
False, return a non-2xx status and stop. - Parse the JSON only after verification, and store the event keyed by
job_idbefore answering2xx. - Job webhooks and run webhooks share one signing secret, so the same function covers both.
A quick self-test
Sign a sample body with a known secret and confirm the function accepts it, rejects a changed body, and rejects an empty secret. If you build the test header by hand, remember the format is sume-v1= followed by the hex digest.
if __name__ == "__main__":
secret, body = "test-secret", b'{"event":"job.completed"}'
ts = str(int(time.time()))
sig = hmac.new(secret.encode(), ts.encode() + b"." + body,
hashlib.sha256).hexdigest()
header = f"sume-v1={sig}"
print(verify_sume_webhook(body, ts, header, secret)) # True
print(verify_sume_webhook(body + b" ", ts, header, secret)) # False
print(verify_sume_webhook(body, ts, header, "")) # FalseMoving off 3.10
The verifier does not depend on anything introduced after 3.10, so it is safe to deploy before the upgrade, which makes it a low-risk first piece to move. When you do upgrade, rerun the self-test on the new interpreter and check that the framework you use to read raw request bytes still returns bytes, not text. A framework that decodes the body for you will break the signature, because the signature is computed over the exact bytes Sume sent.
If your service polls jobs instead of receiving webhooks, the same standard library covers it. Poll GET /v1/jobs/{id}/status with backoff, stop on terminal, and read the result once result_ready is true.
Delivery behavior to remember
Sume retries a delivery up to 10 times with a fixed spacing, 30 seconds by default, and a 10 second timeout per attempt. Answer 2xx only after the event is stored. Delivery is an optimization, not the only recovery path, so keep status_url polling available for events that never arrive.
Sources
Related posts
More in Developers
- Recraft V4.1 Flash: median 1.3 s, p95 1.8 s. Set timeouts from p95
Recraft quotes a median of about 1.3 seconds and a p95 of 1.8 seconds for V4.1 Flash. How to turn latency claims into timeouts and polling for image APIs.
- Reproduce the same AI voiceover later: model id, voice, settings
To redo a narration line months later you need the model id, voice, language, format and settings. Sume's completed TTS job records them. A short routine.
- Shopify rejects file names ending in thumb, icon or large
Shopify file uploads reject names ending in pico, icon, thumb, testing, small, compact, medium, large or grande. A Python rename step for batch outputs.
- Shopify image limits: 20 MB, 25 megapixels vs Sume image outputs
Shopify accepts product images up to 20 MB and 25 megapixels in JPEG, PNG, WEBP, HEIC or GIF. How that lines up with Sume image model sizes and formats.
Written by Sume