Sume job webhooks: failed and canceled both say ERROR, branch on event

job.failed and job.canceled webhook payloads both use status ERROR with an error object. Dispatch on the event name, not status, in Python.

4 min readSume
All posts

A Sume job webhook has three event names: job.completed, job.failed and job.canceled. The status field in the payload has only two values. A completed job sends status: "OK". The docs state that failed and canceled webhooks use status: "ERROR" and include an error object. A handler that checks status == "ERROR" therefore cannot tell a failure from a cancel.

Why it matters

A failure may need an alert, a refund check or a retry with changed input. A cancel is something you asked for, and it normally needs none of those. If you route both to your pager, every user-initiated cancel wakes someone. Branch on event, which is exact.

The three payloads

Job webhook events and the fields they carry (read 2026-10-04)
eventstatusOther fields
job.completedOKpayload.artifacts with id, url, type and content_type
job.failedERRORerror object
job.canceledERRORerror object

Dispatcher

The dispatcher takes a verified body. It looks at event first, ignores unknown events instead of failing (new ones may be added), and keys all work on job_id, as the docs advise for idempotency on your side.

import json

def handle(raw: bytes, done: set) -> str:
    body = json.loads(raw)
    event = body.get("event")
    if not str(event).startswith("job."):
        return f"ignored {event}"
    job_id = body["job_id"]
    if job_id in done:
        return "duplicate"
    done.add(job_id)
    if event == "job.completed":
        urls = [a["url"] for a in body.get("payload", {}).get("artifacts", [])]
        return f"completed {job_id}: {len(urls)} artifact(s)"
    if event == "job.failed":
        return f"failed {job_id}: {body.get('error', {}).get('code', 'unknown')}"
    if event == "job.canceled":
        return f"canceled {job_id}"
    return f"ignored {event}"

seen = set()
print(handle(b'{"event":"job.canceled","job_id":"job_1","status":"ERROR","error":{}}', seen))  # canceled job_1

Duplicate handling

Delivery is at-least-once: a failed attempt is retried up to 10 times. In production, write the job_id and the side effects in one database transaction with a unique constraint, instead of the in-memory set used here.

Keep a poll fallback

A webhook is an optimization. After ten refused attempts, the delivery is marked failed even though the job still reached its real terminal state. Keep polling the status_url for jobs whose event never arrived, and use POST /v1/jobs/{job_id}/webhook/redeliver to send the terminal event again with a fresh timestamp and signature. That route re-sends whichever of the three events matches the job, so the same dispatcher handles it.

Test all three branches with a signed webhook.test delivery from the dashboard first. That payload has no job_id, so it will not reach the job branches, which is the right behavior for a test.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume