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

# Your shade catalogue

Your catalogue holds **variants**: your own shades, each a sequence of immutable
revisions. A revision renders in a try-on only after you approve and publish it.
**Digitisation** measures a colour from a product photo you keep on file, lets you
review the result, and stages it into the catalogue as a draft revision.

## The key you need

Create the key yourself in the Platform: open **API keys**, choose **Create API key**,
then pick the access it needs. No one at MorphiaOne has to provision it.

| Access           | Scopes on the key                                                                                     | Use it for                                  |
| ---------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| **Try-on**       | the product scopes your account offers, such as `makeup.apply`                                        | rendering looks on photos                   |
| **Catalogue**    | `catalogue.read`, `catalogue.write`, `catalogue.approve`, `catalogue.publish`, `catalogue.preview`    | creating, approving and publishing variants |
| **Digitisation** | `digitisation.read`, `digitisation.write`, `digitisation.review`, `catalogue.read`, `catalogue.write` | photos to staged draft revisions            |
| **Full access**  | all of the above                                                                                      | one back-office key for the whole journey   |

Approving and publishing a staged revision takes the **Catalogue** scopes, so a key that
runs the whole flow below, from photo to try-on, is a **Full access** key. A key without
the scope an operation needs is refused `403 INSUFFICIENT_SCOPE`, and you can create a
key with more access at any time. Keep every catalogue key on your server, like any
other key.

## Publish a shade

#### Python

```python
import os

from morphiaone import MorphiaOne, variant

client = MorphiaOne(
    api_key=os.environ["MORPHIAONE_API_KEY"],
    base_url="https://api.morphiaone.ai",
)

v = client.catalogue.create_variant(
    display_color="#AD1457",
    applications=[
        {"application_id": "lips", "category": "lipstick", "finish": "gloss"}
    ],
    external_ref="sku-48213",
)
client.catalogue.approve_revision(client.catalogue.get_variant_revision(v.id, 1))
client.catalogue.publish_variant(v, revision=1)

source = client.assets.upload("face.jpg")
job = client.makeup.try_on(source.id, [variant(v.id)])
done = client.jobs.wait(job.id)
```

#### TypeScript

```ts
import { readFile } from "node:fs/promises";
import { MorphiaOne, variantProduct } from "@morphiaone/sdk";

const client = new MorphiaOne({
  apiKey: process.env.MORPHIAONE_API_KEY!,
  baseUrl: "https://api.morphiaone.ai",
});

const v = await client.catalogue.createVariant({
  displayColor: "#AD1457",
  applications: [{ application_id: "lips", category: "lipstick", finish: "gloss" }],
  externalRef: "sku-48213",
});
await client.catalogue.approveRevision(await client.catalogue.getVariantRevision(v.id, 1));
await client.catalogue.publishVariant(v, { revision: 1 });

const source = await client.assets.upload(await readFile("face.jpg"), {
  mediaType: "image/jpeg",
});
const job = await client.makeup.tryOn({
  assetId: source.id,
  applications: [variantProduct({ variantId: v.id })],
});
const done = await client.jobs.wait(job.id);
```

Approving takes the revision you read, not just its number, so the revision you
reviewed is the one that gets approved. If the revision changed in the meantime, the
approve call is refused.

## Digitise a shade from a photo

An assessment runs in the background. It usually settles within a minute or two, so
give the wait a generous timeout. The SDK polls for you, and a timed-out wait can be
resumed later from the assessment id.

#### Python

```python
evidence = client.digitisation.retain_evidence(client.assets.upload("swatch.png").id)
queued = client.digitisation.create_assessment(
    [{"evidence_id": evidence.id, "regions": [[40, 60, 180, 240]]}]
)
assessment = client.digitisation.wait_assessment(queued.id, timeout=180)

candidate = client.digitisation.create_candidate(assessment, image=0, region=0)
# Accept the object, not its id, so the digest you reviewed is sent with it.
accepted = client.digitisation.accept_candidate(candidate)
staged = client.digitisation.stage_candidate(
    accepted,
    applications=[
        {"application_id": "lips", "category": "lipstick", "finish": "gloss"}
    ],
    external_ref="sku-48213-digitised",
)
client.catalogue.approve_revision(
    client.catalogue.get_variant_revision(staged.variant_id, 1)
)
client.catalogue.publish_variant(
    client.catalogue.get_variant(staged.variant_id), revision=1
)
```

#### TypeScript

```ts
import { readFile } from "node:fs/promises";

const dg = client.digitisation;
const photo = await client.assets.upload(await readFile("swatch.png"), {
  mediaType: "image/png",
});
const evidence = await dg.retainEvidence(photo.id);
const queued = await dg.createAssessment({
  images: [{ evidence_id: evidence.id, regions: [[40, 60, 180, 240]] }],
});
const assessment = await dg.waitAssessment(queued.id, { timeout: 180 });

const candidate = await dg.createCandidate(assessment, { image: 0, region: 0 });
const accepted = await dg.acceptCandidate(candidate); // the object, so the reviewed digest is sent
const staged = await dg.stageCandidate(accepted, {
  applications: [{ application_id: "lips", category: "lipstick", finish: "gloss" }],
  externalRef: "sku-48213-digitised",
});
await client.catalogue.approveRevision(
  await client.catalogue.getVariantRevision(staged.variantId, 1),
);
await client.catalogue.publishVariant(await client.catalogue.getVariant(staged.variantId), {
  revision: 1,
});
```

A staged revision is a draft. It renders only after the approve and publish calls at
the end of the example.

### Choosing regions

The example's `[40, 60, 180, 240]` fits its own swatch photo. Choose regions for
yours:

* A region is `[x0, y0, x1, y1]` in pixels of the uploaded image: origin top-left,
  `x1` and `y1` exclusive. It must lie inside the image and be at least 16 pixels on
  each side. An image takes 1 to 4 regions, and each one becomes its own candidate
  (`region=0`, `region=1`, ...).
* The measured colour is the average of every pixel in the box. Keep the box inside
  the product colour: background, packaging, shadows or a glare highlight in the box
  shift the shade. A region's report carries `clipped_fraction`, the share of its
  pixels with a channel at 0 or 255; a high value usually means glare or deep shadow.
* Upload the image upright. Phone photos often carry an EXIF orientation tag instead
  of rotated pixels; the assessment refuses those regions with `orientation_tagged`
  rather than guess which way is up. Rotate the pixels and strip the tag before you
  upload, for example with Pillow's `ImageOps.exif_transpose`.
* A region outside the image or smaller than 16 pixels refuses the whole request, and
  nothing is queued. A region the colour contract cannot measure (for example
  `transparent_pixels`) comes back with `rejected` in the assessment instead of a
  report, and cannot become a candidate.

## When something is refused

| Answer                                                     | Meaning                                                                                |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `403 INSUFFICIENT_SCOPE`                                   | The key lacks this operation's scope. Create a key with the access in the table above. |
| `404` on every `/v1/catalogue` or `/v1/digitisation` route | That feature is not switched on for this environment.                                  |
| `401 INVALID_API_KEY`                                      | The key is wrong, revoked or expired. Send it in the `X-API-Key` header.               |

## Replace or revoke a key

In **API keys**, **Rotate** issues a replacement with the same access and keeps the old
key working for a grace period you choose (none, 1 hour, 24 hours or 7 days), so you can
deploy the new secret first. **Revoke** stops a key immediately. A revoked key answers
`401` on its next call.