Why did my Sume webhook not arrive? Read the job events
One GET on a job lists a webhook.delivery event with status, attempts, last HTTP code and host. A 19-line Python function turns it into a one-line verdict.

Read GET /v1/jobs/{id}/events and look at the last webhook.delivery event. Its data says whether delivery is pending, delivering, delivered, retrying, failed or exhausted, how many attempts were made, the last HTTP status your server returned, and your receiver's host. That is enough to tell an outage on your side from a receiver that answered 200 and lost the message.
The function below returns that as one line you can paste into a ticket or print in CI. It needs only the standard library and an API key in SUME_API_KEY.
What the event carries
| Field | Meaning |
|---|---|
| type: webhook.delivery | One summary event per job with a webhook |
| data.delivery_status | pending, delivering, delivered, retrying, failed or exhausted |
| data.attempts | Attempts so far; the automatic limit is 10 |
| data.last_status_code | HTTP code your receiver returned last |
| data.url_host | Your host only; the path and query are redacted |
| data.last_error | Redacted text; URLs and ids are replaced |
The function
No webhook.delivery event means the job has no webhook configured or no delivery has been attempted yet, for example because the job is still running.
import json, os, sys, urllib.request
def delivery_verdict(job_id, base="https://api.sume.com"):
req = urllib.request.Request(
f"{base}/v1/jobs/{job_id}/events",
headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"]})
with urllib.request.urlopen(req, timeout=15) as r:
events = json.load(r)["data"]["events"]
hooks = [e for e in events if e["type"] == "webhook.delivery"]
if not hooks:
return "no webhook configured for this job, or none sent yet"
d = hooks[-1].get("data", {})
if d.get("delivery_status") == "delivered":
return f"delivered after {d.get('attempts')} attempt(s): look at your dedupe"
return (f"{d.get('delivery_status')}: {d.get('attempts')} attempts, "
f"last HTTP {d.get('last_status_code')} from {d.get('url_host')}")
if __name__ == "__main__":
print(delivery_verdict(sys.argv[1], os.environ.get("SUME_BASE", "https://api.sume.com")))Reading the verdict
I ran the function against a local server that returns the three event shapes (no event, retrying with a 503, delivered), and it printed the expected lines. Treat that as a shape check, not a test against the live service.
retryingwith a 5xx or 429: your receiver is failing; fix it and the remaining attempts will land.exhaustedorfailed: automatic attempts are over. Settle the job by polling, or use the redeliver endpoint, which the docs say does not consume one of the automatic 10.deliveredbut you have no record: look at your dedupe key and your queue, not at Sume.- A 401 or 403 code: your receiver rejected the call; compare the
x-sume-webhook-secret-fingerprintheader with the secret you hold.
Where to run it
Call it from three places: a support script that takes a job id, the sweeper that settles stale pending rows (so the log says why the webhook was late), and a CI smoke test that submits a job with a webhook to a staging receiver and fails with the verdict if delivery is not delivered within your budget.
The route needs the same key that owns the job; a job id from another workspace returns an error, not an empty list.
Limits
The event is a summary of the latest delivery state, not one row per attempt. Messages are redacted by design, so you will not see your response body. Events describe job webhooks; format runs have their own events route under /v1/format-runs/{id}/events. The status names come from the repo's constants and may grow; treat unknown ones as not delivered.
Sources
Related posts
More in Developers
- Repeat a TTS take: read generation_config and speed from the job
A completed Sume TTS job records its engine, voice, language, output_format, generation_config and speed. Read them back to make the next line sound the same.
- Reconcile a no-code run with GET /v1/jobs and idempotency_key
When a Zap, scenario or flow loses its job ids, list jobs with GET /v1/jobs and join on idempotency_key. Pages are newest first, 100 at most, cursor-paged.
- Retry an avatar video request without paying twice
A timed-out avatar video submit can be retried safely with the same Idempotency-Key. What Sume returns, what causes a 409, and how to build the key.
- Ruby Net::HTTP: submit and poll a Sume job with one key
A Ruby recipe using only Net::HTTP: submit a Sume image job with an Idempotency-Key, set open and read timeouts, poll status_url, and return the artifact URLs.
Written by Sume