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.

5 min readSume
All posts

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.

Artifact fields and how to use them, from Sume webhooks (read 2026-10-06)
FieldUse it forDo not use it for
idDedupe and loggingA download URL
urlThe fetch, after you ackPermanent storage
typeA coarse filterChoosing a codec
content_typeSelecting the clipGuessing 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

All Developers posts

Written by Sume