> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bitstudio.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Generate your first product photo

> Upload a garment, submit one generation, and retrieve the result with a complete Node.js example.

You need **Node.js 22 or later**, a JPEG, PNG, or WebP product photo, and a [Studio API key](/api-reference/authentication) with access and credits for image generation. Running this example submits one generation and uses credits.

## 1. Set your API key

Set this variable in your server environment. Keep it out of source control:

```bash theme={null}
export BITSTUDIO_API_KEY='YOUR_API_KEY'
```

## 2. Save the example

Save the following as `generate.mjs`. It uploads your photo, submits a `generate` job once, then polls the returned job ID. No npm packages are needed.

```javascript generate.mjs theme={null}
import { readFile } from 'node:fs/promises';
import { basename, extname } from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';

const key = process.env.BITSTUDIO_API_KEY;
const base = (process.env.BITSTUDIO_API_BASE_URL || 'https://api.bitstudio.ai').replace(/\/$/, '');
if (!key) throw new Error('Set BITSTUDIO_API_KEY on your server.');

async function request(path, { method = 'GET', body } = {}) {
  const isForm = body instanceof FormData;
  const response = await fetch(`${base}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${key}`,
      ...(!body || isForm ? {} : { 'Content-Type': 'application/json' }),
    },
    body: body ? (isForm ? body : JSON.stringify(body)) : undefined,
    signal: AbortSignal.timeout(60_000),
  });
  const text = await response.text();
  let data;
  try { data = JSON.parse(text); } catch { data = { error: text }; }
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${data.user_message || data.message || data.error || 'Request failed'}`);
  }
  return data;
}

async function waitForJob(id) {
  const deadline = Date.now() + 5 * 60_000;
  while (Date.now() < deadline) {
    const job = await request(`/v1/jobs/${encodeURIComponent(id)}`);
    if (job.status === 'completed') {
      if (!job.result?.url) throw new Error(`Job ${id} completed without a result URL. Contact support.`);
      return job;
    }
    if (['failed', 'policy_blocked'].includes(job.status)) {
      throw new Error(`Job ${id}: ${job.error?.code || job.status}`);
    }
    await sleep(5_000);
  }
  throw new Error(`Still processing. Resume polling with: node generate.mjs --resume ${id}`);
}

let id;
if (process.argv[2] === '--resume') {
  id = process.argv[3];
  if (!id) throw new Error('Usage: node generate.mjs --resume JOB_ID');
} else {
  const filename = process.argv[2];
  if (!filename) throw new Error('Usage: node generate.mjs ./shirt.jpg');
  const types = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp' };
  const type = types[extname(filename).toLowerCase()];
  if (!type) throw new Error('Choose a JPEG, PNG, or WebP product photo.');
  const file = await readFile(filename);
  if (file.length > 15 * 1024 * 1024) throw new Error('Image must be no larger than 15 MiB.');
  const form = new FormData();
  form.append('file', new Blob([file], { type }), basename(filename));
  form.append('type', 'virtual-try-on-outfit');
  const upload = await request('/images', { method: 'POST', body: form });
  console.log(`Uploaded image: ${upload.id}`);

  // Submit once. Do not automatically retry a timed-out generation POST.
  const { jobs } = await request('/v1/jobs', {
    method: 'POST',
    body: {
      operation: 'generate',
      input: {
        model_version: 'nano-banana-2',
        generate_mode: 'presets',
        model_text: 'An adult fashion model',
        outfit_image_ids: [upload.id],
        prompt: 'Full-length front view in a white studio, soft shadows.',
        resolution: 'standard',
        aspect_ratio: '3:4',
        num_images: 1,
      },
    },
  });
  if (!Array.isArray(jobs) || jobs.length !== 1 || !jobs[0].id) {
    throw new Error('Unexpected response. Check Studio before submitting another generation.');
  }
  id = jobs[0].id;
  console.log(`Job accepted: ${id}`);
}

const result = await waitForJob(id);
console.log(`Result: ${result.result.url}`);
```

## 3. Run it with a product photo

```bash theme={null}
node generate.mjs ./shirt.jpg
```

The script prints the uploaded image ID, the accepted job ID, and the completed result URL. Open the result and compare the garment with your source before using it.

The five-minute polling deadline is a client choice, not a completion guarantee. If the job is still processing, resume with its saved ID:

```bash theme={null}
node generate.mjs --resume JOB_ID
```

If the generation POST times out before you receive an ID, check your recent images in Studio or [list images](/api-reference/images/list) before submitting again. This example intentionally does not retry creation requests automatically.

## Reuse a specific avatar or scene

The example describes an adult person in text. For a reusable identity, replace `model_text` with an avatar ID in `asset_ids`. Add a preset ID to the same array for a saved visual setup. Use `outfit_asset_ids` instead of an uploaded image when the product is saved in your wardrobe.

[Build a reusable asset workflow →](/api-reference/assets-workflow)
