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 still a draft, and this SDK may change before 1.0, including in ways that break code written against it. Pin an exact version. This package is not published yet: until the first release, the install command below resolves nothing.

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

products is ordered, and the order is the layer order:

from morphiaone import MakeupTryOnOutput

job = client.makeup.try_on(
    asset_id=source.id,
    products=[{"category": "lipstick", "color": "#C2185B", "opacity": 0.8}],
)
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)

Hair Color Try-On

Hair Color Try-On takes two Assets: the source image and a grayscale hair mask with exactly the same dimensions. You produce the mask; the API does not segment hair, and it changes color only, never style or length. The color is a two-stop gradient, dark and light:

from morphiaone import HairTryOnOutput

hair_mask = client.assets.upload("hair-mask.png")
job = client.hair.try_on(
    source_asset_id=source.id,
    hair_mask_asset_id=hair_mask.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.

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.1.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.1.0
File Size Uploaded Provenance Actions
morphiaone-0.1.0.tar.gz 27.9 kB Without provenance Details for morphiaone-0.1.0.tar.gz

Built distribution (wheel)

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

Total release size: 53.9 kB

Release files / morphiaone-0.1.0.tar.gz

Download URL morphiaone-0.1.0.tar.gz
Size 27.9 kB
Tags Source
SHA-256 checksum
How to use checksums
b766e02f5341835849df42c85e985fa63aa3dec724eb2988ec903860c580c6aa
BLAKE2b-256 checksum
How to use checksums
c8a4428a041e3a39dc9493a609eaec3fe526d012c793a50e3350af8e4bee0cce
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.1.0-py3-none-any.whl

Download URL morphiaone-0.1.0-py3-none-any.whl
Size 26.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7911dd2a6943a3d507286f0b85fa49b60ab25e3a76708b10e95999f111fd17a3
BLAKE2b-256 checksum
How to use checksums
3ba984fcb9efca95a0a7fbc9129aced5c85868e74d59ae8ea380837dc4f44624
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.1.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