npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@morphiaone/sdk

v0.2.2

Published

MorphiaOne API client for JavaScript and TypeScript (Developer Preview): Skin Analysis, Makeup Try-On and Hair Color Try-On.

Readme

@morphiaone/sdk

The MorphiaOne V1 API client for Node.js, in TypeScript and JavaScript: 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.1.0 is published; this source tree is 0.2.2, which is not published until its release is approved.

Install

npm install @morphiaone/sdk

Requires Node.js 20 or newer. ESM only. No runtime dependencies. Type declarations ship with the package, so TypeScript and plain JavaScript use the same package.

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 { MorphiaOne } from "@morphiaone/sdk";

const client = new MorphiaOne({
  apiKey: process.env.MORPHIAONE_API_KEY!,
  baseUrl: "<your API base URL>",
});

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. The supported use of this package is your own backend; 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.
import { readFile, writeFile } from "node:fs/promises";

const source = await client.assets.upload(await readFile("face.jpg"));
const job = await client.skin.analyze({ assetId: source.id, concerns: ["acne", "redness"] });
const done = await client.jobs.wait(job.id); // rejects with ProcessingError if the job failed

assets.upload(image, { mediaType?, timeout? }) takes a file path, a Uint8Array or a Blob/File; the media type (image/jpeg, image/png or image/webp) is inferred from the name or the leading bytes when omitted.

Every submit takes one request object. Besides the product fields it accepts idempotencyKey, timeout and wait. With wait: true the call polls to the succeeded job before resolving, exactly as jobs.wait would:

const done = await client.skin.analyze({ assetId: source.id, concerns: ["acne"], wait: true });

Skin Analysis

const job = await client.skin.analyze({
  assetId: source.id,
  concerns: ["acne", "redness", "wrinkles"],
  visualizations: [{ concern: "acne", kind: "mask" }],
});
const done = await client.jobs.wait(job.id);
if (done.output?.known && done.output.type === "skin.analysis") {
  for (const item of done.output.concerns) console.log(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:

const job = await client.makeup.tryOn({
  assetId: source.id,
  products: [{ category: "lipstick", color: "#C2185B", opacity: 0.8 }],
});
const done = await client.jobs.wait(job.id);
if (done.output?.known && done.output.type === "makeup.try_on") {
  const image = await client.assets.getContent(done.output.image);
  await writeFile("makeup.png", image.content);
}

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 hairMaskAssetId. The color is a two-stop gradient, dark and light:

// The server finds the hair; add hairMaskAssetId only to use your own mask.
const job = await client.hair.tryOn({
  sourceAssetId: source.id,
  colorGradient: {
    dark: { color: "#3B1F16", opacity: 0.9 },
    light: { color: "#C87943", opacity: 0.8 },
  },
});
const done = await client.jobs.wait(job.id);

Jobs and results

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(jobId) or client.jobs.wait(jobId).
  • A wait timeout stops local waiting only. It does not cancel the job on the server.
  • A job output is an open union. Narrow on known, then on type, before reading it. An output type this SDK version does not know yet decodes to UnknownJobOutput (known: false, the payload whole in raw) rather than throwing, so a newer API does not break an older SDK.

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

Usage

const report = await client.usage.get({ startDate: "2026-09-01", endDate: "2026-10-01" });

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

Errors

Every error the SDK throws extends MorphiaOneError, and the tree is the Python 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 (detail is an alias), details, the requestId and, when the server sent one, retryAfter (seconds). A local fault carries each of those as null. Branch on code, never on the message.

An invalid argument throws a plain TypeError before anything is sent, and an upload over maxImageBytes a RangeError; neither is a MorphiaOneError (Python raises ValueError for both).

| Class | Extends | When | | --- | --- | --- | | 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 (DNS, connect, TLS, socket), 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; jobId, code, details and error are on it |

import { ApiError, ProcessingError, RateLimitError } from "@morphiaone/sdk";

try {
  await client.jobs.wait(job.id);
} catch (error) {
  if (error instanceof RateLimitError) scheduleRetry(error.retryAfter);
  else if (error instanceof ApiError) logFailure(error.code, error.requestId);
  else if (error instanceof ProcessingError) logFailure(error.code, error.jobId);
  else throw error;
}

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

Timeouts and retries

  • Timeouts are in seconds. requestTimeout (default 10) bounds one call, its retries included. operationTimeout (default 120, at least requestTimeout) 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 retryAfter set) instead of retrying early. Without one, the SDK backs off exponentially, each delay capped at retryPolicy.maxDelay (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 Python SDK retries exactly the same set.

Idempotency

Every submit carries an Idempotency-Key. When you do not pass one (idempotencyKey), the SDK generates one per call and reuses it, with the same body, 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 your own idempotencyKey, derived from your order or request id. 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 requestId from the error. Report security issues via your MorphiaOne account contact; do not put API keys or images in a report.

License

MIT.