Quickstart
You need your credentials first — an endpoint, a tenant id and a runtime key. Everything below assumes you have them.
Try it first, with no credentials at all
Section titled “Try it first, with no credentials at all”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.
Install
Section titled “Install”bun add @nitida/sdk @nitida/asset-clientWhich one do you actually need?
- Only rendering media that already exists →
@nitida/asset-clientalone. 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
Section titled “Upload”/** * 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.
Render
Section titled “Render”/** * 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.
Where to go next
Section titled “Where to go next”| 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 |