Node.js · AI mannequin generation API

AI mannequin generation API in Node.js

In one sentence

Set PHOTTA_API_KEY in env, write a tiny fetch helper for https://api.photta.app/api/v1 with a Bearer key, POST to /api/v1/mannequins/generate, then poll /api/v1/mannequins/generate/{id} every 3 seconds until status is completed.

  • Typical time: 30-60s
  • Credits: 4 credits
  • Processing: async, polling
  • Limits: 600 requests/min
  • Authentication: Bearer key

Every figure on this page comes from the Photta API docs · checked

POST/api/v1/mannequins/generate
const res = await fetch("https://api.photta.app/api/v1/mannequins/generate", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.PHOTTA_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "category": "Female",
    "ethnicity": "east_asian_korean",
    "age": "late_20s",
    "skin_tone": "light_medium",
    "expression": "neutral",
    "hair_style": "long_straight",
    "hair_color": "black",
    "body_type": "slim",
    "height": "average"
  }),
});
const { data } = await res.json(); // data.id, data.status

async function waitFor(id) {
  for (let attempt = 0; attempt < 120; attempt++) {
    const res = await fetch(`https://api.photta.app/api/v1/mannequins/generate/${id}`, { headers: { Authorization: `Bearer ${process.env.PHOTTA_API_KEY}` } });
    const { data } = await res.json();
    if (data.status === "completed") return data.output_url;
    if (data.status === "failed") throw new Error(data.error_message ?? "generation failed");
    await new Promise((r) => setTimeout(r, 3000));
  }
  throw new Error("timed out after 6 minutes");
}

At a glance

At a glance

Endpoint
POST /api/v1/mannequins/generate
Authentication
Bearer API key
Processing
Asynchronous — poll the status endpoint
Typical time
30-60s
Credits
4 credits
Input limits
Up to 10 MB per image; JPEG, PNG, WebP
Idempotent
No

How it works

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

  1. 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. 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.

    node --version   # 18 or later ships fetch
    echo "PHOTTA_API_KEY=photta_live_xxxxx" >> .env
  3. 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.

    // photta.js — one helper for every endpoint
    const BASE = "https://api.photta.app/api/v1";
    export async function photta(path, init = {}) {
      const res = await fetch(BASE + path, { ...init, headers: { Authorization: `Bearer ${process.env.PHOTTA_API_KEY}`, "Content-Type": "application/json", ...(init.headers ?? {}) } });
      const body = await res.json();
      if (!res.ok) throw Object.assign(new Error(body.error?.message ?? res.statusText), { status: res.status, retryAfter: res.headers.get("Retry-After"), body });
      return body;
    }
  4. Step 4

    Submit a try-on and poll

    POST to `/api/v1/mannequins/generate` with the documented example body — the code below carries every field. The API returns a generation ID immediately. Poll `/api/v1/mannequins/generate/{id}` every 3 seconds until `status === 'completed'`.

    const res = await fetch("https://api.photta.app/api/v1/mannequins/generate", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.PHOTTA_API_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({
        "category": "Female",
        "ethnicity": "east_asian_korean",
        "age": "late_20s",
        "skin_tone": "light_medium",
        "expression": "neutral",
        "hair_style": "long_straight",
        "hair_color": "black",
        "body_type": "slim",
        "height": "average"
      }),
    });
    const { data } = await res.json(); // data.id, data.status
    
    async function waitFor(id) {
      for (let attempt = 0; attempt < 120; attempt++) {
        const res = await fetch(`https://api.photta.app/api/v1/mannequins/generate/${id}`, { headers: { Authorization: `Bearer ${process.env.PHOTTA_API_KEY}` } });
        const { data } = await res.json();
        if (data.status === "completed") return data.output_url;
        if (data.status === "failed") throw new Error(data.error_message ?? "generation failed");
        await new Promise((r) => setTimeout(r, 3000));
      }
      throw new Error("timed out after 6 minutes");
    }
  5. 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.

Errors

400invalid_request_error
The request is malformed or contains invalid parameters.
401authentication_error
The API key is missing, invalid, or expired.
402insufficient_credits_error
Your account does not have enough credits.
404not_found_error
The requested resource does not exist.
409conflict_error
The request conflicts with current state — e.g. your concurrent-batch cap is reached.
429rate_limit_error
Too many requests. Slow down and retry.Retry-After: 30
500api_error
An internal server error occurred. Retry later.
try {
  await photta("/mannequins/generate", { method: "POST", body: JSON.stringify({
    "category": "Female",
    "ethnicity": "east_asian_korean",
    "age": "late_20s",
    "skin_tone": "light_medium",
    "expression": "neutral",
    "hair_style": "long_straight",
    "hair_color": "black",
    "body_type": "slim",
    "height": "average"
  }) });
} catch (err) {
  if (err.status === 402) console.error("insufficient_credits_error: top up before retrying");
  else if (err.status === 429) await new Promise((r) => setTimeout(r, Number(err.retryAfter ?? 30) * 1000));
  else throw err;
}

Limits

  • 600 requests per minute per API key (Fixed window; the limit is the same on every plan)
  • A 429 carries Retry-After: 30 seconds
  • Up to 10 MB per image
  • Formats: JPEG, PNG, WebP
  • Concurrent batches per plan: Free 1 · Hobby 2 · Seller 3 · Brand 4 · Studio 6
  • Polling only — the API sends no webhooks.

Why a helper beats a client library — for now

  • Node 18+ ships native fetch — no runtime dependencies
  • The same helper works inside Next.js route handlers, server actions and any worker
  • Keep the API key in env; never import the helper from a 'use client' module or the bundler will leak it
  • Works with any queue or cron: BullMQ, Inngest, Trigger.dev, Vercel Cron, Cloudflare Queues

What it does not do

  • No official @photta/node package yet — REST is the only path today
  • Polling is the only completion signal; the API sends no webhooks
  • Bearer key only; there is no OAuth client-credentials flow

Questions developers ask

Is there an official Photta Node.js SDK?

Not yet. The documented path is raw REST via Node's native fetch — the helper on this page is the whole integration surface, and it works the same for every capability.

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. Put the key in an environment variable and load it via `process.env`; never import it from a file that runs in the browser.

How do I handle the generation window?

The generation endpoint is fully asynchronous. POST returns a generation ID immediately with `status: 'processing'`. Poll `GET /api/v1/mannequins/generate/{id}` every 3–5 seconds until `status` flips to `completed` or `failed`, and cap the attempts so a stuck job cannot hang a request.

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 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.

Try the API in five minutes

Generate a Photta API key, paste the cURL snippet above, and you'll have your first on-model image before the coffee finishes brewing.

  • 30-60s
  • 4 credits
  • async, polling
  • 600 requests/min
  • Bearer key