@diffusionstudio/api-contract
v0.5.0
Published
API contract for the Diffusion Studio backend: zod schemas and tRPC AppRouter types.
Readme
@diffusionstudio/api-contract
The API contract for the Diffusion Studio backend: zod input schemas, shared
types (Job, Asset, GenerateRequest, …) and the tRPC AppRouter type used
to create a fully typed client.
The AppRouter type is generated from the backend router, so this package
always reflects the deployed API surface. The runtime exports are limited to
zod schemas and constants; no server code is included.
Client
v2 is served at /api/v2/trpc. Queries and mutations go over HTTP;
jobs.watch is a server-sent events subscription, so route subscriptions to
httpSubscriptionLink with a splitLink:
import { createTRPCClient, httpBatchLink, httpSubscriptionLink, splitLink } from "@trpc/client";
import type { AppRouter } from "@diffusionstudio/api-contract";
const url = "https://api.diffusion.studio/api/v2/trpc";
const authorization = async () => `Bearer ${await getAccessToken()}`; // Supabase access token
export const api = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === "subscription",
// EventSource can't send headers, so the token travels as a connection param.
true: httpSubscriptionLink({ url, connectionParams: async () => ({ authorization: await authorization() }) }),
false: httpBatchLink({ url, headers: async () => ({ authorization: await authorization() }) }),
}),
],
});Every procedure requires auth. Request bodies are limited to 1 MB; files are uploaded directly to storage (see below).
Generating
All generation goes through generate, typed by the model discriminator.
Generative models share one schema per kind (imageRequest, videoRequest,
audioRequest, voiceRequest) with the widest bounds any model of the kind
accepts; a request outside the chosen model's own bounds (e.g. a duration Veo
doesn't support) fails with BAD_REQUEST. Tools have a schema each.
It returns a Job immediately; follow it with jobs.watch:
import type { Job } from "@diffusionstudio/api-contract";
const job = await api.generate.mutate({
request: { model: "minimax-h3", prompt: "A red fox at dawn", duration: 6 },
idempotencyKey: crypto.randomUUID(), // same key → same job, no double charge
maxCredits: 100, // optional: reject if the price is higher
});
const sub = api.jobs.watch.subscribe(
{ id: job.id },
{
onData: ({ data }: { data: Job }) => {
if (data.status === "succeeded") console.log(data.assets.map((a) => a.url));
if (data.status === "failed") console.error(data.error);
},
onError: console.error,
},
);- Each event is tracked by the job's version, so the link resumes from the last event after a reconnect. The server closes the stream once the job is terminal (or after 5 minutes; the link reconnects).
jobs.get({ id })is the polling alternative for scripts and the CLI.- Asset URLs are signed for 1 hour on every read; read the job again for fresh ones.
estimate(request)returns{ credits, etaSeconds }without creating a job.- Rerun:
generate({ request: job.request, idempotencyKey: crypto.randomUUID() }). jobs.cancel({ id })cancels and refunds.
Uploading inputs
Model inputs reference files as assetRef: { kind: "asset", id: sha256 } for
uploaded files or { kind: "url", url } for public https URLs. Files are stored
by content hash, so the same file is uploaded once:
const bytes = new Uint8Array(await file.arrayBuffer());
const digest = await crypto.subtle.digest("SHA-256", bytes);
const sha256 = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
const { asset, uploadUrl } = await api.uploads.create.mutate({ sha256, contentType: file.type, size: file.size });
if (uploadUrl) {
// Signed resumable upload (valid 15 min): start a session, then upload to it.
const start = await fetch(uploadUrl, {
method: "POST",
headers: { "x-goog-resumable": "start", "content-type": file.type },
});
await fetch(start.headers.get("location")!, { method: "PUT", body: file });
}
// uploadUrl === null: the file is already stored.
await api.generate.mutate({
request: { model: "minimax-h3", prompt: "Bring this to life", startFrame: asset },
idempotencyKey: crypto.randomUUID(),
});Uploads expire 30 days after their last uploads.create. If generate fails
with "asset not found — re-upload", call uploads.create again and upload.
v1
The v1 API (/api/trpc) stays available while clients migrate; its router type
is exported as AppRouterV1.
