> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.morphiaone.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.morphiaone.ai/_mcp/server.

# Going to production

A quickstart integration works. Everything below is what it does not yet handle, in
rough order of how expensive it is to get wrong.

## Correctness

#### Check job.status before reading job.output

A failed analysis arrives as HTTP `200` with `"status": "failed"`. Nothing in your
HTTP client will raise. Code that assumes `2xx` means "results are here" carries a
failed job forward silently. [Asynchronous jobs](/concepts/jobs)

#### Send an Idempotency-Key on every submission, derived from your own record

Derived from the durable business operation (`order-48213-skin`), not from the
attempt. A fresh key per retry is no idempotency at all, and a submission timeout
without one is a double charge. [Idempotency](/concepts/idempotency)

#### Branch on error.code, never on error.message

Messages are human text and change without notice. Codes are the contract. Treat an
unrecognised code as a failure of its status class rather than crashing.
[Errors](/concepts/errors)

#### Persist the job id before you start waiting

It is the only handle to work you have already paid for, and a client-side timeout
does not cancel the job. Store it next to the order or session that caused it.

#### Get the score direction right

Skin scores are 0–10 and **higher means the concern is less visible**. Reversing this in a customer-facing
UI is a bug that passes review because both directions look plausible.
[Skin analysis](/products/skin-analysis)

## Resilience

#### Honour Retry-After

On a submission, a poll, a `429` and a `503`, it says when to come back. Ignoring them turns brief saturation into
sustained saturation, and earns `429 RATE_LIMITED`.

#### Back off with full jitter, and retry only what is retryable

`408/429/5xx/transport/409 IDEMPOTENCY_IN_PROGRESS`. Nothing else. Retrying a
`400` or a `409 IDEMPOTENCY_KEY_REUSED` repeats a request that is wrong, at cost.
[Retries and backoff](/concepts/retries)

#### Set two deadlines: per-request and per-operation

A per-request timeout alone gives an unbounded loop of quick calls. An operation
deadline alone lets one hung socket eat the budget.

#### Do not treat 503 on a poll as expiry

`503 SERVICE_UNAVAILABLE` means ask again. Only `404 JOB_NOT_FOUND` on a poll, or
`404 ASSET_NOT_FOUND` on a download, past the `expires_at` means the work is gone.
Confusing them discards completed work.

#### Handle 402 as an operational alert, not a retry

`402 INSUFFICIENT_CREDITS` will not clear on its own. Page someone who can top up.
[Credits and usage](/concepts/credits)

#### Send If-None-Match on polls

A `304` is much cheaper for both sides and keeps you under rate limits in a busy
period.

#### Pace submissions by the rate-limit headers where you get them

Where your account has rate-limit headers on, `RateLimit` says how much of each policy
is left and when it refills. Slow down before the `429`, not after it. Without the
headers, `429` and `Retry-After` are the signal. [Rate limits](/concepts/rate-limits)

#### If you use webhooks, verify, dedupe, and keep polling as the fallback

Verify the signature against the raw body, dedupe on `event.id`, answer `2xx` within 10
seconds and do the work afterwards. Delivery is at least once and unordered, so keep a
slow poll for any job whose event you have not seen. [Webhooks](/concepts/webhooks)

## Security

#### The key is only ever server-side

Read it from a secret manager at startup. Not in a browser bundle, a mobile binary,
a client config, an image layer, or a committed file.
[Authentication](/getting-started/authentication)

#### If you have a front end, you have a BFF

And your BFF authorizes your own users, because this API cannot: within your account
there is no isolation between your customers.
[Serving a browser or mobile client](/guides/bff-pattern)

#### Plan a rotation you can execute

Two keys valid at once during the swap, so you are never down to zero. Rotate on
suspicion and on staff changes, and revoke the old key in the Platform once nothing uses
it. A revoked or expired key answers `401 INVALID_API_KEY` at once.

#### Narrow each key

Give a key only the product scopes it calls, and, where the Platform offers it, an
expiry and a network allowlist. A call from outside the allowlist is
`403 API_KEY_IP_NOT_ALLOWED`. [Authentication](/getting-started/authentication)

#### Delete what you no longer need

Assets and jobs expire on their own. If you have finished with one sooner, or a person
asks you to erase their data, delete the job (it takes its result images with it) and
any input Asset. [Jobs](/concepts/jobs), [Assets](/concepts/assets)

#### Never log the key, the bytes, or landmarks — not even partially

A key prefix plus a length is a head start. Log `request_id` and the job id; they are
enough for support and are not personal data.
[Privacy and retention](/concepts/privacy-retention)

#### Store only the fields you use

A skin result carries no demographic inference, but it is still derived from an
identifiable face. Keep the concerns you display and the `analysis_version`, not the
whole output and every result image. [Skin analysis](/products/skin-analysis)

## Operations

#### Record request\_id on every failure

It is on every error body and in the `X-Request-Id` header. Without it a support
conversation starts with an archaeology exercise.

#### Alert on rates, not on individual errors

A single `503` is weather. A sustained `503` rate, a rising `429` rate, or a `402`
at all are the three worth waking someone for.

#### Watch GET /v1/usage for shape changes, not for billing

A tripled `jobs_accepted` count or a growing `jobs_failed` share is usually your own
integration changing behaviour. Read only `final` days as settled. It is not invoice authority.

#### Watch the credit balance

Where your account has the balance endpoint, read `GET /v1/credits/balance` on a
schedule and alert on `low_balance` before submissions start answering `402`.
[Usage and credits](/concepts/credits)

#### Keep live and test apart

Use `pk_test_` keys in development and CI and `pk_live_` keys only in production. The
two environments share nothing: an id from one is a `404` in the other.
[Test mode](/getting-started/test-mode)

## Before you promise anything to your own customers

* **Makeup pixel parity is not proven for any category.** Do not claim your
  rendering matches an existing browser try-on. [Makeup try-on](/products/makeup)
* **Makeup is three categories:** `foundation`, `lipstick` and `lip_liner`. No
  textures, no other products.
* **There is no URL submission.** Upload the bytes to `POST /v1/assets`, from your
  server.
* **Cancel does not stop processing that has started.** A job already being processed
  finishes and bills. Listing, cancel, delete, webhooks and the credit balance are
  enabled per account; confirm which ones yours has before you depend on them.
* **No SLA, uptime or latency figure is published here.** If you need one, it comes
  from your commercial agreement, not from this site.