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.4.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, semantic

job = client.makeup.try_on(
    asset_id=source.id,
    applications=[semantic.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)

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. Digitisation is not enabled for partners yet: until a deployment turns it on, every /v1/digitisation route answers 404 and so does every command below. Each command is one client.digitisation call and prints the answer as JSON:

export MORPHIAONE_API_KEY=... MORPHIAONE_BASE_URL=https://api.morphiaone.com
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.5.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.5.0
File Size Uploaded Provenance Actions
morphiaone-0.5.0.tar.gz 62.0 kB Without provenance Details for morphiaone-0.5.0.tar.gz

Built distribution (wheel)

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

Total release size: 124.3 kB

Release files / morphiaone-0.5.0.tar.gz

Download URL morphiaone-0.5.0.tar.gz
Size 62.0 kB
Tags Source
SHA-256 checksum
How to use checksums
e7282050eeb406d2dd3e9029f6667192d283068703c16c9b8b195e5949968bf6
BLAKE2b-256 checksum
How to use checksums
3267716d9cafb62ba40cdcd5468a6256525dd6c39736c3e8125037b824c916a1
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.5.0-py3-none-any.whl

Download URL morphiaone-0.5.0-py3-none-any.whl
Size 62.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2266a668c7635a9af7459b4ebf56787bdb8d5a09faea45961b5f72ed3a39d7c5
BLAKE2b-256 checksum
How to use checksums
c1c47b4e8fb2df1ac69821be9cd50f1747c02ccf8c136b6121addb004dcca84c
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.5.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