Skip to main content
ArtEmotion
/API Reference

ArtEmotion API

The ArtEmotion REST API gives your application access to virtual try-on technology — let your users see how clothes, glasses, hats, and shoes look on them before buying.

The API is JSON-first, uses standard HTTP verbs and status codes, and authenticates via Bearer tokens. Jobs are submitted asynchronously — you submit a try-on request and poll for results.

Every API call consumes credits from your ArtEmotion account. Virtual try-on costs 15–20 credits per job depending on the model. Monitor your balance at Settings → API Keys.

Base URL

Base URLhttps://app.artemotion.ai/api/v1

All endpoints are relative to this base URL. Requests and responses use application/json.

Authentication

All API requests (except GET /models) must include your API key in the Authorization header as a Bearer token.

HTTP
Authorization: Bearer ae_live_your_key_here
1
Create an API key
Go to Settings → API Keys and click "Create key". Give it a descriptive name (e.g. "Production").
2
Copy and store it securely
The key is shown only once. Store it in an environment variable — never hard-code it in client-side code.
3
Use it in requests
Include Authorization: Bearer ae_live_... in every API request header.

Quick Start

Submit a virtual try-on job and poll until it completes. The whole flow takes about 10–30 seconds.

cURL
# Step 1 — Submit the try-on job
curl -X POST https://app.artemotion.ai/api/v1/tryon \
  -H "Authorization: Bearer $AE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "person_image_url": "https://example.com/model.jpg",
    "garment_image_url": "https://example.com/tshirt.jpg",
    "garment_type": "top"
  }'

# Response:
# {
#   "job_id": "a1b2c3d4...",
#   "model_id": "image-apps-v2/virtual-try-on",
#   "status": "queued",
#   "credits_used": 15,
#   "poll_url": "https://app.artemotion.ai/api/v1/jobs/a1b2c3d4...?model_id=..."
# }

# Step 2 — Poll until completed (use the poll_url from above)
curl "$POLL_URL" \
  -H "Authorization: Bearer $AE_API_KEY"

# Response when done:
# {
#   "job_id": "a1b2c3d4...",
#   "status": "completed",
#   "images": ["https://cdn.artemotion.ai/results/..."],
#   "elapsed_ms": 18400
# }

Submit Try-On Job

POST/tryon

Submit a virtual try-on request. Provide a person photo and a garment image — the API places the garment on the person with realistic fit, lighting, and draping.

The job runs asynchronously. The response returns a job_id and a poll_url you use to check for results.

Request Body

ParameterTypeRequiredDescription
person_image_urlstringRequiredPublic URL or base64 data URL of the person photo. HTTPS only. Max 10 MB.
garment_image_urlstringRequiredPublic URL or base64 data URL of the garment/clothing item. HTTPS only. Max 10 MB.
garment_typestringOptionalType of garment. One of: top, bottom, glasses, hat, sneakers, clothing. Default: clothing.
model_idstringOptionalAI model to use. See Models for options. Default: image-apps-v2/virtual-try-on.

Response 202 Accepted

FieldTypeDescription
job_idstringUnique job identifier for polling.
model_idstringModel that will process the job.
statusstringAlways "queued" on submission.
credits_usednumberCredits deducted from your balance.
poll_urlstringFull URL to GET for job status — includes model_id pre-encoded.

Examples

cURL
curl -X POST https://app.artemotion.ai/api/v1/tryon \
  -H "Authorization: Bearer $AE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "person_image_url": "https://example.com/person.jpg",
    "garment_image_url": "https://example.com/glasses.jpg",
    "garment_type": "glasses"
  }'
JavaScript
const res = await fetch("https://app.artemotion.ai/api/v1/tryon", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.AE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    person_image_url: "https://example.com/person.jpg",
    garment_image_url: "https://example.com/glasses.jpg",
    garment_type: "glasses",
  }),
});

const { job_id, poll_url, credits_used } = await res.json();
console.log(`Job ${job_id} queued — used ${credits_used} credits`);
Python
import os, requests

response = requests.post(
    "https://app.artemotion.ai/api/v1/tryon",
    headers={
        "Authorization": f"Bearer {os.environ['AE_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "person_image_url": "https://example.com/person.jpg",
        "garment_image_url": "https://example.com/glasses.jpg",
        "garment_type": "glasses",
    },
)
data = response.json()
print(f"Job {data['job_id']} queued — used {data['credits_used']} credits")

Get Job Status

GET/jobs/:job_id?model_id=:model_id

Check the status of a try-on job. Poll this endpoint every 2–5 seconds until status is "completed" or "failed". Use the poll_url from the submit response — it already includes the required model_id.

Query Parameters

ParameterTypeRequiredDescription
model_idstringRequiredThe model ID returned by the submit response. Pre-encoded in poll_url.

Response

The response shape depends on the current status:

StatusDescriptionExtra fields
queuedJob is waiting in queuequeue_position (number | null)
processingModel is generating the image
completedImage is readyimages (string[]), elapsed_ms (number)
failedGeneration failedlogs (string[])
Result image URLs expire after 24 hours. Download and store them in your own storage before then.
JavaScript
async function pollJob(pollUrl, apiKey) {
  while (true) {
    const res = await fetch(pollUrl, {
      headers: { "Authorization": `Bearer ${apiKey}` },
    });
    const data = await res.json();

    if (data.status === "completed") {
      return data.images;          // string[] of image URLs
    }
    if (data.status === "failed") {
      throw new Error("Try-on failed: " + (data.logs?.[0] ?? "unknown"));
    }

    // Still queued or processing — wait and retry
    await new Promise(r => setTimeout(r, 3000));
  }
}

const images = await pollJob(poll_url, process.env.AE_API_KEY);
console.log("Result:", images[0]);

List Models

GET/models

Returns the available try-on models with their capabilities and credit costs. No authentication required.

Model IDCreditsBest for
image-apps-v2/virtual-try-on15All garment types — tops, bottoms, glasses, hats, sneakers
flux-2-lora-gallery/virtual-tryon20Tops and bottoms (LoRA-based generation)
cURL
curl https://app.artemotion.ai/api/v1/models

Vision API

The Vision API gives you programmatic access to 12 image-understanding models — captioning, visual Q&A, object detection, and NSFW moderation.

Like the Try-On API, vision jobs are asynchronous: submit a job via POST /vision, then poll GET /jobs/:job_id until status is "completed". Results are normalized into a single result object regardless of which model was used.

Vision jobs cost 2–5 credits depending on the model. The GET /vision catalog endpoint returns the exact cost for each model.

Submit Vision Job

POST/vision

Submit an image-understanding job. Pass your image as a public HTTPS URL or a base64 data URL (max 10 MB). The field is always image_url regardless of model.

Request Body

ParameterTypeRequiredDescription
image_urlstringRequiredPublic HTTPS URL or base64 data: URL of the image. Max 10 MB. JPEG, PNG, WebP.
model_idstringOptionalVision model to use. See List Vision Models for the catalog. Default: moondream2.
promptstringOptional*Question to ask or text label to detect. Required for VQA, detection, and query models. Optional or ignored for caption and moderation models.
task_typestringOptionalFor moondream-next only: "caption" or "query". Defaults to "caption" when no prompt is provided.
lengthstringOptionalFor moondream3-preview/caption only: "short", "normal", or "long". Default: "normal".
objectstringOptional*For moondream2/object-detection only: the specific object label to detect (e.g. "car", "person"). Required for that model.

Response 202 Accepted

FieldTypeDescription
job_idstringUnique job identifier for polling.
statusstringAlways "queued" on submission.
credits_usednumberCredits deducted from your balance.
credits_leftnumberRemaining credit balance after deduction.
poll_urlstringFull URL to GET for job status — includes model_id pre-encoded.

Examples

cURL
# Captioning — no prompt needed
curl -X POST https://app.artemotion.ai/api/v1/vision \
  -H "Authorization: Bearer $AE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": "moondream3-preview/caption",
    "image_url": "https://example.com/photo.jpg"
  }'

# Response (202):
# {
#   "job_id": "a1b2c3d4...",
#   "status": "queued",
#   "credits_used": 2,
#   "credits_left": 48,
#   "poll_url": "https://app.artemotion.ai/api/v1/jobs/a1b2c3d4...?model_id=moondream3-preview%2Fcaption"
# }

# Visual Q&A
curl -X POST https://app.artemotion.ai/api/v1/vision \
  -H "Authorization: Bearer $AE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": "moondream3-preview/query",
    "image_url": "https://example.com/photo.jpg",
    "prompt": "What objects are on the table?"
  }'

# Object detection
curl -X POST https://app.artemotion.ai/api/v1/vision \
  -H "Authorization: Bearer $AE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": "florence-2-large/object-detection",
    "image_url": "https://example.com/street.jpg"
  }'

# NSFW moderation
curl -X POST https://app.artemotion.ai/api/v1/vision \
  -H "Authorization: Bearer $AE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": "imageutils/nsfw",
    "image_url": "https://example.com/photo.jpg"
  }'
JavaScript
async function submitVision({ imageUrl, modelId, prompt }) {
  const res = await fetch("https://app.artemotion.ai/api/v1/vision", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.AE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ model_id: modelId, image_url: imageUrl, prompt }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`[${err.code}] ${err.error}`);
  }

  return res.json(); // { job_id, poll_url, credits_used, credits_left }
}

// Caption an image
const job = await submitVision({
  modelId:  "moondream3-preview/caption",
  imageUrl: "https://example.com/photo.jpg",
});
console.log(`Queued: ${job.job_id} (${job.credits_used} credits)`);
Python
import os, requests

def submit_vision(image_url, model_id="moondream2", prompt=None):
    payload = {"model_id": model_id, "image_url": image_url}
    if prompt:
        payload["prompt"] = prompt
    r = requests.post(
        "https://app.artemotion.ai/api/v1/vision",
        headers={"Authorization": f"Bearer {os.environ['AE_API_KEY']}"},
        json=payload,
    )
    r.raise_for_status()
    return r.json()

# VQA example
job = submit_vision(
    image_url="https://example.com/photo.jpg",
    model_id="llava-next",
    prompt="Describe this image in detail.",
)
print(f"Queued {job['job_id']} — {job['credits_used']} credits")

Poll Vision Job

GET/jobs/:job_id?model_id=:model_id

Same endpoint as the try-on job poller. When the job is a vision job, the completed response includes a result object instead of an images array. Use the poll_url from the submit response — it already encodes the model_id.

Completed Response — Vision

Every vision model returns the same result object. Fields that don't apply to the model are null — never absent.

FieldTypeNon-null for
result.textstring | nullCaption, VQA, and OCR models
result.objectsarray | nullObject detection models — array of { label, bbox } or { label, x_min, y_min, x_max, y_max }
result.annotated_imagestring | nullDetection models that return a visualised image URL
result.is_nsfwboolean | nullModeration models
result.nsfw_probabilitynumber | nullimageutils/nsfw only — float 0–1

Example Completed Responses

JSON
// Caption / VQA model (e.g. moondream2, llava-next, moondream3-preview/caption)
{
  "job_id": "abc123",
  "model_id": "moondream3-preview/caption",
  "status": "completed",
  "result": {
    "text": "A golden retriever sitting on hardwood floors, looking up at the camera.",
    "objects": null,
    "annotated_image": null,
    "is_nsfw": null,
    "nsfw_probability": null
  },
  "elapsed_ms": 1240
}

// Object detection model (e.g. moondream3-preview/detect)
{
  "job_id": "def456",
  "model_id": "moondream3-preview/detect",
  "status": "completed",
  "result": {
    "text": null,
    "objects": [
      { "label": "stop sign", "x_min": 0.42, "y_min": 0.11, "x_max": 0.58, "y_max": 0.34 }
    ],
    "annotated_image": "https://cdn.artemotion.ai/results/annotated_abc.jpg",
    "is_nsfw": null,
    "nsfw_probability": null
  },
  "elapsed_ms": 980
}

// NSFW moderation model (imageutils/nsfw)
{
  "job_id": "ghi789",
  "model_id": "imageutils/nsfw",
  "status": "completed",
  "result": {
    "text": null,
    "objects": null,
    "annotated_image": null,
    "is_nsfw": false,
    "nsfw_probability": 0.02
  },
  "elapsed_ms": 310
}

Error Responses

All error responses share the same shape: { error: string, code: string }.

HTTP statuscodeCause
400INVALID_REQUESTMissing or malformed field (e.g. no image_url, bad JSON)
400INVALID_MODELUnknown model_id
400MISSING_FIELDModel requires a field that wasn't provided (e.g. prompt for llava-next)
400IMAGE_ERRORImage URL unreachable, private IP, or exceeds 10 MB
401UNAUTHORIZEDMissing or invalid API key
402INSUFFICIENT_CREDITSAccount balance too low for the selected model
429RATE_LIMIT_EXCEEDEDMore than 20 requests per minute
502SUBMISSION_ERRORInference backend rejected the job
502STATUS_ERRORInference backend unreachable while polling
JSON
// 401 — missing or invalid API key
{ "error": "Missing Authorization header", "code": "UNAUTHORIZED" }

// 400 — unknown model
{ "error": "Unknown model_id \"bad-model\". GET /api/v1/vision for the model list.", "code": "INVALID_MODEL" }

// 400 — required field missing
{ "error": "model_id \"llava-next\" requires a \"prompt\" field (e.g. \"Describe this image in detail.\").", "code": "MISSING_FIELD" }

// 402 — not enough credits
{ "error": "Insufficient credits. Required: 4, available: 2.", "code": "INSUFFICIENT_CREDITS" }

// 429 — rate limited
{ "error": "Rate limit exceeded. Max 20 requests per minute.", "code": "RATE_LIMIT_EXCEEDED" }

// 502 — backend failure
{ "error": "Failed to submit job: upstream timeout", "code": "SUBMISSION_ERROR" }
JavaScript
async function pollVisionJob(pollUrl, apiKey) {
  while (true) {
    const res = await fetch(pollUrl, {
      headers: { "Authorization": `Bearer ${apiKey}` },
    });
    const data = await res.json();

    if (data.status === "completed") {
      return data.result; // { text, objects, annotated_image, is_nsfw, nsfw_probability } — all fields always present
    }
    if (data.status === "failed") {
      throw new Error("Vision job failed: " + (data.error ?? "unknown"));
    }

    // queued or processing — wait and retry
    await new Promise(r => setTimeout(r, 2000));
  }
}

// Full example
const job = await fetch("https://app.artemotion.ai/api/v1/vision", {
  method: "POST",
  headers: { "Authorization": `Bearer ${process.env.AE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ model_id: "moondream3-preview/caption", image_url: "https://example.com/photo.jpg" }),
}).then(r => r.json());

const result = await pollVisionJob(job.poll_url, process.env.AE_API_KEY);
console.log("Caption:", result.text);
Python
import os, time, requests

AE_API_KEY = os.environ["AE_API_KEY"]
BASE_URL   = "https://app.artemotion.ai/api/v1"

def run_vision(image_url, model_id="moondream2", **kwargs):
    # Submit
    r = requests.post(
        f"{BASE_URL}/vision",
        headers={"Authorization": f"Bearer {AE_API_KEY}"},
        json={"model_id": model_id, "image_url": image_url, **kwargs},
    )
    r.raise_for_status()
    job = r.json()

    # Poll
    while True:
        time.sleep(2)
        poll = requests.get(
            job["poll_url"],
            headers={"Authorization": f"Bearer {AE_API_KEY}"},
        )
        poll.raise_for_status()
        data = poll.json()

        if data["status"] == "completed":
            return data["result"]
        if data["status"] == "failed":
            raise RuntimeError(f"Vision job failed: {data.get('error')}")

# Caption
result = run_vision("https://example.com/photo.jpg", "moondream3-preview/caption", length="long")
print("Caption:", result["text"])

# VQA
result = run_vision("https://example.com/photo.jpg", "llava-next", prompt="What is in this image?")
print("Answer:", result["text"])

# NSFW check
result = run_vision("https://example.com/photo.jpg", "imageutils/nsfw")
print(f"NSFW: {result['is_nsfw']} (probability: {result.get('nsfw_probability', 'n/a')})")
Vision result data is available for 24 hours from our inference backend. If you need to retain results, persist them in your own storage before they expire.

List Vision Models

GET/vision

Returns the full vision model catalog. No authentication required.

cURL
curl https://app.artemotion.ai/api/v1/vision
Model IDCreditsTasksPrompt
moondream22caption, vqaOptional (VQA)
llava-next4vqa, ocr, captionRequired
nvidia/nemotron-3-nano-omni/vision5vqa, reasoningRequired
nvidia/nemotron-3-nano-omni5vqa, reasoning, multimodalRequired
florence-2-large/object-detection2detectionNot used
moondream2/object-detection2detectionRequired (object field)
moondream3-preview/detect3detectionRequired
x-ailab/nsfw2moderationNot used
moondream3-preview/caption2captionNot used — optional length
moondream3-preview/query2vqaRequired
moondream-next2caption, vqaOptional (sets task_type)
imageutils/nsfw2moderationNot used
Short-form model IDs are also accepted: "moondream2", "llava-next", "imageutils/nsfw", etc.

Vision Error Examples

Vision-specific errors you may encounter:

JSON
// Missing required prompt for a VQA model
// POST /vision with model_id "llava-next" and no prompt field
{
  "error": "model_id \"llava-next\" requires a \"prompt\" field (e.g. \"Describe this image in detail.\").",
  "code": "MISSING_FIELD"
}

// Unknown model ID
{
  "error": "Unknown model_id \"my-custom-model\". GET /api/v1/vision for the model list.",
  "code": "INVALID_MODEL"
}

// Image URL not publicly accessible
{
  "error": "Image error: Failed to fetch image: HTTP 403",
  "code": "IMAGE_ERROR"
}

// Image larger than 10 MB
{
  "error": "Image error: Image exceeds 10 MB",
  "code": "IMAGE_ERROR"
}

// Private/internal URL blocked
{
  "error": "Image error: Private URLs not allowed",
  "code": "IMAGE_ERROR"
}

// moondream-next with task_type=query but no prompt
{
  "error": "model_id \"moondream-next\" with task_type \"query\" requires a \"prompt\" field with the question to ask.",
  "code": "MISSING_FIELD"
}

// Not enough credits
{
  "error": "Insufficient credits. Required: 4, available: 1.",
  "code": "INSUFFICIENT_CREDITS"
}

// Rate limit hit (20 req/min for vision)
{
  "error": "Rate limit exceeded. Max 20 requests per minute.",
  "code": "RATE_LIMIT_EXCEEDED"
}

Garment Types

The garment_type parameter tells the model what kind of garment to expect. Using the correct type improves results significantly.

ValueExamplesNotes
topT-shirts, shirts, blouses, sweaters, jacketsWorks well with both models
bottomJeans, pants, shorts, skirtsWorks well with both models
glassesSunglasses, eyeglasses, gogglesUse Image Apps model for best results
hatCaps, beanies, hats, headbandsUse Image Apps model for best results
sneakersSneakers, shoes, boots, sandalsUse Image Apps model for best results
clothingAny other garmentGeneric fallback

Error Codes

Errors follow a consistent JSON format: { error: string, code: string }.

CodeHTTPDescription
UNAUTHORIZED401Missing, malformed, or invalid API key.
INSUFFICIENT_CREDITS402Your account balance is too low. Add credits at Pricing.
INVALID_REQUEST400Missing required fields, invalid JSON, or unsupported URL format.
IMAGE_ERROR400Could not download or process the provided image URL. Check the URL is publicly accessible and points to an image file.
RATE_LIMIT_EXCEEDED429Too many requests. Retry after 60 seconds.
SUBMISSION_ERROR502Upstream error submitting to the AI model. Credits are automatically refunded.
METHOD_NOT_ALLOWED405Wrong HTTP method for this endpoint.
JSON
{
  "error": "Insufficient credits. Required: 15, available: 3.",
  "code": "INSUFFICIENT_CREDITS"
}

Rate Limits

LimitValue
Try-on submissions per minute10 per API key
Status polls per minute120 per API key
Max image size10 MB per image
Image formatsJPEG, PNG, WebP

When you hit a rate limit you'll receive a 429 response with code RATE_LIMIT_EXCEEDED. Wait 60 seconds before retrying.

Webhooks (skip polling)

For long-running jobs you can register a webhook at submit time instead of polling. When the job finishes (success or failure) we POST a signed JSON payload to the URL you provided. Webhooks are supported on both /generate and /vision submits.

Registering the webhook

Add two optional fields to your submit body. webhook_url must be HTTPS.webhook_secret is whatever you want — it's used to sign the delivery so you can verify it actually came from ArtEmotion.

JSON
{
  "model_id":       "flux-pro",
  "prompt":         "Cinematic portrait, golden hour",
  "webhook_url":    "https://your-app.com/api/webhooks/artemotion",
  "webhook_secret": "whsec_pick_any_random_string"
}

Payload

We POST a JSON body with the result (for vision) or image URLs (for generate). The completed payload mirrors what you'd get from GET /jobs/:requestId.

JSON
{
  "request_id": "abc123-def456",
  "model_id":   "flux-pro",
  "kind":       "image",
  "status":     "completed",
  "timestamp":  1716000000,
  "result":     { "images": ["https://cdn.artemotion.ai/...png"] }
}

Headers

HeaderValue
X-ArtEmotion-Eventjob.completed or job.failed
X-ArtEmotion-TimestampUnix epoch seconds when we signed the request
X-ArtEmotion-Signaturev1=<hex> — only sent when you provide webhook_secret
User-AgentArtEmotion-Webhooks/1.0

Verifying the signature

HMAC-SHA256 over `${timestamp}.${rawBody}` using your secret as the key, then compare constant-time against the hex portion of X-ArtEmotion-Signature. Reject the request if the timestamp drifts more than 5 minutes from your server clock.

Node.js
import crypto from "node:crypto";

export function verifyWebhook(rawBody, headers, secret) {
  const sig = headers["x-artemotion-signature"];
  const ts  = headers["x-artemotion-timestamp"];
  if (!sig?.startsWith("v1=") || !ts) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");
  const provided = sig.slice(3);

  if (provided.length !== expected.length) return false;
  if (!crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected))) return false;

  // 5-minute replay window
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  return true;
}

Delivery guarantees

  • Exactly once per terminal status. We fire when the job-status poll first sees completed or failed, then mark the row delivered.
  • No retries. If your endpoint is down we don't retry — fall back to polling /jobs/:requestId any time after.
  • Manual redirects. We do not follow HTTP 3xx — keep your endpoint at one URL.
  • 8-second timeout. Respond within 8 seconds even if you defer processing.

Polling Pattern

Virtual try-on jobs take 10–30 seconds. The recommended approach is to poll every 3 seconds with exponential backoff on errors.

JavaScript
async function runTryon({ personUrl, garmentUrl, garmentType, apiKey }) {
  // 1. Submit
  const submit = await fetch(&quot;https://app.artemotion.ai/api/v1/tryon&quot;, {
    method: &quot;POST&quot;,
    headers: {
      &quot;Authorization&quot;: `Bearer ${apiKey}`,
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
    body: JSON.stringify({
      person_image_url: personUrl,
      garment_image_url: garmentUrl,
      garment_type: garmentType ?? &quot;clothing&quot;,
    }),
  });

  if (!submit.ok) {
    const err = await submit.json();
    throw new Error(`Submit failed [${err.code}]: ${err.error}`);
  }

  const { poll_url, credits_used } = await submit.json();
  console.log(`Queued — ${credits_used} credits used`);

  // 2. Poll every 3 seconds
  let delay = 3000;
  while (true) {
    await new Promise(r =&gt; setTimeout(r, delay));
    const poll = await fetch(poll_url, {
      headers: { &quot;Authorization&quot;: `Bearer ${apiKey}` },
    });
    const data = await poll.json();

    if (data.status === &quot;completed&quot;) return data.images;
    if (data.status === &quot;failed&quot;)    throw new Error(&quot;Generation failed&quot;);

    delay = Math.min(delay * 1.2, 10_000); // slow down over time
  }
}

// Usage
const images = await runTryon({
  personUrl:   &quot;https://example.com/model.jpg&quot;,
  garmentUrl:  &quot;https://example.com/tshirt.jpg&quot;,
  garmentType: &quot;top&quot;,
  apiKey:      process.env.AE_API_KEY,
});
console.log(&quot;Result image:&quot;, images[0]);

SDK Examples

There is no official SDK yet — the API is simple enough to call directly with any HTTP client. Here are complete examples.

Python

Python
import os, time, requests

AE_API_KEY = os.environ[&quot;AE_API_KEY&quot;]
BASE_URL   = &quot;https://app.artemotion.ai/api/v1&quot;

def run_tryon(person_url, garment_url, garment_type=&quot;clothing&quot;):
    # Submit
    r = requests.post(
        f&quot;{BASE_URL}/tryon&quot;,
        headers={&quot;Authorization&quot;: f&quot;Bearer {AE_API_KEY}&quot;},
        json={
            &quot;person_image_url&quot;:  person_url,
            &quot;garment_image_url&quot;: garment_url,
            &quot;garment_type&quot;:      garment_type,
        },
    )
    r.raise_for_status()
    job = r.json()
    print(f&quot;Queued: {job[&#39;job_id&#39;]} ({job[&#39;credits_used&#39;]} credits)&quot;)

    # Poll
    delay = 3
    while True:
        time.sleep(delay)
        poll = requests.get(
            job[&quot;poll_url&quot;],
            headers={&quot;Authorization&quot;: f&quot;Bearer {AE_API_KEY}&quot;},
        )
        poll.raise_for_status()
        data = poll.json()

        if data[&quot;status&quot;] == &quot;completed&quot;:
            return data[&quot;images&quot;]
        if data[&quot;status&quot;] == &quot;failed&quot;:
            raise RuntimeError(&quot;Generation failed&quot;)
        delay = min(delay * 1.2, 10)

images = run_tryon(
    person_url=&quot;https://example.com/model.jpg&quot;,
    garment_url=&quot;https://example.com/hoodie.jpg&quot;,
    garment_type=&quot;top&quot;,
)
print(&quot;Result:&quot;, images[0])

Node.js (TypeScript)

TypeScript
import fetch from &quot;node-fetch&quot;; // or built-in fetch in Node 18+

const AE_API_KEY = process.env.AE_API_KEY!;
const BASE_URL   = &quot;https://app.artemotion.ai/api/v1&quot;;

async function runTryon(opts: {
  personUrl:   string;
  garmentUrl:  string;
  garmentType?: string;
}): Promise&lt;string[]&gt; {
  const submit = await fetch(`${BASE_URL}/tryon`, {
    method:  &quot;POST&quot;,
    headers: {
      Authorization:  `Bearer ${AE_API_KEY}`,
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
    body: JSON.stringify({
      person_image_url:  opts.personUrl,
      garment_image_url: opts.garmentUrl,
      garment_type:      opts.garmentType ?? &quot;clothing&quot;,
    }),
  });

  if (!submit.ok) throw new Error(`Submit failed: ${submit.status}`);
  const job = await submit.json() as { poll_url: string; credits_used: number };
  console.log(`Queued — ${job.credits_used} credits used`);

  let delay = 3000;
  while (true) {
    await new Promise(r =&gt; setTimeout(r, delay));
    const poll = await fetch(job.poll_url, {
      headers: { Authorization: `Bearer ${AE_API_KEY}` },
    });
    const data = await poll.json() as { status: string; images?: string[] };
    if (data.status === &quot;completed&quot;) return data.images!;
    if (data.status === &quot;failed&quot;)    throw new Error(&quot;Generation failed&quot;);
    delay = Math.min(delay * 1.2, 10_000);
  }
}

// Usage
const images = await runTryon({
  personUrl:   &quot;https://example.com/model.jpg&quot;,
  garmentUrl:  &quot;https://example.com/jeans.jpg&quot;,
  garmentType: &quot;bottom&quot;,
});
console.log(&quot;Result:&quot;, images[0]);