Sume webhook.test has no job_id: keep it out of your job table
The dashboard's Send test posts a signed webhook.test with no job_id. Route on event first, dedupe on job_id second, and no phantom job row appears.

The signed webhook.test event that Sume sends from the dashboard or POST /v1/webhooks/test-deliveries has no job_id and no run id. A handler that inserts payload["job_id"] unconditionally will write None or crash. Route on event first, treat webhook.test as a connectivity check that gets a 204, and dedupe real events on job_id.
The docs say the dummy body is not a job or Format run and that the test never replays a real job.
What arrives
Taken from the webhooks page. Job events and run events are different families with different payloads.
| Event | Has job_id? | What to do |
|---|---|---|
job.completed | Yes | Store once, fetch result |
job.failed | Yes | Store once, read error |
job.canceled | Yes | Store once, mark canceled |
webhook.test | No | Verify signature, answer 204, store nothing |
Run events (*.run.terminal) | Run id, not job id | Different receiver or branch |
Why order matters
Verify the signature first, on the raw body, for every event including the test; a test that fails verification is a useful signal that your secret is wrong. Then branch. The idempotency rule in the docs is that receivers treat job_id as the key, and that Redeliver does not create a new key: the same terminal event arrives again with a fresh timestamp and signature.
So the dedupe table holds job ids only. Putting test events in it adds a row with an empty key, and a unique constraint on an empty key will then reject a legitimate event later.
A tested handler core
The function below returns an action name; plug it in after your signature check. It uses an in-memory set here, and a unique database column in production.
SEEN = set()
def handle(event):
kind = event.get("event")
if kind == "webhook.test":
return "ack"
if kind in ("job.completed", "job.failed", "job.canceled"):
job_id = event.get("job_id")
if not job_id:
return "reject"
if job_id in SEEN:
return "duplicate"
SEEN.add(job_id)
return "store"
return "ack"
print(handle({"event": "webhook.test"}))
print(handle({"event": "job.completed", "job_id": "job_x"}))
print(handle({"event": "job.completed", "job_id": "job_x"}))
print(handle({"event": "future.thing"}))
Rehearse a failure
Send a test after every deploy of the receiver and fail the deploy if it does not answer 2xx. Use Redeliver, not the test, when you want to replay a real job; the test never will.
The same rule for unknown events
Apply the same discipline to events you do not know. Verify, log the event name, answer 2xx, and store nothing. Sume's run webhooks are a separate family, and future events may appear; a 500 on an unknown event turns a harmless addition into ten retries.
In your monitoring, count test events separately from real ones, so a burst of tests during setup does not look like traffic. A dedicated /hooks/sume path per environment helps: staging tests should never reach the production table, and the secret differs per workspace anyway.
Log the event name and the signature fingerprint for tests, so a misconfigured secret shows up in the first test delivery and not in the first real job.
- Verify before you parse.
- Dedupe on
job_idonly for the three terminal job events. - Use Redeliver to replay a real job.
Sources
Related posts
More in Developers
- Can a Sume webhook arrive twice? Build an idempotent receiver
Sume retries failed webhook deliveries up to 10 times and Redeliver replays a real event, so one terminal event can reach you twice. Dedupe on job_id or run_id.
- Sweep Format runs for failed webhook deliveries, then redeliver
Sweep in Python: read each run's webhook_delivery, redeliver only failed or exhausted ones, and leave the rest alone. Uses formats:read, formats:write.
- 10 hooks by 10 endings: a 100-variant grid in one Sume bulk queue
A 10 by 10 hook and ending grid is exactly 100 items, the bulk queue maximum. How to build the items array, pick concurrency up to 16, and read the result.
- A ten-photo edit regression suite to re-run when a model launches
Ten photos, five edits each, one scoring sheet: a cheap test to re-run whenever a new image edit model launches. 50 edits cost $1.875 at the low tier on Sume.
Written by Sume