API Reference

Virtual Try-On API

Send a photo of a person and a photo of a garment — get back a photorealistic image of that person wearing that garment. Submit a job, then poll for the result or let us call you back.

POSThttps://api.benitoai.com/v1/tryon
POSThttps://api.benitoai.com/v1/tryon/upload
GEThttps://api.benitoai.com/v1/tryon/jobs/{id}

Breaking change

This API used to be fully synchronous — one request, one blocking response carrying the finished image. It is now asynchronous. POST /v1/tryon (and /upload) returns a job id immediately with 202; you get the result by polling GET /v1/tryon/jobs/{id} and/or by supplying a callback_url. If you integrated against an older version of these docs, update your integration — the old single-blocking-call flow no longer works.

Using an AI coding agent?

Download a condensed markdown spec covering every endpoint, field, job status, and error code — or copy it straight into your chat. English only.

Create an API key in your dashboard

Open the API keys tab in your BenitoAI dashboard and create a key in a couple of clicks. A key is shown only once, at creation time — copy it straight into your server's secret store. You can also give an individual key its own credit limit there.

01

Key concepts

Read these first — they shape how you integrate.

  • Asynchronous job model. Submitting a request returns a job id immediately — it does not wait for generation to finish. You then either poll GET /v1/tryon/jobs/{id} until status is completed (or failed), or supply a callback_url and we'll POST the finished job to it. Both mechanisms are always available — polling never stops working just because you also gave a callback_url.
  • Generation takes ~10–60 seconds once a job starts processing, plus any queue wait. Design your integration around checking back later (polling every 1–3s is reasonable), not around a single long-held connection.
  • Two ways to provide images. POST /v1/tryon takes JSON with image URLs that are publicly reachable over http/https — the server downloads them itself. POST /v1/tryon/upload takes multipart/form-data so you can upload the raw file instead, which saves you hosting an image publicly just so this API can download it again.
  • Output is a hosted PNG URL. The API never returns base64 — you get back a URL to the generated .png.
02

Authentication

Every request requires a Bearer API key in the Authorization header:

http
Authorization: Bearer bnto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Keys are prefixed bnto_live_ followed by a random string.
  • Keys are shown once, at creation time — store yours securely.
  • A missing, malformed, revoked, or unknown key returns 401.
  • Treat the key as a secret: never embed it in client-side/browser code or commit it to source control. Call the API from your backend.
Don't have a key yet? Create one in the API keys tab of your dashboard, or contact us if you need a hand.
03

Credits & quota

Every accepted try-on costs one credit, charged when you submit — not when the job finishes. A job that ends failed is refunded automatically, so you are only ever charged for a generation you actually got. Two counters pay for it, in this order:

  1. Plan quota — the try-ons included in your monthly plan. Resets each billing cycle; unused quota does not roll over, and it stops being spendable the moment the cycle ends.
  2. Purchased credits — top-ups you bought separately. These never expire and survive a lapsed subscription. They cover overage once quota runs out.
They are two separate numbers, and the usage tab shows them separately. Don't add them together in your own UI: they are spent under different rules, so a merged figure will overstate what you can actually use once a cycle lapses.

Credits belong to your account, not to a key. All your API keys draw from the same pool — three keys running two try-ons each spends six credits in total. You can optionally give an individual key its own credit limit, which caps how much of the shared pool that key may spend. That is a ceiling, not a separate allowance: it never adds spending power.

If neither counter can cover a call you get 402 insufficient_credits, and nothing is queued. If the key is capped out while the account still has credits you get 402 key_credit_limit_reached instead — same status, but the fix is different, so check the code.

04

Submitting a job

Send a JSON body to the endpoint. The response is immediate and does not contain the image.

POSThttps://api.benitoai.com/v1/tryon

Body schema:

json
{
  "model_name": "tryon-v1.6",
  "inputs": {
    "model_image": "https://example.com/person.jpg",
    "garment_image": "https://example.com/garment.jpg"
  },
  "callback_url": "https://your-server.example/webhooks/tryon"
}
FieldTypeRequiredDescription
model_namestringNoLogical model identifier. Defaults to tryon-v1.6. Echoed back in the response.
inputs.model_imageURLYesPublic URL of the person/model photo. This image defines the output's identity, pose, background, and aspect ratio.
inputs.garment_imageURLConditionalExactly one of garment_image / garment_id. Public URL of the garment photo. Used as a reference for fabric, cut, and colour only — its background is ignored.
inputs.garment_idstringConditionalExactly one of garment_image / garment_id. Id of a garment preset you registered from the dashboard, e.g. grm_V1StGXR8Z5jdHi6BmyT. See Garment presets below.
callback_urlURLNoIf set, we POST the finished job's envelope here once it completes or fails. See Callbacks. Polling stays available either way.

A successful submission returns 202 with a job id — see Submit response below, then Polling for the result.

Order matters conceptually: model_image is the person; garment_image is the clothing. The person's face, body, pose, and background are preserved; the garment is transferred onto them.
05

Garment presets

Most integrations send the same garment images over and over while the person photo changes on every call. For those, register each garment once in the dashboard instead of re-sending its URL: we clean the photo up ahead of time — the garment extracted front-facing onto a solid background — store it, and give you back a stable garment_id.

  • Better results — the model conditions on a clean garment reference rather than a photo of someone else wearing it on a busy background.
  • Faster start — we skip fetching and normalizing the garment image on every request, because it is already stored and ready.

The same request, using a preset instead of a URL:

json
{
  "model_name": "tryon-v1.6",
  "inputs": {
    "model_image": "https://example.com/person.jpg",
    "garment_id": "grm_V1StGXR8Z5jdHi6BmyT"
  }
}

Presets are managed from the garments tab of your dashboard, not through this API. A garment_id that doesn't exist, belongs to another account, or has been deleted returns 404 garment_not_found; one that is still being prepared returns 409 garment_not_ready — retry the identical request once it's ready.

06

File upload

Use POST /v1/tryon/upload instead of the JSON endpoint when you already have the raw image bytes — a file your own user just uploaded, say — and don't want to host them somewhere public first just to hand this API a URL. Same async job model: it also returns 202 with a job id.

POSThttps://api.benitoai.com/v1/tryon/upload

Headers:

http
POST /v1/tryon/upload
Content-Type: multipart/form-data
Authorization: Bearer <your-api-key>
FieldTypeRequiredDescription
model_nametextNoSame as the JSON endpoint. Defaults to tryon-v1.6.
model_imagefileConditionalExactly one of model_image / model_image_url. Person/model photo, uploaded directly.
model_image_urltextConditionalExactly one of model_image / model_image_url. URL of the person/model photo.
garment_imagefileConditionalExactly one of garment_image / garment_image_url / garment_id. Garment reference photo, uploaded directly.
garment_image_urltextConditionalExactly one of garment_image / garment_image_url / garment_id. URL of the garment reference photo.
garment_idtextConditionalExactly one of garment_image / garment_image_url / garment_id. Id of a garment preset registered from the dashboard.
callback_urltextNoSame as the JSON endpoint.

The model image and the garment input are resolved independently — you can upload one as a file and pass the other as a URL in the same request, for example uploading a fresh model photo while reusing a previously-hosted garment image or a garment_id. Providing more than one form of the same input, or none, returns 422 validation_error.

Runnable multipart examples are in Examples below.

07

Image requirements

Both the model image and the garment image must satisfy all of the following. Because URL-based inputs are validated after the job is accepted, a failure surfaces as status: "failed" on the job rather than as an HTTP error at submit time. A file uploaded to /v1/tryon/upload is the exception: it is already in hand and cheap to check, so it is rejected synchronously with 400 invalid_image_upload.

RequirementLimit / Rule
Schemehttp or https only
FormatPNG, JPEG, WebP, or GIF
Content-TypeThe server must receive an image/* content type
Max size10 MB per image
ReachabilityURL must return HTTP 200 within 10 seconds
RedirectsAt most 2 redirects are followed
HostMust resolve to a public IP address

The Reachability, Redirects, and Host rows apply only to the URL-based flow — URLs submitted directly, or via model_image_url/garment_image_url on the upload endpoint. A file uploaded through model_image/garment_image skips the network fetch entirely, but is still checked against Format, Content-Type, and Max size.

SSRF protection: URLs that resolve to private, loopback, link-local, reserved, multicast, or unspecified addresses are rejected — including cloud metadata endpoints (169.254.169.254), 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1, and fc00::/7. Every redirect hop is re-validated. Use public image hosting (your own CDN, object storage with public read, etc.). The same rule applies to your callback_url.

Output aspect ratio

The output's aspect ratio matches the model_image, snapped to the nearest supported ratio (e.g. 1:1, 16:9, 9:16, 4:3, 3:4, 4:5). For predictable framing, crop your person photo to the aspect ratio you want before submitting.

Tips for best results

  • Use a clear, well-lit, front-facing photo of the person.
  • Use a clean garment reference (flat-lay or on a model) where the item is fully visible. The garment's background is discarded, so it doesn't need to be plain.
  • If the garment image includes accessories (shoes, hats, belts, glasses, bags, jewelry), they may also be applied.
  • A full-body garment (dress/jumpsuit) replaces both top and bottom; a "top" is layered appropriately.
08

Submit response

Accepted → 202 Accepted

json
{
  "id": "tryon_V1StGXR8Z5jdHi6BmyT8x",
  "status": "queued",
  "model_name": "tryon-v1.6",
  "created_at": "2026-08-09T12:00:00Z",
  "updated_at": "2026-08-09T12:00:00Z",
  "output": null,
  "error": null
}
FieldDescription
idUnique job id (tryon_…). Use it to poll GET /v1/tryon/jobs/{id}. It is also the output filename once the job completes.
statusOne of queued, processing, completed, or failed.
model_nameThe model identifier you sent, or the default.
created_at / updated_atUTC timestamps (ISO 8601).
outputnull until the job is completed, then { "image_url": "…" }.
errornull unless status is failed, then { "code", "message" }.

202 means the job was accepted for processing, not that it will succeed — check status/error via polling or your callback for the actual outcome.

Every response includes an X-Request-ID header — include it when reporting issues. You can also set your own X-Request-ID on the request and it will be echoed back.
09

Polling for the result

GEThttps://api.benitoai.com/v1/tryon/jobs/{id}

This returns exactly the same envelope shape as the submit response, updated to reflect the job's current state.

A completed job:

json
{
  "id": "tryon_V1StGXR8Z5jdHi6BmyT8x",
  "status": "completed",
  "model_name": "tryon-v1.6",
  "created_at": "2026-08-09T12:00:00Z",
  "updated_at": "2026-08-09T12:00:15Z",
  "output": {
    "image_url": "https://pub-xxxx.r2.dev/outputs/tryon_V1StGXR8Z5jdHi6BmyT8x.png"
  },
  "error": null
}
StatusWhat it means
queuedAccepted, waiting for a worker. Poll again shortly.
processingGeneration is running. Poll again shortly.
completedDone — output.image_url is set.
failederror.code / error.message describe why. See the job-level table under Errors.
  • Polling every 1–3 seconds is reasonable. This endpoint is not subject to the submission rate limit.
  • A job id you don't own, or that doesn't exist, returns 404 job_not_found either way — we don't reveal whether a job id belongs to someone else.
10

Callbacks

Optional. If you set callback_url at submit time, we POST the same envelope shown above to that URL once the job reaches a terminal state (completed or failed).

http
POST <your callback_url>
Content-Type: application/json

{ "id": "tryon_...", "status": "completed", "output": {...}, "error": null, ... }
  • Unsigned. This is a convenience notification, not a source of truth. Anyone who learns your callback_url could POST a fake payload to it.
  • Retried with backoff on timeout, connection error, 5xx, or 429, up to a bounded number of attempts. A 4xx response from your endpoint (other than 429) is treated as permanent — we won't keep retrying a URL that's actively rejecting the payload.
  • Delivery is not guaranteed. If your endpoint is down for the entire retry window, the callback is simply dropped. Polling remains the authoritative fallback, which is why it is never disabled just because you supplied a callback_url.
  • Respond 2xx promptly. A slow endpoint risks its own delivery attempt being treated as a timeout and retried.
Because the callback is unsigned, always confirm the result by calling GET /v1/tryon/jobs/{id} — which is authenticated by your API key — rather than trusting the callback body alone.
11

Errors

All errors share one envelope:

json
{ "error": { "code": "invalid_api_key", "message": "Invalid API key." } }

Validation errors (422) on POST /v1/tryon additionally include a details array describing which fields failed. On POST /v1/tryon/upload, the file-vs-URL exactly-one-of check also reports validation_error but without a details array — the message describes which field was missing or duplicated.

HTTP-level errors

Returned synchronously, before a job is even created.

HTTPcodeMeaning / cause
401missing_api_keyNo or malformed Authorization header.
401invalid_api_keyUnknown or revoked key.
402insufficient_creditsYour account has no quota and no credits left. Top up or renew from the dashboard.
402key_credit_limit_reachedThis key has hit its own credit limit even though the account still has credits. Raise the limit on the key, or call with a different one.
404job_not_foundUnknown job id, or one belonging to another account.
404garment_not_foundUnknown garment_id, or one belonging to another account, or since deleted.
409garment_not_readyThe garment preset exists but is still being prepared, or its last preparation failed. Retry the identical request once it's ready.
422validation_errorRequest body failed schema validation, or — on the upload endpoint — both or neither of an image file and its URL counterpart were provided.
422invalid_callback_urlcallback_url isn't a usable public http/https URL.
400invalid_image_uploadAn uploaded file (upload endpoint only) was invalid — wrong type, too large, corrupt, or unparseable. Checked synchronously since the bytes are already in hand.
429rate_limitedPer-key rate limit exceeded on submission. Includes a Retry-After header.
503queue_unavailableTransient — the job queue was unreachable. Includes a Retry-After header; safe to retry the submission.
500internal_errorUnexpected server error.

Job-level errors

These are not HTTP statuses. They arrive as status: "failed" on the job, via polling or your callback, with the code in error.code.

error.codeMeaning / cause
image_fetch_failedAn input image URL couldn't be fetched or validated — bad URL, too big, wrong type, blocked host, timeout, or non-200.
image_processing_failedAn image was fetched but couldn't be decoded or normalized.
generation_failedThe model could not produce an image.
content_blockedGeneration blocked by safety filters.
upstream_unavailableThe upstream generation provider stayed unreachable through the full retry window.
internal_errorUnexpected server error during processing.

Handling guidance

  • 429 — read the Retry-After response header (seconds) and back off before retrying submission.
  • 503 queue_unavailable — transient; retry the submission with backoff.
  • generation_failed / content_blocked (on the job) — usually input-related. Try a clearer person photo or a different garment image; retrying the identical request rarely helps for content_blocked.
  • image_fetch_failed (on the job) — verify the URL is public, returns an image, is under 10 MB, and resolves to a public host.
  • invalid_image_upload (upload endpoint only) — verify the uploaded file is a PNG/JPEG/WebP/GIF under 10 MB and not corrupted.
  • internal_error / upstream_unavailable — transient; safe to resubmit with backoff.
12

Rate limits

  • Applies to job submission only (POST /v1/tryon and POST /v1/tryon/upload) — polling GET /v1/tryon/jobs/{id} is not rate-limited.
  • Default: 60 requests per minute per API key (some keys may have a custom limit).
  • Enforced with a rolling 60-second window.
  • Exceeding it returns 429 with a Retry-After header telling you how many seconds to wait.
13

Examples

Every example below submits a job and then collects the result. Nothing holds a connection open waiting for generation.

Submit, then poll

The baseline integration: POST the job, then poll the job endpoint until it leaves queued/processing.

# 1. Submit
resp=$(curl -s -X POST https://api.benitoai.com/v1/tryon \
  -H "Authorization: Bearer bnto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
        "model_name": "tryon-v1.6",
        "inputs": {
          "model_image": "https://example.com/person.jpg",
          "garment_image": "https://example.com/garment.jpg"
        }
      }')
job_id=$(echo "$resp" | jq -r .id)

# 2. Poll until done
until [ "$(curl -s https://api.benitoai.com/v1/tryon/jobs/$job_id \
  -H "Authorization: Bearer bnto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | jq -r .status)" != "queued" ]; do
  sleep 2
done
curl -s https://api.benitoai.com/v1/tryon/jobs/$job_id \
  -H "Authorization: Bearer bnto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | jq

Receiving a callback instead of polling

Supply a callback_url at submit time and we'll POST the finished job to it. Confirm with a GET before trusting the payload — the callback is unsigned.

javascript
app.post("/webhooks/tryon", express.json(), (req, res) => {
  const job = req.body;
  // Always confirm via GET /v1/tryon/jobs/{id} before trusting this —
  // this endpoint is unsigned.
  if (job.status === "completed") {
    console.log("Result:", job.output.image_url);
  } else if (job.status === "failed") {
    console.error("Failed:", job.error);
  }
  res.sendStatus(200);
});

Uploading a file directly

Same auth rules; only the body shape changes. These upload the model photo and reuse a hosted garment image by URL, to show the mixed case — either input can be a file or a URL independently.

curl -X POST https://api.benitoai.com/v1/tryon/upload \
  -H "Authorization: Bearer bnto_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -F "model_name=tryon-v1.6" \
  -F "model_image=@person.jpg" \
  -F "garment_image_url=https://example.com/garment.jpg"
14

Other endpoints

EndpointPurpose
GET /healthLiveness check. Returns 200 with {"status":"ok","mongo":true,"rabbitmq":true} when healthy, 503 otherwise. No auth required.
GET /docsInteractive Swagger/OpenAPI UI for exploring and testing the API in the browser.
15

Quick checklist before you call

  • ☐ Bearer API key set in the Authorization header.
  • ☐ Image URLs are public http/https and return a PNG/JPEG/WebP/GIF — or you're uploading the files directly.
  • ☐ Each image is under 10 MB and loads within 10s.
  • ☐ Person photo cropped to your desired output aspect ratio.
  • ☐ Your integration polls GET /v1/tryon/jobs/{id} and/or handles a callback_url delivery, rather than expecting a blocking response.
  • ☐ Retry/backoff logic for 429 and 503 on submission, honoring Retry-After.

Ready to start?

Create an API key in your dashboard and make your first call.

Open the dashboard