Skip to main content
ArtEmotion/API Reference

ArtEmotion API

The ArtEmotion REST API gives your application programmatic access to 400+ AI models spanning image, video, audio, 3D, and vision — the same models available in the studio, accessible via a single JSON endpoint with one shared credit balance.

The API is JSON-first, uses standard HTTP verbs and status codes, and authenticates via Bearer tokens. Long-running generations are submitted asynchronously — you submit a job and poll (or use webhooks) for results. Short jobs return inline.

Every generation job consumes credits from your ArtEmotion account. Requests authenticated with an ArtEmotion API key use wholesale pricing targeted at a 20% gross margin over provider cost; whole-credit rounding applies. Use /estimate for the authoritative charge. Monitor your balance at Settings → API Keys.

Base URL

Base URLhttps://www.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.

BASH
# Step 1 — Submit the try-on job
curl -X POST https://www.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://www.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
# }

Generate

POST/generate

The main entry point: run any of ArtEmotion's 400+ models — image, video, audio, voice, and 3D — with one request shape. Pick a model_id from GET /models, then pass the model's native input fields (prompt, image_url,duration, …). Jobs run asynchronously and return a poll_url.

Request body

FieldTypeDescription
model_idstringRequiredModel identifier from GET /models?catalog=full.
promptstringOptionalText prompt, up to 4,000 characters.
image_url / video_url / audio_url / second_audio_urlstringOptionalSingle HTTPS media inputs for image, video, audio, lipsync, avatar, and multi-speaker models.
image_urlsstring[]OptionalMultiple HTTPS image inputs. Item limits are model-specific.
reference_image_url / start_image_url / end_image_urlstringOptionalSingle reference image and first/last-frame inputs. Accepted combinations are model-specific.
reference_image_urls / reference_video_urls / reference_audio_urlsstring[]OptionalMultiple reference media inputs. Item limits and accepted combinations are model-specific.
aspect_ratiostringOptionale.g. "16:9", "9:16", "1:1" — must be one the model supports.
resolutionstringOptionalOutput resolution, such as "1080p". Must be one the model supports.
duration / source_video_durationnumberOptionalOutput length and source clip length in seconds for models that price or validate against duration.
num_imagesintegerOptionalImage batch size. The supported range is returned in the selected model's input schema.
seedance_task_type / seedance_image_rolestringOptionalSeedance 2.5 controls for generate, edit, or extend and auto, reference, or first_frame image roles.
extraobjectOptionalModel-specific schema fields such as seed, negative_prompt, guidance_scale, quality, output_format, and generate_audio. Use the model's schema for the authoritative field list.
max_creditsnumberOptionalReject the job up front if the estimated cost exceeds this value.
webhook_url / webhook_secretstringOptionalHTTPS URL to POST results to when the job completes, with optional HMAC-SHA256 signing. See Webhooks.

Response 202 Accepted

FieldTypeDescription
job_idstringUnique job identifier for polling.
kindstringimage, video, audio, or 3d.
statusstringAlways "queued" on submission.
credits_usednumberCredits deducted (refunded automatically if the job fails).
credits_leftnumberBalance after the deduction.
poll_urlstringFull URL to GET for job status — includes model_id pre-encoded.

Examples

BASH
# Text-to-image
curl -X POST https://www.artemotion.ai/api/v1/generate \
  -H "Authorization: Bearer $AE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": "fal-ai/flux/dev",
    "prompt": "a lighthouse at dusk, photorealistic",
    "aspect_ratio": "16:9",
    "max_credits": 50
  }'

# Image-to-video
curl -X POST https://www.artemotion.ai/api/v1/generate \
  -H "Authorization: Bearer $AE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": "fal-ai/kling-video/v2.5-turbo/pro/image-to-video",
    "prompt": "slow cinematic dolly forward",
    "image_url": "https://example.com/still.jpg",
    "duration": 5
  }'
JavaScript
const res = await fetch("https://www.artemotion.ai/api/v1/generate", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.AE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model_id: "fal-ai/flux/dev",
    prompt: "a lighthouse at dusk, photorealistic",
    aspect_ratio: "16:9",
  }),
});

const { job_id, poll_url, credits_used } = await res.json();
console.log(`Job ${job_id} queued — used ${credits_used} credits`);
// Poll poll_url (see Polling Pattern) or register a webhook_url instead.
iRate limit: 20 requests/minute per account. A hard per-request ceiling of 2,000 credits protects against runaway loops — use max_credits for tighter control.

List Models

GET/models?catalog=full

The full generation catalog: every model's id, kind, supported inputs (supports / requires), allowed aspect_ratios, resolutions and durations, plus base_credits — the API-key price at the model's default settings. Each model also reports api_gross_margin_percent: 20.

Model-by-model API & LLM schemasSearch every available model, inspect all accepted settings, required fields, enums and ranges, and copy its raw JSON Schema.
Browse pricing & schemas →
BASH
# Full catalog
curl "https://www.artemotion.ai/api/v1/models?catalog=full" \
  -H "Authorization: Bearer $AE_API_KEY"

# Include each model's complete LLM-ready input_schema
curl "https://www.artemotion.ai/api/v1/models?catalog=full&include_schema=true" \
  -H "Authorization: Bearer $AE_API_KEY"

# One model with its parameters and complete schema
curl "https://www.artemotion.ai/api/v1/models?id=fal-ai%2Fnano-banana-pro" \
  -H "Authorization: Bearer $AE_API_KEY"

# Filter by kind and sort by price
curl "https://www.artemotion.ai/api/v1/models?catalog=full&kind=video&sort=cheapest" \
  -H "Authorization: Bearer $AE_API_KEY"

Useful query params: kind (image / video / audio / 3d), sort (cheapest, priciest, best, popular, newest), search for substring matching against id and name, include_schema=true for each model's exact LLM-ready input_schema, and limit (default 200). Without catalog=full the endpoint returns the legacy try-on catalog only.

Estimate Cost

POST/estimate

Preview a job's credit cost without submitting it. Accepts the same body as /generate and returns the estimate plus your balance.

BASH
curl -X POST https://www.artemotion.ai/api/v1/estimate \
  -H "Authorization: Bearer $AE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model_id": "fal-ai/kling-video/v2.5-turbo/pro/text-to-video", "duration": 10 }'

# {
#   "model_id": "fal-ai/kling-video/v2.5-turbo/pro/text-to-video",
#   "kind": "video",
#   "estimated_credits": 70,
#   "balance": 1200,
#   "sufficient": true,
#   "deficit": 0
# }

Credit Balance

GET/credits

Current balance and plan summary. Cheap to call — fine to check before every job.

BASH
curl https://www.artemotion.ai/api/v1/credits \
  -H "Authorization: Bearer $AE_API_KEY"

# {
#   "credits": 1200,
#   "plan": { "id": "creator", "billing": "monthly", "status": "active", "current_period_end": 1751155200000 }
# }

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.
num_imagesintegerOptionalNumber of outputs from 1 to 4. Supported by google/virtual-try-on only. Default: 1.

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

BASH
curl -X POST https://www.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://www.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://www.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
google/virtual-try-on15 per imageRealistic person and clothing-product fidelity; up to 4 outputs
image-apps-v2/virtual-try-on15All garment types — tops, bottoms, glasses, hats, sneakers
flux-2-lora-gallery/virtual-tryon13Tops and bottoms (LoRA-based generation)
BASH
curl https://www.artemotion.ai/api/v1/models

Pipelines

Pipelines are saved multi-step chains built on the Pipelines canvas — e.g. prompt → image → animate → score. The API lets you list your saved pipelines and execute them server-side: each generator node runs in topological order, feeding its output to downstream nodes, with credits charged per node at the same API-key wholesale rate as /generate.

GET/pipelines

Returns your saved pipelines (id, name, node/edge counts, timestamps). Use the id with /pipelines/run.

Run a Pipeline

POST/pipelines/run

Request Body

FieldTypeRequiredDescription
pipeline_idstringRequiredThe saved pipeline to execute (from GET /pipelines).

Responds 202 with a run_id and poll_url. Generation happens asynchronously; poll until the run is terminal. Each node charges credits on completion (failed nodes are refunded).

Poll a Pipeline Run

GET/pipelines/runs/:run_id

Returns the run status (running · completed · failed ·cancelled) plus per-node states with output URLs and credits as they finish. Runs expire from the poll cache one hour after completion. Send DELETE to the same URL to cancel a run in flight. The MCP server exposes the same flow as the run_pipeline andget_pipeline_run tools.

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 use the API-key 20% gross-margin target. The GET /vision catalog endpoint returns the exact whole-credit 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

BASH
# Captioning — no prompt needed
curl -X POST https://www.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://www.artemotion.ai/api/v1/jobs/a1b2c3d4...?model_id=moondream3-preview%2Fcaption"
# }

# Visual Q&A
curl -X POST https://www.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://www.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://www.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://www.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://www.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.textstringnullCaption, VQA, and OCR models
result.objectsarraynullObject detection models — array of { label, bbox } or { label, x_min, y_min, x_max, y_max }
result.annotated_imagestringnullDetection models that return a visualised image URL
result.is_nsfwbooleannullModeration models
result.nsfw_probabilitynumbernullimageutils/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://www.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://www.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.

BASH
curl https://www.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"
}

API Pricing & Model Schemas

Select any available model to see its baseline API-key price and exact LLM-ready JSON Schema without leaving this reference. API-key calls target a 20% gross margin, so these prices are lower than regular Studio pricing. Duration, resolution, quality, and output count can change the final charge; POST /estimate is authoritative before submission.

iTypes follow JSON Schema: integer means a whole number, while number also permits decimals. Strings, arrays, objects, booleans, and null values keep the same type color everywhere in this reference.
480 available models480 matching

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.
COST_CAP_EXCEEDED402The estimated charge is higher than your request's max_credits.
STORAGE_LIMIT_EXCEEDED402The account has no remaining storage allowance for a new result.
INVALID_REQUEST400Missing required fields, invalid JSON, or unsupported URL format.
INVALID_MODEL400 / 404 / 410The model ID is unknown, not exposed to this API caller, retired, or no longer available.
MISSING_FIELD400A field required by the selected model schema was not supplied.
PROMPT_BLOCKED400The prompt was rejected by the safety policy.
IMAGE_ERROR400Could not download or process the provided image URL. Check the URL is publicly accessible and points to an image file.
AGE_VERIFICATION_REQUIRED403The selected restricted model requires an age-verified account.
NOT_FOUND404The job, upload, or other requested resource does not exist for this account.
RATE_LIMIT_EXCEEDED429The account exceeded an endpoint request window. Honor the Retry-After response header.
API_KEY_CAP_EXCEEDED429The API key reached its configured daily or monthly credit cap.
SUBMISSION_ERROR502Upstream error submitting to the AI model. Credits are automatically refunded.
STATUS_ERROR502The upstream job status could not be read. Retry the same poll URL with backoff.
MODEL_NOT_AVAILABLE / MODEL_DISABLED503The model is temporarily unavailable or disabled by an operator.
BILLING_UNAVAILABLE503Pricing could not be determined safely, so the request was rejected before submission.
METHOD_NOT_ALLOWED405Wrong HTTP method for this endpoint.
JSON
{
  "error": "Insufficient credits. Required: 15, available: 3.",
  "code": "INSUFFICIENT_CREDITS"
}

Rate Limits

EndpointLimitScope
POST /generate20 per minuteAccount
POST /vision20 per minuteAccount
POST /tryon10 per minuteAccount
GET /jobs/:job_id120 per minuteAccount
POST /upload30 per minuteAccount
POST /merge10 per 5 minutesAccount
POST /edit6 per 10 minutesAccount

Rate limits are shared by API keys belonging to the same account. A 429 response uses code RATE_LIMIT_EXCEEDED and includes Retry-After; wait for that many seconds before retrying. API-key daily and monthly spend caps are separate and return API_KEY_CAP_EXCEEDED.

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

  • One claimed delivery attempt per terminal status. An atomic claim prevents normal concurrent pollers and provider callbacks from sending the same event twice.
  • 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

Poll the exact poll_url returned by submission. Start near 3 seconds, honor Retry-After on 429, back off on transient server/network failures, and enforce a client-side deadline.

JavaScript
async function runTryon({ personUrl, garmentUrl, garmentType, apiKey }) {
  // 1. Submit
  const submit = await fetch("https://www.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",
    }),
  });

  const submitted = await submit.json().catch(() => ({}));
  if (!submit.ok) {
    throw new Error(`Submit failed [${submitted.code ?? submit.status}]: ${submitted.error ?? submit.statusText}`);
  }

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

  // 2. Poll with a 15-minute client-side deadline
  const deadline = Date.now() + 15 * 60_000;
  let delay = 3000;
  while (Date.now() < deadline) {
    await new Promise(r => setTimeout(r, delay));

    let poll;
    try {
      poll = await fetch(poll_url, {
        headers: { "Authorization": `Bearer ${apiKey}` },
      });
    } catch {
      delay = Math.min(Math.round(delay * 1.5), 10_000);
      continue; // transient network failure
    }

    if (poll.status === 429) {
      const retryAfter = Number(poll.headers.get("Retry-After"));
      delay = Number.isFinite(retryAfter) && retryAfter > 0
        ? retryAfter * 1000
        : Math.min(Math.round(delay * 1.5), 10_000);
      continue;
    }

    const data = await poll.json().catch(() => ({}));
    if (!poll.ok) {
      if (poll.status >= 500) {
        delay = Math.min(Math.round(delay * 1.5), 10_000);
        continue; // retry transient provider/status errors
      }
      throw new Error(`Poll failed [${data.code ?? poll.status}]: ${data.error ?? poll.statusText}`);
    }

    if (data.status === "completed") return data.images;
    if (data.status === "failed") {
      throw new Error("Generation failed: " + (data.error ?? "unknown provider error"));
    }

    delay = Math.min(Math.round(delay * 1.2), 10_000);
  }

  throw new Error("Polling timed out after 15 minutes");
}

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

Python
import os, time, requests

API_KEY = os.environ["AE_API_KEY"]
BASE_URL = "https://www.artemotion.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

def run_tryon(person_url, garment_url, garment_type="clothing"):
    submit = requests.post(
        f"{BASE_URL}/tryon",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={
            "person_image_url": person_url,
            "garment_image_url": garment_url,
            "garment_type": garment_type,
        },
        timeout=30,
    )
    submit.raise_for_status()
    job = submit.json()

    deadline = time.monotonic() + 15 * 60
    delay = 3.0
    while time.monotonic() < deadline:
        time.sleep(delay)
        poll = requests.get(job["poll_url"], headers=HEADERS, timeout=30)
        if poll.status_code == 429:
            delay = float(poll.headers.get("Retry-After", delay))
            continue
        poll.raise_for_status()
        data = poll.json()
        if data["status"] == "completed":
            return data["images"]
        if data["status"] == "failed":
            raise RuntimeError(data.get("error", "Generation failed"))
        delay = min(delay * 1.2, 10)
    raise TimeoutError("Polling timed out after 15 minutes")

images = run_tryon(
    "https://example.com/model.jpg",
    "https://example.com/hoodie.jpg",
    "top",
)
print(images[0])