Node.js · Virtual Try-On API

Virtual Try-On API in Node.js

There's no @photta/node package yet — and you don't need one. Node 18+ ships `fetch` natively, and a 20-line helper gets you to your first on-model image in about five minutes.

In one sentence

Set `PHOTTA_API_KEY` in env, write a tiny `phottaFetch()` helper that calls `https://ai.photta.app/api/v1` with `Authorization: Bearer photta_live_xxx`, POST to `/tryon/apparel` with a product image URL, mannequin ID and pose ID, then poll `/tryon/apparel/:id` every 3 seconds until `status === 'completed'` — typically within 1.5–4 minutes.

Updated · 2026-04-19

Your first request

Node.jsPythoncURLcURL
import { phottaFetch } from "./photta.js";

// 1. Submit the job — returns a generation ID immediately.
const created = await phottaFetch("/tryon/apparel", {
  method: "POST",
  body: JSON.stringify({
    product_type: "dress",
    product_images: ["https://example.com/dress.jpg"],
    mannequin_id: "mnq_athena_ts",
    pose_id: "pose_standing_front",
    resolution: "2K",
    aspect_ratio: "3:4",
  }),
});
const generationId = created.data.id;

// 2. Poll every 3 seconds until processing completes. Typical
// completion is 1.5–4 minutes; put an upper bound so a stuck
// job can't hang your request forever.
const pollInterval = 3000;
const maxAttempts = 120;     // 120 × 3s = 6 minutes
let result;

for (let i = 0; i < maxAttempts; i++) {
  result = await phottaFetch(`/tryon/apparel/${generationId}`);
  if (result.data.status === "completed") break;
  if (result.data.status === "failed") {
    throw new Error(result.data.error_message);
  }
  await new Promise((r) => setTimeout(r, pollInterval));
}

console.log("Result:", result.data.output_url);

What to expect

Typical completion

1–3min

2K / 4K credits

4 / 6

Styles

2

Batch-ready

yes

How it works

Virtual Try-On API in Node.js

Five steps, about five minutes from signup to first image.

  1. 01

    Step 1

    Sign up and generate a key

    Create an account at ai.photta.app. Open the Developers section of the dashboard, click Generate API key, and grab the live key. It starts with `photta_live_` followed by 32 hex characters.

  2. 02

    Step 2

    Store the key in env, not source

    Add `PHOTTA_API_KEY=...` to `.env` (or your platform's secret store) and load it via `process.env`. Never commit the key; never import it into a file marked `'use client'` — the bundler will pull it into the browser.

  3. 03

    Step 3

    Write a tiny fetch helper

    One 20-line wrapper around `fetch()` handles the base URL, Authorization header, JSON body and error normalisation. This is the only abstraction you need until an official SDK ships.

  4. 04

    Step 4

    Submit a try-on and poll

    POST to `/tryon/apparel` with `product_type`, `product_images`, `mannequin_id`, `pose_id`, `resolution` and `aspect_ratio`. The API returns a generation ID immediately. Poll `/tryon/apparel/:id` every 3 seconds until `status === 'completed'`.

  5. 05

    Step 5

    Persist the result

    The completed payload includes `output_url` and `thumbnail_url`. Fetch the bytes once and store them in your own object storage — the URLs are stable but your product shouldn't depend on Photta's CDN for rendering.

Code, end to end

Copy, paste, done.

Four snippets — install prerequisites, wrap the REST call, submit + poll, then handle the errors that actually happen in production.

01No SDK required — use native fetch in Node 18+
bash
# Node 18+ ships fetch natively. No install step needed.
node --version   # ensure v18 or later

# Optional: keep your API key out of source control
echo "PHOTTA_API_KEY=photta_live_xxxxx" >> .env
02Wrap the REST call in a tiny client helper
javascript
// photta.js — a 20-line wrapper you can reuse across your app.
const PHOTTA_BASE_URL = "https://ai.photta.app/api/v1";

export async function phottaFetch(path, init = {}) {
  const res = await fetch(`${PHOTTA_BASE_URL}${path}`, {
    ...init,
    headers: {
      "Authorization": `Bearer ${process.env.PHOTTA_API_KEY}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });
  const body = await res.json();
  if (!res.ok) {
    const err = new Error(body?.error?.message ?? res.statusText);
    err.status = res.status;
    err.code = body?.error?.code;
    throw err;
  }
  return body;
}
03Submit a try-on job and poll until it lands
javascript
import { phottaFetch } from "./photta.js";

// 1. Submit the job — returns a generation ID immediately.
const created = await phottaFetch("/tryon/apparel", {
  method: "POST",
  body: JSON.stringify({
    product_type: "dress",
    product_images: ["https://example.com/dress.jpg"],
    mannequin_id: "mnq_athena_ts",
    pose_id: "pose_standing_front",
    resolution: "2K",
    aspect_ratio: "3:4",
  }),
});
const generationId = created.data.id;

// 2. Poll every 3 seconds until processing completes. Typical
// completion is 1.5–4 minutes; put an upper bound so a stuck
// job can't hang your request forever.
const pollInterval = 3000;
const maxAttempts = 120;     // 120 × 3s = 6 minutes
let result;

for (let i = 0; i < maxAttempts; i++) {
  result = await phottaFetch(`/tryon/apparel/${generationId}`);
  if (result.data.status === "completed") break;
  if (result.data.status === "failed") {
    throw new Error(result.data.error_message);
  }
  await new Promise((r) => setTimeout(r, pollInterval));
}

console.log("Result:", result.data.output_url);
04Handle 402 credit exhaustion and 429 rate limits
javascript
try {
  await phottaFetch("/tryon/apparel", {
    method: "POST",
    body: JSON.stringify({ /* … */ }),
  });
} catch (err) {
  if (err.status === 402) {
    // Out of credits — surface a top-up CTA to the user.
    // err.code === "insufficient_credits"
  } else if (err.status === 429) {
    // Rate-limited. Honour the Retry-After header.
    const retryAfter = err.retryAfter ?? 30;
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    // Then retry…
  } else if (err.status >= 500) {
    // Server-side issue — backoff + retry.
  }
  throw err;
}

Why this shape

Why a helper beats a client library — for now

  • Node 18+ ships native fetch — no runtime deps required
  • Same helper works inside Next.js route handlers, API routes, server actions and edge functions
  • Keep API keys in env; never import the helper from a 'use client' module or the bundler will leak them
  • Works with any queue/cron: BullMQ, Inngest, Trigger.dev, Vercel Cron, Cloudflare Queues

What it doesn't do

Honest caveats

  • No official @photta/node SDK yet — REST is the only path today
  • No built-in webhook delivery; polling is the documented pattern (3–5s interval)
  • Bearer token lives in env; the API doesn't support OAuth client credentials yet

Questions other developers ask

Questions other developers ask

Is there an official Photta Node.js SDK?+

Not yet. The Photta docs explicitly plan SDKs after adoption thresholds: Python once ten API users are onboarded, Node.js at twenty. Until then, the documented path is raw REST via cURL, Node's native fetch, or the Python requests library. The 20-line helper on this page is equivalent to a client for the endpoints you'll use most.

What Node.js versions are supported?+

Anything that supports native `fetch` — which means Node 18 and later. On Node 16 you either polyfill with `node-fetch` or upgrade. The rest of the code is plain ECMAScript 2020 (async/await, template literals, dynamic imports) so no other runtime requirements.

How do I authenticate from Node?+

Send `Authorization: Bearer photta_live_xxx` on every request. The key starts with `photta_live_` in production, `photta_test_` when sandbox mode ships. Put it in an environment variable and load it via `process.env.PHOTTA_API_KEY`. The API does not yet support OAuth client credentials or session-based auth.

How do I handle the 1.5–4 minute generation window?+

The try-on endpoint is fully asynchronous. POST returns a generation ID immediately with `status: 'processing'`. Poll `GET /tryon/apparel/:id` every 3–5 seconds until `status` flips to `completed` or `failed`. Put an upper bound on attempts so a stuck job can't hang your request forever — 120 attempts at 3 seconds covers the documented window with margin.

Can I call the API inside Next.js?+

Yes — from a route handler (`app/**/route.ts`), a server action (`'use server'`), or middleware. Keep the API key on the server; never import the helper from a `'use client'` file. For long-running jobs that exceed Vercel's function timeout, defer the polling to a queue (Inngest, Trigger.dev, BullMQ) and return the generation ID to the browser so it can poll a webhook or your own status endpoint.

How does the API signal errors?+

Non-2xx responses carry a JSON body with an `error` object: `type`, `code`, `message` and a request ID for support. Common cases: 400 invalid_request_error with `param` pointing at the bad field; 402 insufficient_credits with `required` and `available` counts; 429 rate_limit_exceeded with `retry_after` in seconds and a matching Retry-After header. 5xx errors are safe to retry with exponential backoff.

Node.js · Virtual Try-On API

Create an account and get an API key

Set `PHOTTA_API_KEY` in env, write a tiny `phottaFetch()` helper that calls `https://ai.photta.app/api/v1` with `Authorization: Bearer photta_live_xxx`, POST to `/tryon/apparel` with a product image URL, mannequin ID and pose ID, then poll `/tryon/apparel/:id` every 3 seconds until `status === 'completed'` — typically within 1.5–4 minutes.

Virtual Try-On API for Node.js — Photta | Photta