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.6.0is 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:
- Upload each input image once as an Asset.
- Submit a product request that references the Asset ids. The API answers with a Job; the work runs asynchronously.
- 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)
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.idbefore you wait. It is how you resume after a restart, from another process or from a queue consumer:client.jobs.get(job_id)orclient.jobs.wait(job_id). - A wait timeout stops local waiting only. It does not cancel the job on the server.
done.outputisSkinAnalysisOutput,MakeupTryOnOutput,HairTryOnOutput, orUnknownJobOutputfor 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 leastrequest_timeout) boundsjobs.waitand a submit withwait=True, submit and polling together. Both are client options, and every call accepts its owntimeout, which replaces the relevant default for that call: on a submit withwait=Trueit 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,503and504are retried (4 attempts in all by default). A serverRetry-Afteris waited as named, never shortened; when it is over 60 s, or longer than the call's remaining deadline, the error is raised (withretry_afterset) instead of retrying early. Without one, the SDK backs off exponentially, each delay capped atRetryPolicy.max_delay(5 s). A submit's retries resend the sameIdempotency-Key, and a409 IDEMPOTENCY_IN_PROGRESSis retried too. A quota or credit429is 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.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| morphiaone-0.7.0.tar.gz | 64.3 kB | Without provenance | Details for morphiaone-0.7.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| morphiaone-0.7.0-py3-none-any.whl | Python 3 | none | any | Details for morphiaone-0.7.0-py3-none-any.whl |
Total release size: 129.1 kB
Release files / morphiaone-0.7.0.tar.gz
| Download URL | morphiaone-0.7.0.tar.gz |
|---|---|
| Size | 64.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
03cab7663883b1057b792676d7fd90fd02a4a3c818c5cba15ed3ff4c9e201845
|
|
BLAKE2b-256 checksum How to use checksums |
8d2054e7de3e97bdd5592a0dfead1f4caa12bed9901ca6e921edc9d8de7c0e16
|
| 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.7.0-py3-none-any.whl
| Download URL | morphiaone-0.7.0-py3-none-any.whl |
|---|---|
| Size | 64.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e87ccbf602abfa4e1601db544b60f11eab97b4c5b8d6c419b8cdea263fe166a5
|
|
BLAKE2b-256 checksum How to use checksums |
8a041aa669c3bce72819bef85122277a4e140f6cd34d9bbe6c5ac5a6f274f4bd
|
| 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.7.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.