Skip to content
scroll to zoom · drag to pan

Quickstart

You need your credentials first — an endpoint, a tenant id and a runtime key. Everything below assumes you have them.

media-harness.vercel.app is a public bench running this exact SDK. Open it on your phone, drop in a photo or a video, and watch the five steps happen one at a time — format in and out, bytes in and out, dimensions, aspect ratio, and milliseconds per step. No account, no key, no install.

It is the fastest way to see what the platform does to a file before you write any code, and it is the same page we use to catch format regressions: the silent PNG fallback that shipped for months only reproduces in a real browser on a real phone, and a table like this one would have shown it on the first photo.

Terminal window
bun add @nitida/sdk @nitida/asset-client

Which one do you actually need?

  • Only rendering media that already exists → @nitida/asset-client alone. No network, no key, nothing to configure but a tenant id.
  • Also uploading → add @nitida/sdk. It re-exports every URL builder, so you never import both for the same job.
upload-an-image.ts
/**
* Upload one image, end to end.
*
* This file is REAL: `bun run check` type-checks it in CI against the same
* workspace version of the SDK this site documents. If a signature changes,
* the build fails instead of the website quietly lying.
*/
import { NitidaClient } from "@nitida/sdk";
// The `amk_rt_*` runtime key is a WRITE key to a paid platform. It lives on the
// server, in a secret — never in a client bundle, never in `NEXT_PUBLIC_*`.
const nt = new NitidaClient({
endpoint: process.env.NITIDA_ENDPOINT ?? "https://assets.example.com",
apiKey: process.env.NITIDA_RUNTIME_KEY,
tenantCode: "demo",
// Required: variant URLs are tenant-prefixed in base36 (`/a/v/<sha>-lg.webp`).
tenantId: Number(process.env.NITIDA_TENANT_ID ?? 1),
});
export async function uploadImage(file: File) {
const result = await nt.upload(file, {
// ⚠️ "original" is non-negotiable at FIRST ingest. A variant you do not ask
// for here can never be recovered later: the cleanup job reaps `raw/`, and
// `regenerate()` then has nothing left to read from.
presets: ["original", "thumb", "md", "lg"],
// Browser-only; in Node/Bun it no-ops with a warning and uploads raw bytes.
compress: true,
});
// `upload` returns ids and the canonical URL — not a full asset. Build the
// other URLs from the sha.
return {
assetId: result.assetId,
canonical: result.cdnUrl,
display: nt.urlFor({ sha: result.sha256 }, "lg"),
};
}

One call does the whole loop: compress → hash → presign → PUT straight to storage → register → poll until ready. You get back an id, a SHA and a URL.

image-urls.ts
/**
* Build image URLs without uploading anything.
*
* `@nitida/asset-client` has no network and no key — it is pure URL
* construction. Import it directly when all you do is render.
*/
import {
getTransformSrcSet,
getTransformUrl,
setCdnBase,
setTenantId,
type TransformWidth,
} from "@nitida/asset-client";
// The builders read PROCESS-GLOBAL config. Pin it once where your helpers live,
// so every module that imports a builder is configured.
setCdnBase(process.env.NEXT_PUBLIC_NITIDA_CDN ?? "https://8ok.uk");
setTenantId(Number(process.env.NEXT_PUBLIC_NITIDA_TENANT_ID ?? 1));
// Lock the output format to your project's policy rather than using `auto`.
const FORMAT = "webp" as const;
/**
* ⚠️ The width MUST be on the unsigned ladder. Any other width is an HTTP 400
* at the edge — that is a deliberate DoS guard, not a bug. Importing
* `TransformWidth` turns an off-ladder width into a COMPILE error, which is
* where you want to find out.
*/
export const imageUrl = (sha: string, width: TransformWidth) =>
getTransformUrl({ sha }, { format: FORMAT, width });
export const imageSrcSet = (sha: string, widths: readonly TransformWidth[]) =>
getTransformSrcSet({ sha }, widths, { format: FORMAT });
/** Fixed-aspect cards and thumbnails want a cover crop. */
export const thumbUrl = (sha: string, width: TransformWidth) =>
getTransformUrl(
{ sha },
{ format: FORMAT, width, fit: "cover", gravity: "auto" },
);

Widths must be on the allowed ladder — 21 of them: 96 · 128 · 160 · 180 · 240 · 256 · 320 · 400 · 480 · 600 · 640 · 800 · 960 · 1080 · 1200 · 1280 · 1440 · 1600 · 1920 · 2560 · 3840. Anything else answers 400 at the edge — deliberately, because an open resize endpoint is a denial-of-service amplifier. Importing the TransformWidth type makes an off-ladder width a compile error instead of a production surprise.

you want to… read
understand what runs where How it works
know which presets to ask for Variants and presets
upload video Video and HLS
keep a PNG’s transparency Formats and transparency
use the palette or an AI proxy Beyond images
work out what an error means What each symptom means
hand this to a coding agent For agents