Skip to navigation

Job outputs

Submit typed JSON under an Idempotency-Key, poll a job, decode a typed output.

Submitting

A submit names its input assets and its product parameters, and carries an Idempotency-Key.

curl -sS -X POST "$BASE_URL/v1/skin/analyses" \
-H "X-API-Key: $API_KEY" \
-H "Idempotency-Key: 5d0e1f0c-3b53-4d1f-9d0a-7f4f4c1d2e11" \
-H "Content-Type: application/json" \
-d '{
"inputs": [{"role": "source", "asset_id": "asset_6pa8ad0ds2t7g1z6pg4tcr4zqp"}],
"concerns": ["acne", "redness", "wrinkles"],
"visualizations": [{"concern": "acne", "kind": "mask"}]
}'

A makeup try-on has the same inputs and an ordered products list instead. The order is the layer order, and each category appears at most once:

{
"inputs": [{"role": "source", "asset_id": "asset_6pa8ad0ds2t7g1z6pg4tcr4zqp"}],
"products": [
{"category": "lip_liner", "color": "#7B2E42", "opacity": 0.8},
{"category": "lipstick", "color": "#C2185B"}
]
}

The answer is 202 with the job, Location: /v1/jobs/{job_id} and Retry-After.

The key

  • A missing key is 400 IDEMPOTENCY_KEY_REQUIRED; a malformed one is 400 INVALID_IDEMPOTENCY_KEY.
  • The same key with the same body answers the same job, so a retry after a timeout is safe.
  • The same key with a different body is 409 IDEMPOTENCY_KEY_REUSED. A retry that arrives while the first use is still being admitted is 409 IDEMPOTENCY_IN_PROGRESS with a short Retry-After: send the same request again after it.

Generate one key per logical request and keep it with the request until you have a job id. Idempotency explains why, and applies unchanged.

Polling

GET /v1/jobs/{job_id} until status is succeeded or failed. Wait the Retry-After the previous answer gave you between reads. Send the previous ETag in If-None-Match and an unchanged job answers 304 with no body.

The job’s fields obey fixed invariants, and a client can rely on them:

statuscompleted_atoutputerror
queued, runningnullnullnull
succeededsetsetnull
failedsetnullset

A failed job still answers HTTP 200. Read status before output. error.code on a failed job is one of INPUT_UNPROCESSABLE, INPUT_EXPIRED or PROCESSING_FAILED. The first needs a different image, the second a fresh upload, and the third may succeed on a retry under a new Idempotency-Key.

Decoding the output

output.type names the schema. Three types exist today:

  • makeup.try_on carries revision and one image, a result asset reference.
  • skin.analysis carries analysis_version, one entry per requested concern, and the requested visualizations as result asset references.
  • hair.try_on carries one result image Asset plus applied.color_gradient with normalized dark and light colors and opacities. See the Hair color try-on guide for its inputs and supported options.
{
"type": "skin.analysis",
"analysis_version": "skin-2026-09-draft.1",
"concerns": [
{"concern": "acne", "status": "measured", "score": 8.7, "unavailable_reason": null},
{"concern": "wrinkles", "status": "unavailable", "score": null, "unavailable_reason": "not_measured"}
],
"visualizations": [
{
"id": "asset_cnep5j6sw0kphca1v6gv6qwayc",
"role": "mask.acne",
"media_type": "image/png",
"size_bytes": 55312,
"width": 1200,
"height": 1600,
"expires_at": "2026-09-25T10:00:05Z",
"href": "/v1/assets/asset_cnep5j6sw0kphca1v6gv6qwayc/content"
}
]
}

A concern score runs from 0 to 10 and higher means less visible: 8.7 for acne is a good reading, not a bad one. A concern the service could not measure says so with status: unavailable and a reason, and never with a score of 0.

Only what you asked for comes back. A skin analysis returns exactly the concerns and visualizations the request named.

The union is open

output is an open union. A later product adds a new type, and your code must keep working when one arrives: branch on the types you know and keep the rest whole rather than failing. inputs[].role is an open string for the same reason, and any response object may gain fields.

Result images

Each result reference’s href is /v1/assets/{id}/content for its own id, relative to the API’s base URL. Download it with your X-API-Key, as described in Assets. It expires with the job.

Usage

GET /v1/usage?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD reports distinct accepted jobs per UTC day and operation over [start_date, end_date). A submission that never reached acceptance is not counted. These are operational counts, not an invoice.