job.canceled: the third Sume terminal event your handler forgets
Sume sends job.completed, job.failed and job.canceled and nothing else. A handler that covers only two leaves canceled jobs open. Route all three, 204 the rest.

Sume job webhooks deliver exactly three events, all terminal: job.completed, job.failed and job.canceled. There are no progress or partial deliveries. The routing example in the SDK docs shows job.completed and job.failed, so a handler copied from it never closes a job that you or a teammate canceled. Route all three, and answer 204 for any event name you do not know, so a new event type never causes a retry storm.
The three events
| Event | When | Payload status |
|---|---|---|
| job.completed | Job completed; a public result is available | OK, with payload.artifacts |
| job.failed | Job failed with a public error | ERROR, with an error object |
| job.canceled | Job reached the canceled state | ERROR, with an error object |
When a job ends up canceled
Cancellation succeeds only before generation work starts. After generation starts, the cancel call returns 409 job_generation_already_started and the job finishes normally. So a job.canceled event means the work never ran. A cancel of an already canceled job is idempotent, which makes duplicate events harmless if you dedupe on job_id.
Your handler should therefore stop any downstream step waiting for that job, mark the item as canceled in your own table, and not retry it automatically. A person or a rule canceled it on purpose.
The router
The TypeScript below dispatches on event, not on the status field, and returns 204 for everything unknown. The verification step from verifyWebhook belongs before it.
type JobEvent = {
event: string;
job_id?: string;
request_id?: string;
status?: string;
payload?: { artifacts?: { url: string; type: string }[] };
error?: { code?: string; message?: string };
};
export function route(e: JobEvent): Response {
switch (e.event) {
case "job.completed":
markDone(e.job_id!, e.payload?.artifacts ?? []);
break;
case "job.failed":
markFailed(e.job_id!, e.error);
break;
case "job.canceled":
markCanceled(e.job_id!);
break;
default:
break; // unknown event: acknowledge, do not 500
}
return new Response(null, { status: 204 });
}
declare function markDone(id: string, a: unknown[]): void;
declare function markFailed(id: string, e: unknown): void;
declare function markCanceled(id: string): void;Test with a fixture
Add one test per event, plus an unknown event. The unknown-event test is the one that protects you the day Sume adds a fourth type.
Poll fallback also sees canceled
The same rule holds when you poll. The terminal job statuses are completed, failed and canceled, and a loop that stops only on the first two runs until its deadline when a job was canceled. Use the terminal boolean from the status response instead of a hand-written list, and branch on sume_status afterward.
For /v1/videos polling the spelling is cancelled with two Ls. Treat both spellings as terminal if one loop serves both surfaces.
Only the member whose key or Agent turn created a job can cancel it. A cancel request after generation has begun is refused with 409 job_generation_already_started and details.cancelable: false, and the job completes or fails normally. A cancel of an already canceled job is idempotent. Between them, these rules mean you can see job.canceled, job.completed or job.failed for a job you tried to cancel, and your handler must accept all three.
Sources
Related posts
More in Developers
- Sume MCP first call: mcp_health must say mcp_oauth, then tools_list
After adding the Sume connector, call mcp_health and check authenticated.auth_source is mcp_oauth. Then call tools_list to see which tools your grant exposes.
- Limit an agent's paid MCP calls with script_run max_paid_calls
script_run runs a short program on the Sume side with max_calls, max_paid_calls and a 5-55 second timeout. What it can bound, and what it does not cap.
- result_ready vs terminal vs completed: gate the Sume result fetch
Poll a Sume job until terminal, fetch the result only when result_ready is true. Failed and canceled jobs answer 409 job_not_completed on /result.
- Sume run webhook retries: ten attempts span about 3 hours
Ten delivery attempts with 30-second doubling backoff capped at one hour add up to 11,010 seconds before jitter. The timeline, and what to do after exhaustion.
Written by Sume