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.

4 min readSume
All posts

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

Fields read from the job events route and its tests in the Sume repo, 2026-10-02
FieldMeaning
type: webhook.deliveryOne summary event per job with a webhook
data.delivery_statuspending, delivering, delivered, retrying, failed or exhausted
data.attemptsAttempts so far; the automatic limit is 10
data.last_status_codeHTTP code your receiver returned last
data.url_hostYour host only; the path and query are redacted
data.last_errorRedacted 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.

  • retrying with a 5xx or 429: your receiver is failing; fix it and the remaining attempts will land.
  • exhausted or failed: 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.
  • delivered but 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-fingerprint header 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

All Developers posts

Written by Sume