Skip to content
scroll to zoom · drag to pan

The rest of the surface

The pages before this cover what almost every integration needs. This one covers what exists beyond that, so nothing is a surprise you discover from a stack trace.

Signed transform URLs — the only way to a custom width

Section titled “Signed transform URLs — the only way to a custom width”

Unsigned transforms accept only widths on the fixed ladder. Anything else is 400 at the edge, and that is a denial-of-service guard, not an oversight: an open resize endpoint lets anyone mint unlimited unique renders on your bill.

A signed URL earns the bypass, because you vouched for it:

import { getSignedTransformUrl } from "@nitida/asset-client";
// width is a plain number here — any number.
// The signing key is the THIRD argument and it is required.
const url = await getSignedTransformUrl(
{ sha },
{ width: 733, format: "webp" },
process.env.NITIDA_SIGNING_KEY!,
);
// or, with the client holding the key:
// const nt = new NitidaClient({ …, signingKey: process.env.NITIDA_SIGNING_KEY });
// await nt.transform(asset, { width: 733 }, { sign: true });

The types enforce the rule so you cannot get it wrong by accident: TransformOptions.width is the ladder union, while SignedTransformOptions.width is number — and only the signing helpers accept the latter.

The signing key is not part of the credential set you are issued, and none of your own keys can read it — a tenant admin key gets 403 SYSTEM_KEY_REQUIRED. It is minted by:

POST /admin/projects/:code/rotate-signing-key

which requires a system-scope credential held by the platform operator, and returns the key once. So: ask for it, then keep it in your secret manager next to the API key.

Rotating invalidates every URL signed with the previous key immediately, so coordinate it with whatever pre-signs — a build-time cache, a BFF route.

If you already hold a key and a signed URL still answers 401 invalid_signature, the key is wrong or rotated, not the URL shape: an off-ladder width that answers 400 unsigned and 401 signed proves the signature path is reached and the bypass works.

An AI upscale of an existing asset, through a configured provider.

POST /assets/:id/upscale start (or return an existing one)
GET /assets/:id/upscales everything upscaled for this sha
POST /assets/upscale/:id/poll manual finalize, when a webhook was missed

Idempotent on (tenant, sha, preset, provider) — asking twice does not pay twice. It is asynchronous and provider-backed, so treat it like the video path: kick it off, poll or wait for the webhook, and have a fallback for the missed one. That is what /poll is for.

This is metered separately from storage — the upscale op is its own scope on the runtime key.

await nt.usage.snapshot(); // current period
await nt.usage.timeseries(30); // daily points
await nt.usage.keys(); // per-key breakdown

The Cloudinary-style consumption view: what you have stored, transformed and served. keys() is the one worth wiring early — when usage jumps, “which key did this” is the first question, and per-key attribution is the only cheap answer.

Social preview images have a different shape from user uploads: they are pre-rendered at deploy time, already the exact size, and they must not go through a resize ladder that would only re-encode them.

POST /og-cards/upload service-to-service, Bearer OG_UPLOAD_API_KEY

Bytes go base64 in a JSON envelope, because the callers are build-time jobs (Vercel, CI) where a simple wire format beats multipart.

Use this only for that case. If a human uploaded it, it belongs on the normal path.

Two processors that build a new video from pieces you already uploaded:

  • Slideshow — stills into a clip.
  • Marketing composition — stitch segments into one MP4.

Both follow the same contract as any long video job: a processing row comes back immediately, and it flips to ready or failed when the job finishes. Poll with waitReady and budget minutes, not seconds — a multi-segment kit is a 10-minute timeout, not a 5-minute one.

Worth knowing, because they explain latency and failure modes:

system role what happens if it is down
Object storage the bytes uploads fail — this is the hard dependency
CDN delivery serving degrades; uploads unaffected
The video job runner video transcode, HLS video stays processing; images unaffected
Upscale provider AI upscaling that upscale fails; nothing else does
Your webhooks palette / asset-ready nothing. Fire-and-forget by design

That last row is a deliberate choice: an upload does not fail because a downstream consumer was unavailable. You lose a notification, not an asset — reconcile with a retry or a backfill. The alternative trades a recoverable gap for an unrecoverable one.