Skip to main content

MorphiaOne Python SDK

morphiaone is the Python client for the MorphiaOne V1 API: Skin Analysis, Makeup Try-On and Hair Color Try-On, plus the Assets, Jobs and Usage calls they rest on.

Developer Preview. The V1 API contract is release candidate 1.0.0-rc.1, still a draft, and this SDK may change before 1.0, including in ways that break code written against it. Pin an exact version. 0.11.0 is the current published version.

Install

pip install morphiaone

Requires Python 3.11 or newer. One runtime dependency: httpx.

Authenticate

Keep your API key in the environment of a server process, and read it from there:

export MORPHIAONE_API_KEY="<your API key>"
import os

from morphiaone import MorphiaOne

client = MorphiaOne(
    api_key=os.environ["MORPHIAONE_API_KEY"],
    base_url="<your API base URL>",
)

Use the client as a context manager (with MorphiaOne(...) as client:) or call client.close() when you are done. AsyncMorphiaOne has the same namespaces, with every call a coroutine.

Server-side only. Your API key authorizes spending on your account. Never ship it in a browser bundle, a mobile app or a chatbot client; call MorphiaOne from your own backend and hand your front end only the result.

How a request works

Every product call follows the same three steps:

  1. Upload each input image once as an Asset.
  2. Submit a product request that references the Asset ids. The API answers with a Job; the work runs asynchronously.
  3. Wait for the Job, or poll it, and read its result Assets.
source = client.assets.upload("face.jpg")  # a str or Path, or raw bytes
job = client.skin.analyze(asset_id=source.id, concerns=["acne", "redness"])
done = client.jobs.wait(job.id)  # raises ProcessingError if it failed

assets.upload(image, *, media_type=None, timeout=None) infers the media type (image/jpeg, image/png or image/webp) from the file suffix or the leading bytes when omitted.

Every submit also accepts the keyword-only idempotency_key, timeout and wait. With wait=True the call polls to the succeeded job before returning, exactly as jobs.wait would:

done = client.skin.analyze(asset_id=source.id, concerns=["acne"], wait=True)

Skin Analysis

from morphiaone import SkinAnalysisOutput

job = client.skin.analyze(
    asset_id=source.id,
    concerns=["acne", "redness", "wrinkles"],
    visualizations=[{"concern": "acne", "kind": "mask"}],
)
done = client.jobs.wait(job.id)
if isinstance(done.output, SkinAnalysisOutput):
    for item in done.output.concerns:
        print(item.concern, item.status, item.score)

Skin Analysis is a cosmetic reading, not a medical one.

Makeup Try-On

applications is kept in the order given:

from morphiaone import MakeupTryOnOutput, custom

job = client.makeup.try_on(
    asset_id=source.id,
    applications=[custom.lipstick("#C2185B")],
)
done = client.jobs.wait(job.id)
if isinstance(done.output, MakeupTryOnOutput):
    image = client.assets.get_content(done.output.image)
    with open("makeup.png", "wb") as out:
        out.write(image.content)

A shade you published in your catalogue is a variant application, named by its id. The job's resolution names the variant revision it rendered:

from morphiaone import variant

job = client.makeup.try_on(source.id, [variant("var_...")])
print(job.resolution.applications[0].variant.revision)

A curated look you published in your catalogue is sent by its id in place of applications. The job's resolution.look names the revision it rendered:

job = client.makeup.try_on(source.id, look_id="look_...")
print(job.resolution.look.revision)

Catalogue authoring

Publishing your own shades and digitising them from product photos needs a key that carries the catalogue.* and digitisation.* scopes. Choose Full access (or Catalogue / Digitisation) when you create the key in the Platform. A deployment that has digitisation switched off answers 404 on every /v1/digitisation route.

A variant is a sequence of immutable revisions. A revision renders only once it is approved and published:

from morphiaone import variant

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)

job = client.makeup.try_on(source.id, [variant(v.id)])

Digitisation measures a colour from a retained product photo and stages the reviewed result as a variant revision. An assessment is processed in the background and usually settles within a minute or two, so give wait_assessment a generous timeout:

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
)

Hair Color Try-On

Hair Color Try-On takes one source image: the server finds the hair and recolors only that, never style or length. If you segment hair yourself, upload a grayscale hair mask with exactly the same dimensions and pass hair_mask_asset_id. The color is a two-stop gradient, dark and light:

from morphiaone import HairTryOnOutput

# The server finds the hair; pass hair_mask_asset_id= only to use your own mask.
job = client.hair.try_on(
    source_asset_id=source.id,
    color_gradient={
        "dark": {"color": "#3B1F16", "opacity": 0.9},
        "light": {"color": "#C87943", "opacity": 0.8},
    },
)
done = client.jobs.wait(job.id)
if isinstance(done.output, HairTryOnOutput):
    image = client.assets.get_content(done.output.image)

Jobs

jobs.wait polls for you and honours the server's Retry-After. It is a convenience over the job API, not a replacement for it:

  • Persist job.id before you wait. It is how you resume after a restart, from another process or from a queue consumer: client.jobs.get(job_id) or client.jobs.wait(job_id).
  • A wait timeout stops local waiting only. It does not cancel the job on the server.
  • done.output is SkinAnalysisOutput, MakeupTryOnOutput, HairTryOnOutput, or UnknownJobOutput for an output type this SDK version does not know yet. An unknown type never raises, so a newer API does not break an older SDK.

Result Assets

A result names Assets, not bytes. Read them with client.assets.get_content(...), which authenticates with the client's own key. The read is built from the Asset id, never from a URL in the response, so a response can never redirect your credential to another host. Redirects are never followed.

Usage

report = client.usage.get(start_date="2026-09-01", end_date="2026-10-01")

The window is half-open, in UTC. The report is counts, not an invoice.

Command line

The distribution installs a morphiaone command for the colour digitisation flow. Each command is one client.digitisation call and prints the answer as JSON:

export MORPHIAONE_API_KEY=... MORPHIAONE_BASE_URL=https://api.morphiaone.ai
morphiaone digitisation retain --file swatch.jpg
morphiaone digitisation assess --image evidence_... --region 10,10,90,90 --wait
morphiaone digitisation make-candidate assessment_... --image 0 --region 0
morphiaone digitisation accept candidate_... --sha256 <the digest you reviewed>

The key is read only from MORPHIAONE_API_KEY, never from a flag. The exit status is 0 when the command is done, 1 when the request failed (the error code and request id go to stderr), 2 for a usage error, 3 when --wait reached its deadline, and 4 when the assessment settled without succeeding. morphiaone --help lists the commands.

Errors

Every error the SDK raises derives from MorphiaOneError, and the tree is the TypeScript SDK's, class for class. An HTTP error response is an ApiError, carrying the HTTP status, the public error code, the human sentence as message, details, the request_id and, when the server sent one, retry_after (seconds). On a local fault (transport, protocol, deadline) those fields are None. Branch on code, never on the message.

Class Derives from When
MorphiaOneError Exception every SDK error
ApiError MorphiaOneError any HTTP error response; the class for a non-5xx status with no narrower one
AuthenticationError ApiError 401: the key is missing, malformed or not accepted
PermissionDeniedError ApiError 403: the key is valid but refused; code says why. Not an AuthenticationError: reloading the key does not help
InsufficientScopeError PermissionDeniedError 403 INSUFFICIENT_SCOPE: the key's grant is too narrow
ValidationError ApiError 400, 415 or 422: the request is invalid
PayloadTooLargeError ValidationError 413: the image is too large; downscale, do not retry
UnsupportedFeatureError ValidationError 501: this deployment does not perform the requested feature
InsufficientCreditsError ApiError 402: no credit left; never retried
NotFoundError ApiError 404, or 410 for an expired input Asset: upload again
ConflictError ApiError 409: such as an idempotency key reused with a different body
RateLimitError ApiError 429: rate or quota limits, after the SDK's own retries
ServerError ApiError any other 5xx, after the SDK's own retries
ServiceUnavailableError ServerError 503: the service could not take the request, after retries
TransportError MorphiaOneError no response at all, after retries
ProtocolError MorphiaOneError a response the contract does not allow
DeadlineExceeded MorphiaOneError the call's own timeout elapsed
ProcessingError MorphiaOneError the job ran and failed; job_id, code, details and error are on it

An upload over the client's max_image_bytes, an empty image or a malformed argument is a ValueError raised before any request is sent (TypeScript throws RangeError for the oversize upload and TypeError for the rest).

from morphiaone import MorphiaOneError, RateLimitError

try:
    done = client.jobs.wait(job.id)
except RateLimitError as error:
    schedule_retry(after=error.retry_after)
except MorphiaOneError as error:
    log.warning("MorphiaOne request failed: %s %s", error.code, error.request_id)
    raise

Log request_id on every failure. It is the one value support needs to find a request. An error code this SDK version does not know still raises the class for its HTTP status; it never crashes decoding.

Timeouts and retries

  • Timeouts are in seconds. request_timeout (default 10) bounds one call, its retries included. operation_timeout (default 120, at least request_timeout) bounds jobs.wait and a submit with wait=True, submit and polling together. Both are client options, and every call accepts its own timeout, which replaces the relevant default for that call: on a submit with wait=True it bounds the whole call.
  • Polling never runs forever: the interval is the server's Retry-After, else about one second, never less than 0.25 s and at most 60 s, plus a little positive jitter.
  • Transport failures, 408, 429, 500, 502, 503 and 504 are retried (4 attempts in all by default). A server Retry-After is waited as named, never shortened; when it is over 60 s, or longer than the call's remaining deadline, the error is raised (with retry_after set) instead of retrying early. Without one, the SDK backs off exponentially, each delay capped at RetryPolicy.max_delay (5 s). A submit's retries resend the same Idempotency-Key, and a 409 IDEMPOTENCY_IN_PROGRESS is retried too. A quota or credit 429 is not retried, because waiting does not clear it. The TypeScript SDK retries exactly the same set.

Idempotency

Every submit carries an Idempotency-Key. When you do not pass one, the SDK generates one per call and reuses it on every retry of that call, so a retry never runs the work twice.

That protects one call, not your operation. To make a logical request retry-safe across process restarts, pass a key derived from your own order or request id:

job = client.skin.analyze(
    asset_id=source.id,
    concerns=["acne"],
    idempotency_key=f"order-{order_id}-skin",
)

Reusing a key with a different request is a ConflictError. Asset uploads carry no key: a retried upload can leave an unused Asset behind, which expires on its own.

Privacy

The SDK sends no telemetry of its own. It never logs your API key, the images you upload or their content.

Versioning

/v1 is the HTTP API major. The SDK has its own SemVer: while it is 0.x, a minor release may break. The SDK will reach 1.0 only once the V1 contract is released.

Support and security

Full documentation is coming with the public release. For support, include the request_id from the error. Report security issues via your MorphiaOne account contact; do not put API keys or images in a report.

License

MIT.

Metadata

Release files for morphiaone 0.11.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for morphiaone 0.11.0
File Size Uploaded Provenance Actions
morphiaone-0.11.0.tar.gz 76.0 kB Without provenance Details for morphiaone-0.11.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for morphiaone 0.11.0
File Interpreter ABI Platform Actions
morphiaone-0.11.0-py3-none-any.whl Python 3 none any Details for morphiaone-0.11.0-py3-none-any.whl

Total release size: 152.6 kB

Release files / morphiaone-0.11.0.tar.gz

Download URL morphiaone-0.11.0.tar.gz
Size 76.0 kB
Tags Source
SHA-256 checksum
How to use checksums
68fb89c9b535c28b096c56454100e92b544830ff91687b616fa8c7e18e231085
BLAKE2b-256 checksum
How to use checksums
75b82a09a232c39ebc734ecf505a969f7f81d3ba28f47015f4d90bc1c3c7ffaa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / morphiaone-0.11.0-py3-none-any.whl

Download URL morphiaone-0.11.0-py3-none-any.whl
Size 76.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4974d6f18a116ae2d62f9d3caf621211d20c6100fa3e5811459c98fcd2396cfc
BLAKE2b-256 checksum
How to use checksums
23f5847fede266bd53567e6a20877e6d665f6310e734510c7513bfd74e7de476
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Security for morphiaone 0.11.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page