Skip to navigation

Authentication

One key, one header, server-side only.

Every published operation takes the same credential:

X-API-Key: <your key>

There is one scheme and no alternative, which means there is no decision for you to make and one code path for each of us to get right. The contract names it ApiKeyAuth; the API reference on this site shows it on every operation.

The one rule that matters more than the rest

The key is a server-side secret. It authenticates your account, it spends your credits, and anything that holds it can do everything you can do.

So it must never reach a browser bundle, a mobile binary, a mobile app’s configuration, a public repository, a client-side environment variable, or a URL query string. If you are building a front end, the supported shape is your own backend holding the key and exposing a narrow endpoint of its own: Serving a browser or mobile client.

We do not publish a browser snippet for this API. Plenty of API quickstarts do, because it demos beautifully, and the result is keys in published bundles.

Live and test keys

A key belongs to one environment of your account. A key that starts with pk_live_ runs real analyses and spends credits. A key that starts with pk_test_ runs the same lifecycle with fixture results and charges nothing; it can never read live jobs or assets, and a live key can never read test ones. See Test mode. Create and revoke keys on the API keys page of the Platform; see Get access.

Scopes

A key carries scopes. Each product submission requires that product’s scope, and cancelling or deleting a job requires the scope of the job’s product. This table is generated from the API contract:

OperationRequires
GET /v1/assetsany valid key for the environment
POST /v1/assetsany valid key for the environment
DELETE /v1/assets/{asset_id}any valid key for the environment
GET /v1/assets/{asset_id}any valid key for the environment
GET /v1/assets/{asset_id}/contentany valid key for the environment
GET /v1/credits/balanceany valid key for the environment
POST /v1/hair/try-onshair.apply
GET /v1/jobsany valid key for the environment
DELETE /v1/jobs/{job_id}the scope of the job’s product
GET /v1/jobs/{job_id}any valid key for the environment
POST /v1/jobs/{job_id}/cancelthe scope of the job’s product
POST /v1/makeup/try-onsmakeup.apply
POST /v1/skin/analysesskin.analyze
GET /v1/usageany valid key for the environment

A key’s available scopes depend on its environment and the grants selected when it is created. INSUFFICIENT_SCOPE means the key is valid but does not include the grant this product operation requires.

Uploads, polling and result reads are authenticated but not scope-gated. A key that could submit work but not read the result it created would be useless, and a scope every key must hold is not a permission — it is noise.

What a rejection tells you

StatusCodeWhat it meansRetry?
401INVALID_API_KEYno usable key: missing, unrecognised, revoked, expired, or its account disabledno — fix the credential
403INSUFFICIENT_SCOPEvalid key, not allowed this operationno — reconfigure
403API_KEY_IP_NOT_ALLOWEDvalid key, called from an address outside its network allowlistno — call from an allowed address or change the allowlist
503SERVICE_UNAVAILABLEwe could not check the keyyes, after Retry-After

The first row is deliberately one code: a credential that cannot be used answers 401 INVALID_API_KEY whatever the reason, and only a usable key without the operation’s scope, or called from outside its allowlist, is a 403. Branch on the status, and check the key you configured.

The last row is the one to handle deliberately. It is a 503 and not a 401 because your credential was never judged — nothing is wrong with it. A client that treated it as an authentication failure would discard a working key during an outage of ours, which is precisely the wrong reaction.

Full list, generated from the code that raises them: Error codes.

Isolation

Assets and jobs belong to the environment whose key created them. A valid key from another account, or from your own other environment, polling one of your job ids gets 404 JOB_NOT_FOUND, and reading one of your asset ids gets 404 ASSET_NOT_FOUND — the same answers as an id that never existed.

That is not vagueness; 403 there would confirm the resource exists and turn its id into a lookup oracle.

Rotation and hygiene

  • Read the key from your secret manager or environment at process start; do not commit it, and do not bake it into an image.
  • Rotate on any suspicion of exposure, and on staff changes. Plan for two keys to be valid at once during a rotation so you are never down to zero.
  • Log the request_id from a failed call. Never log the key, and never log it partially — a prefix plus a length is a meaningful head start for an attacker.