Media in your app.

Serve generated images and video to your users without exposing your key. Next.js and Express.

Bright Scandinavian living room with light oak floors, linen sofa and large windows, real estate interior photography
Bright Scandinavian living room with light oak floors, linen sofa and large windows, real estate interior photographyZ-Image Turbo · seed 42
Stack
Next.js 15+ App Router, or Express on Node 18+
Needs
API key in server env only
Uses
backend proxy, signed URLs, assets

Your key never reaches the browser.

An API key spends your organization's credits and can read every asset in it. Windpaint has no publishable or browser-safe keys, so a web or mobile client can't call the API directly.

Instead the browser talks to your backend. Your backend submits the job with your key, remembers which user owns it, and hands back signed URLs that work in an <img> or <video> tag for 15 minutes.

How it fits together.

The recipe in order, from the first request to the saved file.

  1. Submit from your server

    POST /api/images checks the user, validates the prompt and aspect ratio, and submits with your key. Model and resolution stay on the server.

  2. Record ownership

    Store the request_id against the user. The status endpoint returns any job in your organization, so your backend does the access check.

  3. Poll your own route

    The browser polls /api/images/{id}, which reads the job's status with your key and returns only what the page needs.

  4. Hand out signed URLs

    When the job completes, mint a URL per output with GET /v1/generation/assets/{id}/url. It's valid for 15 minutes, so mint a fresh one each time.

  5. Copy what you keep

    Store asset ids, not URLs. For media that must outlive your retention period, copy the bytes to your own bucket once.

The core of it.

An excerpt from the recipe. The full version runs as-is.

app/api/images/[id]/route.ts, excerpttypescript
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {  const { id } = await params;  const userId = await currentUserId();  if (!userId || !(await ownsJob(id, userId))) {    return NextResponse.json({ error: "Not found" }, { status: 404 });  }  const job = await windpaint<Status>(`/generation/requests/${id}/status`);  const outputs = job.status === "completed"    ? await Promise.all(job.outputs.map(async (o) => ({        assetId: o.id,        url: await signedUrl(o.id), // valid 15 minutes        contentType: o.content_type,      })))    : [];  return NextResponse.json({ status: job.status, outputs });}

The full recipe has the server-only API helper, the submit route, a byte proxy, the client page, an Express version, and a copy-to-S3 helper.

Full recipe in the docs

Whatever your users ask for.

Your backend passes their prompt through and hands back signed URLs. These are real Z-Image Turbo outputs, each with its prompt and seed.

  • Stylized 3D avatar of a smiling woman with short teal hair, soft studio lighting, pastel background, friendly

    smiling woman with short teal hair

  • Stylized 3D avatar of a bearded man with round glasses, soft studio lighting, pastel background, friendly

    bearded man with round glasses

  • Stylized 3D avatar of a teenager with braids and headphones, soft studio lighting, pastel background, friendly

    teenager with braids and headphones

Stylized 3D avatar of a {person}, soft studio lighting, pastel background, friendlyZ-Image Turbo · seed 11 · 1:1
  • Clay avatar

    Clay avatarClay figurine style avatar of a smiling woman with a yellow raincoat, soft studio…Z-Image Turbo · seed 11

  • Anime avatar

    Anime avatarAnime style avatar of a young man with messy hair and headphones, clean…Z-Image Turbo · seed 42

  • Watercolor avatar

    Watercolor avatarWatercolor portrait avatar of an older woman with silver hair and round glasses…Z-Image Turbo · seed 11

  • Pixel avatar

    Pixel avatarPixel art avatar of a cheerful astronaut with a mint visor, 32-bit style, plain…Z-Image Turbo · seed 11

  • Recipe card

    Recipe cardOverhead photograph of a bowl of green curry with jasmine rice and fresh herbs…Z-Image Turbo · seed 11

  • Travel card

    Travel cardA white-washed village with blue domes above the sea at golden hour, travel…Z-Image Turbo · seed 11

Three ways to serve a file.

A job's own output URL needs your key, so it's no use to a browser. Pick one of these instead.

Options from the full recipe.
OptionLifetimeUse it when
Signed URL
15 minutes
Showing results in your UI. GET /v1/generation/assets/{id}/url, minted fresh on each page load.
Proxy the bytes
Until the asset expires
You want a stable URL on your own domain, or browser JavaScript has to fetch() the file.
Copy to your storage
As long as you keep it
Public sharing, embedding in emails, heavy traffic, or media that must outlive retention.

Questions.

Why not put the API key in a public environment variable?

Anything in a client bundle, including NEXT_PUBLIC variables, is readable by your users, and the key can spend your credits and read every asset in your organization. Keep it in server environment variables and call Windpaint from your backend.

How long do signed URLs last?

15 minutes. Store the asset id and mint a new URL when a page loads. Once the asset passes your retention period, 7 days by default, the URL endpoints return 410.

Can browser JavaScript fetch a signed URL?

Signed URLs work in img and video tags and in download links. A fetch() that reads the bytes can be blocked by CORS on the storage host, so proxy the bytes through your backend for that case.

Start yours.

Request early access. We’ll email you when your account is approved, with 200 free credits to spend in the first 30 days.