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

# Node.js Quickstart

> A complete working example with job submission, polling, and webhook handling.

# Node.js Quickstart

This guide shows a complete integration: submit a try-on job, receive the result via webhook, and verify the signature.

## Submit a try-on job

```javascript theme={null}
const apiKey = process.env.ZUPERTRY_API_KEY; // zt_live_sk_... or zt_test_sk_...

async function createTryOnJob(modelImageUrl, garmentImageUrl, webhookUrl) {
  const res = await fetch('https://app.zupertry.com/v1/tryon', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model_image_url: modelImageUrl,
      garment_image_url: garmentImageUrl,
      webhook_url: webhookUrl, // optional
    }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Zupertry error: ${err.error.code} — ${err.error.message}`);
  }

  return res.json(); // { job_id, status: "pending", created_at, mode }
}

// Usage
const job = await createTryOnJob(
  'https://your-cdn.com/model-photo.jpg',
  'https://your-cdn.com/product-image.jpg',
  'https://your-app.com/webhooks/zupertry'
);
console.log('Job created:', job.job_id);
```

## Poll for completion (without webhook)

```javascript theme={null}
async function waitForResult(jobId) {
  while (true) {
    const res = await fetch(`https://app.zupertry.com/v1/jobs/${jobId}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    const job = await res.json();

    if (job.status === 'completed') {
      return job.output_url;
    }
    if (job.status === 'failed') {
      throw new Error(`Job failed: ${job.error}`);
    }

    await new Promise((r) => setTimeout(r, 2000)); // poll every 2s
  }
}

const outputUrl = await waitForResult(job.job_id);
console.log('Result:', outputUrl);
```

## Receive results via webhook (recommended)

Set up an HTTP endpoint to receive real-time job completion events. Zupertry signs every request with HMAC-SHA256.

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');

const app = express();

// Parse raw body BEFORE json middleware for signature verification
app.use('/webhooks/zupertry', express.raw({ type: 'application/json' }));

app.post('/webhooks/zupertry', (req, res) => {
  // 1. Verify the signature
  const signature = req.headers['x-zupertry-signature'];
  const secret = process.env.ZUPERTRY_WEBHOOK_SECRET;

  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(req.body) // use raw Buffer, not parsed JSON
    .digest('hex');

  if (signature !== expectedSig) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 2. Handle the event
  const event = JSON.parse(req.body.toString());

  switch (event.type) {
    case 'job.completed': {
      const { job_id, output_url, credits_consumed } = event.data;
      console.log(`Job ${job_id} done. URL: ${output_url}. Credits: ${credits_consumed}`);
      // Save output_url to your database, notify user, update product listing, etc.
      break;
    }
    case 'job.failed': {
      const { job_id, error } = event.data;
      console.error(`Job ${job_id} failed: ${error}`);
      break;
    }
  }

  // Always respond 2xx quickly — Zupertry retries on any non-2xx response
  res.status(200).json({ received: true });
});

app.listen(3000);
```

## Full working example

```javascript theme={null}
// server.js — try this locally with: node server.js
require('dotenv').config();
const express = require('express');
const crypto = require('crypto');

const app = express();
const API_KEY = process.env.ZUPERTRY_API_KEY;
const WEBHOOK_SECRET = process.env.ZUPERTRY_WEBHOOK_SECRET;

// Submit a job
app.get('/submit', async (req, res) => {
  const job = await fetch('https://app.zupertry.com/v1/tryon', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model_image_url: 'https://picsum.photos/800/1200',
      garment_image_url: 'https://picsum.photos/600/800',
      webhook_url: 'https://your-ngrok-url.ngrok.io/webhooks/zupertry',
    }),
  }).then((r) => r.json());

  res.json(job);
});

// Receive webhook
app.use('/webhooks/zupertry', express.raw({ type: 'application/json' }));
app.post('/webhooks/zupertry', (req, res) => {
  const sig = req.headers['x-zupertry-signature'];
  const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(req.body).digest('hex');
  if (sig !== expected) return res.status(401).end();

  const event = JSON.parse(req.body.toString());
  console.log('Webhook received:', event.type, event.data);
  res.json({ ok: true });
});

app.listen(3000, () => console.log('http://localhost:3000'));
```

```bash theme={null}
# .env
ZUPERTRY_API_KEY=zt_test_sk_your_test_key
ZUPERTRY_WEBHOOK_SECRET=your_webhook_signing_secret
```

<Tip>
  Use [ngrok](https://ngrok.com) to expose your local webhook endpoint during development: `ngrok http 3000`. Use the generated URL as your webhook URL.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="API Reference" icon="code" href="/api-reference">
    Every endpoint, parameter, and error schema
  </Card>

  <Card title="Webhooks Guide" icon="webhook" href="/webhooks">
    Event types, retry logic, signature verification in depth
  </Card>
</CardGroup>
