Sume job.canceled and job.failed webhooks both say status ERROR
A Sume job.failed and a job.canceled webhook both carry status ERROR and an error object. Branch on the event name, not on status, to tell them apart.

On a Sume job webhook, a failed job and a canceled job both arrive with status: "ERROR" and an error object. The only field that separates them is event: job.failed or job.canceled. Branch on the event name, and treat status as a coarse OK or ERROR flag.
The three events
Sume sends terminal job events only, so there are exactly three, and no progress or partial deliveries.
| Event | status | Body carries |
|---|---|---|
job.completed | OK | payload.artifacts[] |
job.failed | ERROR | error object |
job.canceled | ERROR | error object |
A handler that does not merge them
Cancellation is something you asked for, and failure is not. Keeping them apart matters for retry logic: re-submitting after a cancel you issued is a duplicate, and re-submitting after a failure may be right once you have read the error.
def handle(event: dict) -> str:
kind = event["event"]
if kind == "job.completed":
return "save " + event["payload"]["artifacts"][0]["url"]
if kind in ("job.failed", "job.canceled"):
# status is "ERROR" for both, so branch on the event name
return f"{kind}: {event.get('error')}"
return "ignore " + kind
print(handle({"event": "job.canceled", "status": "ERROR", "error": {"code": "x"}}))
print(handle({"event": "job.completed", "status": "OK", "payload": {"artifacts": [{"url": "https://media.sume.com/a"}]}}))Do not use status as the router
Job webhooks and run webhooks share one signature scheme, but the events differ. Run events end in .run.terminal, so route on event there too.
Sources
Related posts
More in Developers
- Sume job status logs_available is false: read events_url instead
A Sume job status carries logs_available, false while diagnostics live behind events_url. Read the events timeline for the lifecycle, not inline logs.
- Sume job webhook: request_id equals job_id, store one
In a Sume job webhook payload, request_id and job_id are the same value. Which to use as your dedupe key, and what else the payload carries.
- Sume jobs list: no next_cursor on the last page ends the loop
GET /v1/jobs returns up to 100 jobs newest first. Pass data.next_cursor back as starting_after, and stop when it is absent. Do not build a cursor yourself.
- Sume GET /v1/jobs: a misspelled filter returns 400, not all jobs
Sume rejects an unrecognized query parameter on GET /v1/jobs with 400 unknown_parameter, so a typo cannot return an unfiltered page. Valid filters listed.
Written by Sume