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.

5 min readSume
All posts

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.

Sume job webhook facts (Webhooks docs, read 2026-10-10)
ItemBehavior
Eventsjob.completed, job.failed, job.canceled; no progress events
SignatureHMAC SHA 256 over timestamp, a dot, and the raw body
Headersx-sume-webhook-timestamp and x-sume-webhook-signature (sume-v1=...)
Replay windowReject timestamps outside your tolerance; five minutes is a reasonable default
RetriesUp to 10 attempts, a fixed 30 s apart by default, 10 s timeout each
IdempotencyUse 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 2xx only 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-secret and keep it in an environment variable.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume