Twenty image jobs, one webhook: mode webhook plus a Python verifier
Submit 20 image requests with mode webhook, receive signed job.completed callbacks, and verify them in Python. Retries, replay window and the poll fallback.

To generate 20 images without holding 20 connections open, send each POST /v1/images with mode: "webhook" and a public HTTPS webhook_url. Sume answers 202 at once and later POSTs a signed job.completed event for each job to that URL.
Your endpoint must verify the signature, store the event, and return a 2xx. The sample below refuses an empty secret, which is the check people forget.
What arrives
The Webhooks page says Sume sends terminal job events only. A completed image job carries artifacts, each with an id, a url, a type and a content_type.
| Item | Behavior |
|---|---|
| Events | job.completed, job.failed, job.canceled; no progress events |
| Signature | HMAC SHA 256 over timestamp, a dot, and the raw body |
| Headers | x-sume-webhook-timestamp and x-sume-webhook-signature (sume-v1=...) |
| Replay window | Reject timestamps outside your tolerance; five minutes is a reasonable default |
| Retries | Up to 10 attempts, a fixed 30 s apart by default, 10 s timeout each |
| Idempotency | Use job_id as the key on your side |
A verifier in Python
Verify against the raw body bytes, not a re-serialised copy. During a secret rotation the signature header can carry several sume-v1= entries separated by commas, and any match is enough. The function returns False for an empty secret.
import hashlib, hmac, time
def verify(raw_body: bytes, timestamp: str, header: str, secret: str, tolerance=300) -> bool:
if not secret:
return False
try:
ts = int(timestamp)
except ValueError:
return False
if abs(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}"
ok = False
for entry in header.split(","):
if hmac.compare_digest(entry.strip(), expected):
ok = True
return ok
print(verify(b"{}", "0", "sume-v1=abc", ""))Test the endpoint before the real run
Do not spend money to find out your endpoint is wrong. The Webhooks page describes a Send test control, on the dashboard Webhooks tab or at POST /v1/webhooks/test-deliveries with the account:write scope. It posts a dummy signed webhook.test payload to a URL you type. It never replays a real job and carries no job_id, so your handler must accept that shape without treating it as an image.
Redeliver is a different action. It re-sends the real terminal event of a job you already ran, with a fresh timestamp and signature, which is the right tool when your endpoint was down.
Submitting the twenty
Each submit is a normal image request with two extra fields. Because the response is the job envelope, you can fire all twenty from a loop and stop thinking about them. Store the job.id from every 202 first, so a missing callback can be traced.
Webhook delivery is an optimisation and never the only way to learn the result. After ten refused attempts the delivery is marked failed, yet the job still reached its true terminal state, so keep a poll on status_url for any job that has not reported after a reasonable time. Redelivery of a real terminal event is available at POST /v1/jobs/{job_id}/webhook/redeliver with the jobs:write scope, and it sends a fresh timestamp and signature.
- Return
2xxonly after the event is stored durably. - Deduplicate on
job_id, since a retry can deliver the same event twice. - Reject a webhook URL on localhost or a private network; Sume rejects it too.
- Read the signing secret from the dashboard or
GET /v1/webhooks/signing-secretand keep it in an environment variable.
Sources
Related posts
More in Developers
- Unit-test a Sume job poll loop with a fake clock and no network
Inject the status reader and the sleep function to test a Sume poll loop in milliseconds: next_poll_after_seconds, backoff fallback and the client deadline.
- Upgraded your Sume plan but ratelimit-limit is still the old number?
A plan change can take up to 60 seconds to reach the per-key rate limit, because the tier is cached. Why ratelimit-limit lags, and what changes at once.
- uv run a single-file Python script against the Sume API (PEP 723)
A one-file Sume script with inline PEP 723 dependencies runs with uv run and no virtualenv. Submit, poll next_poll_after_seconds, print the result.
- Veo 3.1 image_url rejected on Sume: text-only error and what to use
Sume's Veo rows refuse image, frame, reference and video inputs as 'text-to-video only here'. The refused fields, other messages, and rows that take an image.
Written by Sume