Skip to content

Downloading

2DAITeam edited this page Jul 30, 2026 · 2 revisions

Downloading

client.cdn fetches the actual bytes of a creation — image or video — authenticating with your key, so it works for private creations too (needs the read scope). Every method accepts a CDN id, a Creation, a QueueState, or a generation result.

Save to disk (Node)

const path = await client.cdn.download(gen, { savePath: './out' });
// → './out.jpg'  (extension inferred from the content type)

// shortcut on the client:
await client.download(gen, { savePath: './out' });

Get the bytes

const asset = await client.cdn.fetch(gen);
// { bytes: Uint8Array, contentType: 'image/jpeg', ext: 'jpg' }

Browser

const blob = await client.cdn.toBlob(gen);
const url = URL.createObjectURL(blob);

// or straight into an <img>:
img.src = await client.cdn.toDataUrl(gen);

Previews and thumbnails

Pass a transform and the CDN resizes the asset before it sends the bytes — nothing is decoded or re-encoded on your side, so a grid of previews costs a fraction of a grid of full assets. On a 2496×1664 generation measured against production: 1164 kB untouched, 132 kB at maxSide: 512, 47 kB at maxSide: 256.

// A preview: `maxSide` scales the longest edge and keeps the aspect ratio,
// so it works for portrait and landscape without knowing which you got.
const preview = await client.cdn.fetch(gen, { maxSide: 512 });

// Transforms compose with savePath — a thumbnail written straight to disk.
await client.cdn.download(gen, { maxSide: 256, savePath: './thumb' });

// And with abort: the signal now travels in the same options object.
const ctrl = new AbortController();
const blob = await client.cdn.toBlob(gen, { maxSide: 512, signal: ctrl.signal });

maxSide (?s) is the one you want for previews, thumbnails and contact sheets. width (?w) and height (?h) target those axes specifically, which means the result can be cropped to fit — reach for them only when a layout needs an exact dimension. Sending maxSide alongside either is pointless — the CDN gives ?s precedence over ?w / ?h.

Values must be finite and greater than zero, or the call throws ApiError('INVALID_TRANSFORM') before any request leaves the process — a NaN slipping in from a parsed query string fails at your call site instead of coming back as a CDN error. Fractions are rounded to whole pixels.

fetch, toBlob and toDataUrl still take a bare AbortSignal as their second argument, so code written before transforms existed keeps working unchanged:

const asset = await client.cdn.fetch(gen, ctrl.signal); // still valid

Just the URL

client.cdn.url(gen);                   // 'https://dapp.2dai.io:444/cdn/file/<id>'
client.cdn.url(gen, { maxSide: 512 }); // '…/cdn/file/<id>?s=512'

This raw URL loads directly in an <img> only for public creations. Private creations require the key, so use fetch / toBlob / toDataUrl for those — they accept the same transform.

Image or video?

The same methods work for both — the returned contentType (and ext) tell them apart (image/jpeg → jpg, video/mp4 → mp4, …).

const vid = await client.generate.video({ prompt: '…', inputCreationId: gen.creationId });
const asset = await client.cdn.fetch(vid);
console.log(asset.contentType); // 'video/mp4'
await client.download(vid, { savePath: './clip' }); // → './clip.mp4'

Clone this wiki locally