Get the video URL from a Sume webhook: pick the artifact by type
A Sume job.completed payload lists artifacts with id, url, type and content_type. Select the video by content_type, not array index. TypeScript for Node.

To get the video URL out of a Sume job.completed webhook, read payload.artifacts and select the entry whose content_type starts with video/, instead of taking artifacts[0]. Each artifact carries id, url, type and content_type, as shown in Sume webhooks. Index zero works today for a plain text-to-video job, and breaks the day a job returns a poster, a caption file or a second clip ahead of the video.
Teams moving off the OpenAI Sora API, which ended on 2026-09-24 according to the MagicHour tracker (read 2026-10-06), often carry over a data[0].url habit from the old client. The Sume payload is shaped differently, so port the lookup deliberately and add the type check once.
What are the cases to handle?
A completed event has event: "job.completed", status: "OK", the request_id and job_id, and the artifacts array. A failed or canceled event has status: "ERROR" and an error object, and no artifacts worth reading. So the handler has three outcomes: no artifact of the wanted type on a success event (a bug worth an alert, not a crash), a success with a match, and an error event. Write each as a branch with its own log line, and return 2xx for all three once the event is stored, because a webhook that returns an error only makes Sume retry the same event up to ten times. The no-artifact case in particular should alert a person: retrying will never create a file that the job did not produce, so a 500 here only adds noise on top of a real fault.
What does the selector look like?
type Artifact = { id: string; url: string; type: string; content_type: string };
type Event = {
event: string; job_id: string; status: "OK" | "ERROR";
payload?: { artifacts?: Artifact[] }; error?: { message?: string };
};
export function pickArtifact(ev: Event, prefix: string): Artifact | null {
if (ev.event !== "job.completed" || ev.status !== "OK") return null;
return ev.payload?.artifacts?.find((a) => a.content_type.startsWith(prefix)) ?? null;
}
const sample: Event = {
event: "job.completed", job_id: "job_9", status: "OK",
payload: { artifacts: [
{ id: "a1", url: "https://cdn.example/poster.jpg", type: "image", content_type: "image/jpeg" },
{ id: "a2", url: "https://cdn.example/clip.mp4", type: "video", content_type: "video/mp4" },
] },
};
console.log(pickArtifact(sample, "video/")?.url);
console.log(pickArtifact({ ...sample, event: "job.failed", status: "ERROR" }, "video/"));Should I match on type or content_type?
Match on content_type rather than the shorter type string if you plan to accept several formats: video/mp4 and video/webm are both videos, and a prefix test covers them without a list. If you need exactly one container, test the full value.
Selecting the artifact is separate from storing it. The URL points at Sume's storage, so a pipeline that needs the file for longer than your own retention should copy it to storage you control, and it should do that after acknowledging the webhook, not inside the request. Sume gives a 10 second timeout per delivery attempt, and a download of a long clip does not fit in that.
| Field | Use it for | Do not use it for |
|---|---|---|
| id | Dedupe and logging | A download URL |
| url | The fetch, after you ack | Permanent storage |
| type | A coarse filter | Choosing a codec |
| content_type | Selecting the clip | Guessing a file extension |
What changes if I poll instead?
If you poll instead of receiving webhooks, the same idea holds on the result: GET /v1/jobs/{id}/result returns the artifacts, and you select by type there too. For the OpenRouter-shaped video route, GET /v1/videos/{id}/content?index=0 redirects to an artifact by position, which is a deliberate API choice for that surface and still worth wrapping in a helper so the index lives in one place. Whichever route you use, log the job_id next to the URL you chose, so a support question about a missing clip can be answered from your own logs, without a second lookup against the API.
Sources
Related posts
More in Developers
- Go net/http client for Sume images: handle 200 and 202 on a model swap
A Go program that posts to Sume /v1/images with the model id from an env var and branches on 200 versus 202, so a gpt-image-1 swap is not a rebuild.
- gpt-image-1 returns 404 model_not_found on Sume: which id to send
gpt-image-1, gpt-image-1.5 or gemini-2.5-flash-image sent to Sume /v1/images return 404 model_not_found. Ids to send instead, plus a lookup.
- Heroku H12 at 30 seconds: call Sume async, not sync
Heroku's router ends a request at 30 s (H12). Sume sync mode waits up to 30 s too. Submit async, return 202 to the browser, then poll or take a webhook.
- How Sume TTS splits a script into sentence ids (. ! ? only)
Sume's script source cuts sentences at periods, exclamation and question marks only, keeps every character, and numbers them sent_000000. Examples and traps.
Written by Sume