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.
Base URL
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.
Authorization: Bearer ae_live_your_key_hereAuthorization: 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.
# 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
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
| Parameter | Type | Required | Description |
|---|---|---|---|
person_image_url | string | Required | Public URL or base64 data URL of the person photo. HTTPS only. Max 10 MB. |
garment_image_url | string | Required | Public URL or base64 data URL of the garment/clothing item. HTTPS only. Max 10 MB. |
garment_type | string | Optional | Type of garment. One of: top, bottom, glasses, hat, sneakers, clothing. Default: clothing. |
model_id | string | Optional | AI model to use. See Models for options. Default: image-apps-v2/virtual-try-on. |
Response 202 Accepted
| Field | Type | Description |
|---|---|---|
job_id | string | Unique job identifier for polling. |
model_id | string | Model that will process the job. |
status | string | Always "queued" on submission. |
credits_used | number | Credits deducted from your balance. |
poll_url | string | Full URL to GET for job status — includes model_id pre-encoded. |
Examples
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"
}'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`);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
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
| Parameter | Type | Required | Description |
|---|---|---|---|
model_id | string | Required | The model ID returned by the submit response. Pre-encoded in poll_url. |
Response
The response shape depends on the current status:
| Status | Description | Extra fields |
|---|---|---|
| queued | Job is waiting in queue | queue_position (number | null) |
| processing | Model is generating the image | — |
| completed | Image is ready | images (string[]), elapsed_ms (number) |
| failed | Generation failed | logs (string[]) |
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
Returns the available try-on models with their capabilities and credit costs. No authentication required.
| Model ID | Credits | Best for |
|---|---|---|
image-apps-v2/virtual-try-on | 15 | All garment types — tops, bottoms, glasses, hats, sneakers |
flux-2-lora-gallery/virtual-tryon | 20 | Tops and bottoms (LoRA-based generation) |
curl https://app.artemotion.ai/api/v1/modelsVision 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 catalog endpoint returns the exact cost for each model.Submit Vision Job
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
| Parameter | Type | Required | Description |
|---|---|---|---|
image_url | string | Required | Public HTTPS URL or base64 data: URL of the image. Max 10 MB. JPEG, PNG, WebP. |
model_id | string | Optional | Vision model to use. See List Vision Models for the catalog. Default: moondream2. |
prompt | string | Optional* | Question to ask or text label to detect. Required for VQA, detection, and query models. Optional or ignored for caption and moderation models. |
task_type | string | Optional | For moondream-next only: "caption" or "query". Defaults to "caption" when no prompt is provided. |
length | string | Optional | For moondream3-preview/caption only: "short", "normal", or "long". Default: "normal". |
object | string | Optional* | For moondream2/object-detection only: the specific object label to detect (e.g. "car", "person"). Required for that model. |
Response 202 Accepted
| Field | Type | Description |
|---|---|---|
job_id | string | Unique job identifier for polling. |
status | string | Always "queued" on submission. |
credits_used | number | Credits deducted from your balance. |
credits_left | number | Remaining credit balance after deduction. |
poll_url | string | Full URL to GET for job status — includes model_id pre-encoded. |
Examples
# 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"
}'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)`);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
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.
| Field | Type | Non-null for |
|---|---|---|
result.text | string | null | Caption, VQA, and OCR models |
result.objects | array | null | Object detection models — array of { label, bbox } or { label, x_min, y_min, x_max, y_max } |
result.annotated_image | string | null | Detection models that return a visualised image URL |
result.is_nsfw | boolean | null | Moderation models |
result.nsfw_probability | number | null | imageutils/nsfw only — float 0–1 |
Example Completed Responses
// 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 status | code | Cause |
|---|---|---|
400 | INVALID_REQUEST | Missing or malformed field (e.g. no image_url, bad JSON) |
400 | INVALID_MODEL | Unknown model_id |
400 | MISSING_FIELD | Model requires a field that wasn't provided (e.g. prompt for llava-next) |
400 | IMAGE_ERROR | Image URL unreachable, private IP, or exceeds 10 MB |
401 | UNAUTHORIZED | Missing or invalid API key |
402 | INSUFFICIENT_CREDITS | Account balance too low for the selected model |
429 | RATE_LIMIT_EXCEEDED | More than 20 requests per minute |
502 | SUBMISSION_ERROR | Inference backend rejected the job |
502 | STATUS_ERROR | Inference backend unreachable while polling |
// 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" }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);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')})")List Vision Models
Returns the full vision model catalog. No authentication required.
curl https://app.artemotion.ai/api/v1/vision| Model ID | Credits | Tasks | Prompt |
|---|---|---|---|
moondream2 | 2 | caption, vqa | Optional (VQA) |
llava-next | 4 | vqa, ocr, caption | Required |
nvidia/nemotron-3-nano-omni/vision | 5 | vqa, reasoning | Required |
nvidia/nemotron-3-nano-omni | 5 | vqa, reasoning, multimodal | Required |
florence-2-large/object-detection | 2 | detection | Not used |
moondream2/object-detection | 2 | detection | Required (object field) |
moondream3-preview/detect | 3 | detection | Required |
x-ailab/nsfw | 2 | moderation | Not used |
moondream3-preview/caption | 2 | caption | Not used — optional length |
moondream3-preview/query | 2 | vqa | Required |
moondream-next | 2 | caption, vqa | Optional (sets task_type) |
imageutils/nsfw | 2 | moderation | Not used |
"moondream2", "llava-next", "imageutils/nsfw", etc.Vision Error Examples
Vision-specific errors you may encounter:
// 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.
| Value | Examples | Notes |
|---|---|---|
top | T-shirts, shirts, blouses, sweaters, jackets | Works well with both models |
bottom | Jeans, pants, shorts, skirts | Works well with both models |
glasses | Sunglasses, eyeglasses, goggles | Use Image Apps model for best results |
hat | Caps, beanies, hats, headbands | Use Image Apps model for best results |
sneakers | Sneakers, shoes, boots, sandals | Use Image Apps model for best results |
clothing | Any other garment | Generic fallback |
Error Codes
Errors follow a consistent JSON format: { error: string, code: string }.
| Code | HTTP | Description |
|---|---|---|
| UNAUTHORIZED | 401 | Missing, malformed, or invalid API key. |
| INSUFFICIENT_CREDITS | 402 | Your account balance is too low. Add credits at Pricing. |
| INVALID_REQUEST | 400 | Missing required fields, invalid JSON, or unsupported URL format. |
| IMAGE_ERROR | 400 | Could not download or process the provided image URL. Check the URL is publicly accessible and points to an image file. |
| RATE_LIMIT_EXCEEDED | 429 | Too many requests. Retry after 60 seconds. |
| SUBMISSION_ERROR | 502 | Upstream error submitting to the AI model. Credits are automatically refunded. |
| METHOD_NOT_ALLOWED | 405 | Wrong HTTP method for this endpoint. |
{
"error": "Insufficient credits. Required: 15, available: 3.",
"code": "INSUFFICIENT_CREDITS"
}Rate Limits
| Limit | Value |
|---|---|
| Try-on submissions per minute | 10 per API key |
| Status polls per minute | 120 per API key |
| Max image size | 10 MB per image |
| Image formats | JPEG, 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.
{
"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.
{
"request_id": "abc123-def456",
"model_id": "flux-pro",
"kind": "image",
"status": "completed",
"timestamp": 1716000000,
"result": { "images": ["https://cdn.artemotion.ai/...png"] }
}Headers
| Header | Value |
|---|---|
X-ArtEmotion-Event | job.completed or job.failed |
X-ArtEmotion-Timestamp | Unix epoch seconds when we signed the request |
X-ArtEmotion-Signature | v1=<hex> — only sent when you provide webhook_secret |
User-Agent | ArtEmotion-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.
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
completedorfailed, then mark the row delivered. - No retries. If your endpoint is down we don't retry — fall back to polling
/jobs/:requestIdany 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.
async function runTryon({ personUrl, garmentUrl, garmentType, apiKey }) {
// 1. Submit
const submit = await fetch("https://app.artemotion.ai/api/v1/tryon", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
person_image_url: personUrl,
garment_image_url: garmentUrl,
garment_type: garmentType ?? "clothing",
}),
});
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 => setTimeout(r, delay));
const poll = await fetch(poll_url, {
headers: { "Authorization": `Bearer ${apiKey}` },
});
const data = await poll.json();
if (data.status === "completed") return data.images;
if (data.status === "failed") throw new Error("Generation failed");
delay = Math.min(delay * 1.2, 10_000); // slow down over time
}
}
// Usage
const images = await runTryon({
personUrl: "https://example.com/model.jpg",
garmentUrl: "https://example.com/tshirt.jpg",
garmentType: "top",
apiKey: process.env.AE_API_KEY,
});
console.log("Result image:", 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
import os, time, requests
AE_API_KEY = os.environ["AE_API_KEY"]
BASE_URL = "https://app.artemotion.ai/api/v1"
def run_tryon(person_url, garment_url, garment_type="clothing"):
# Submit
r = requests.post(
f"{BASE_URL}/tryon",
headers={"Authorization": f"Bearer {AE_API_KEY}"},
json={
"person_image_url": person_url,
"garment_image_url": garment_url,
"garment_type": garment_type,
},
)
r.raise_for_status()
job = r.json()
print(f"Queued: {job['job_id']} ({job['credits_used']} credits)")
# Poll
delay = 3
while True:
time.sleep(delay)
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["images"]
if data["status"] == "failed":
raise RuntimeError("Generation failed")
delay = min(delay * 1.2, 10)
images = run_tryon(
person_url="https://example.com/model.jpg",
garment_url="https://example.com/hoodie.jpg",
garment_type="top",
)
print("Result:", images[0])Node.js (TypeScript)
import fetch from "node-fetch"; // or built-in fetch in Node 18+
const AE_API_KEY = process.env.AE_API_KEY!;
const BASE_URL = "https://app.artemotion.ai/api/v1";
async function runTryon(opts: {
personUrl: string;
garmentUrl: string;
garmentType?: string;
}): Promise<string[]> {
const submit = await fetch(`${BASE_URL}/tryon`, {
method: "POST",
headers: {
Authorization: `Bearer ${AE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
person_image_url: opts.personUrl,
garment_image_url: opts.garmentUrl,
garment_type: opts.garmentType ?? "clothing",
}),
});
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 => 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 === "completed") return data.images!;
if (data.status === "failed") throw new Error("Generation failed");
delay = Math.min(delay * 1.2, 10_000);
}
}
// Usage
const images = await runTryon({
personUrl: "https://example.com/model.jpg",
garmentUrl: "https://example.com/jeans.jpg",
garmentType: "bottom",
});
console.log("Result:", images[0]);