Skip to main content

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

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)

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);
Set up an HTTP endpoint to receive real-time job completion events. Zupertry signs every request with HMAC-SHA256.
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

// 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'));
# .env
ZUPERTRY_API_KEY=zt_test_sk_your_test_key
ZUPERTRY_WEBHOOK_SECRET=your_webhook_signing_secret
Use ngrok to expose your local webhook endpoint during development: ngrok http 3000. Use the generated URL as your webhook URL.

Next steps

API Reference

Every endpoint, parameter, and error schema

Webhooks Guide

Event types, retry logic, signature verification in depth