Bun 1.4.1 Bun.write(path, response): save Sume artifacts to disk
Bun 1.4.1 streams a Response body to disk instead of buffering it. Read GET /v1/jobs/:id/result, then Bun.write each artifact URL, checking the status first.

Short answer
After a Sume job completes, read GET /v1/jobs/:id/result, then await Bun.write(path, await fetch(artifact.url)) for each artifact. Bun v1.4.1, released 4 September 2026, changed Bun.write to stream a Response, Request or ReadableStream body into the file instead of reading it into memory first.
That matters for video artifacts. The release notes measure a 128 MiB download: before the change it added 161 MB to peak RSS, after it adds 13 MB. Check response.ok before writing, or an error page ends up in a file named like your video.
What 1.4.1 changed
Two other items in the same release touch HTTP code: Bun.serve gained HTTP/2, and fetch() now verifies TLS against the URL rather than the Host header. Neither changes how you call Sume.
| Item | Detail |
|---|---|
| Bun.write(path, response) | streams the body to disk; 128 MiB download: +161 MB RSS before, +13 MB after |
| Bun.serve | HTTP/2 on the same port as HTTP/1.1, same routes and fetch handler |
| fetch() | TLS verified against the URL, not the Host header; unix socket connections reused |
The result shape
A completed job returns data.result.artifacts, each entry with id, type, url and content_type. The URLs are Sume media URLs; raw provider URLs are not public outputs. The result route answers 409 job_not_completed before completion, so call it after the status poll reports result_ready.
| Field | Example |
|---|---|
| id | artf_... |
| type | image |
| url | https://media.sume.com/artifacts/... |
| content_type | image/png |
The script
Run SUME_API_KEY=... bun save.mjs job_123. The API key goes only to api.sume.com; the media download is a plain fetch with no credential, so the key is not sent to the CDN host.
const id = process.argv[2];
const res = await fetch(`https://api.sume.com/v1/jobs/${id}/result`, {
headers: { Authorization: `Bearer ${process.env.SUME_API_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
for (const a of data.result.artifacts) {
const ext = (a.content_type.split("/")[1] ?? "bin").replace("jpeg", "jpg");
const dl = await fetch(a.url);
if (!dl.ok) throw new Error(`download ${a.id}: ${dl.status}`);
const bytes = await Bun.write(`./${a.id}.${ext}`, dl);
console.log(a.id, a.type, bytes, "bytes");
}What Sume does and does not do
Sume stores outputs behind durable media URLs and lists them in the job result, so you can re-run this script later against the same job id. Do not store a raw provider URL; the public API does not return one.
Sume does not guarantee a file extension from type; derive it from content_type, as above, and treat an unknown type as a binary file.
Sources
Related posts
More in Developers
- Bun.cron() for Sume jobs: an OS scheduler is not a job store
Bun 1.4 adds Bun.cron(), OS-level scheduling. Use it for a Sume reconcile pass over stored job ids, not as the place that remembers which jobs exist.
- Canary 10% of video jobs to Sume before cutover: sticky bucketing
Moving video traffic off a shut-down API: hash a stable key into a percent bucket so each customer stays on one backend, and raise Sume's share in steps.
- Cancel a GPT Image 2.5 job: only possible before it starts
Sume's POST /v1/jobs/{id}/cancel works only before generation work starts. How to read cancelable, what the 409 means, and what a client timeout does not do.
- Cancel a queued music take: only before generation starts
Sume cancels a queued music job, but once generation starts the API returns 409 job_generation_already_started and the job runs on. Who can cancel it.
Written by Sume