Next.js
Mark every AI file your Next.js app makes with one server-side helper. The key stays on the server, big files and videos are handled, and a file that can't be marked is never served.
In short
- Where: a route handler or a server action, never client code.
- What: one helper,
lib/kennmarq.ts, for images, songs and videos. - Fields: your company, your app and the markets you serve: California's five items, and a receipt per file.
- If it fails: the request fails, and the unmarked original is never stored or shown.
1. The key
Create a key in the dashboard (Quickstart) and put it in your environment: on Vercel, in the project's environment variables; locally, in .env.local.
# .env.local: never NEXT_PUBLIC_, so it never reaches the browser
KENNMARQ_API_KEY=sk_test_…2. The helper
One function, mark(), for images, songs and videos. Files over 4 MB go up first through an upload, and a video is marked as a job it waits for. It needs nothing but fetch.
// lib/kennmarq.ts: server code only (npm install server-only)
import "server-only";
const API = "https://api.kennmarq.com/v1";
const auth = { Authorization: `Bearer ${process.env.KENNMARQ_API_KEY}` };
// Your company and your AI system: with them, every mark carries California's five items.
const fields: Record<string, string> = {
source: "created", // or "edited", "composite"
provider: "Acme Inc.",
app_name: "Acme Studio",
app_version: "2.1",
markets: "eu,us-ca",
};
async function failed(res: Response): Promise<never> {
const { error } = await res.json();
throw new Error(`Kennmarq.com ${res.status} ${error.code}: ${error.message}`);
}
/** The marked copy of an image, song or video. Throws rather than hand back the original. */
export async function mark(file: Blob, filename: string, extra: Record<string, string> = {}): Promise<Blob> {
const form = new FormData();
for (const [k, v] of Object.entries({ ...fields, ...extra })) form.append(k, v);
// A request carries at most 4 MB: a bigger file goes up first.
if (file.size < 3_900_000) form.append("file", file, filename);
else form.append("upload", await upload(file));
const res = await fetch(`${API}/mark`, { method: "POST", headers: auth, body: form });
if (res.status === 202) return finished(await res.json()); // a video: marked as a job
if (!res.ok) return failed(res);
return res.blob();
}
async function upload(file: Blob): Promise<string> {
const res = await fetch(`${API}/uploads`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ bytes: file.size }),
});
if (!res.ok) return failed(res);
const { id, url } = await res.json();
// No key here: the URL is the permission. fetch sends the Content-Length it's signed for.
const put = await fetch(url, { method: "PUT", body: file });
if (!put.ok) throw new Error(`Kennmarq.com upload: ${put.status}`);
return id;
}
async function finished(job: { url: string }): Promise<Blob> {
for (;;) {
await new Promise((r) => setTimeout(r, 5_000));
const res = await fetch(job.url, { headers: auth });
if (!res.ok) return failed(res);
const m = await res.json();
if (m.status === "failed") throw new Error(`Kennmarq.com: ${m.error?.code}: ${m.error?.message}`);
if (m.status === "completed") return (await fetch(m.download.url)).blob();
}
}3. Mark what your model makes
In a route handler, right after generating:
// app/api/generate/route.ts
import { mark } from "@/lib/kennmarq";
import { generate } from "@/lib/model"; // your model: Replicate, fal, OpenAI, your own ComfyUI…
// Marking takes a second or two; leave room for a video, or for a slow first call.
export const maxDuration = 60;
export async function POST(request: Request) {
const { prompt } = await request.json();
const image: Blob = await generate(prompt);
const marked = await mark(image, "image.png", { model: "FLUX.1 schnell" });
// Store or return the marked copy only. If mark() threw, this request fails instead.
return new Response(marked, { headers: { "Content-Type": marked.type || "image/png" } });
}Or in a server action that stores the result:
// app/actions.ts
"use server";
import { put } from "@/lib/storage"; // S3, R2, Vercel Blob…
import { mark } from "@/lib/kennmarq";
import { generate } from "@/lib/model";
export async function createImage(prompt: string) {
const marked = await mark(await generate(prompt), "image.png");
return put(`images/${crypto.randomUUID()}.png`, marked); // never the unmarked original
}Videos
A video is marked as a job: the call answers 202, and the helper asks every five seconds until it's done, usually a minute or two. In production, let a webhook (mark.completed) tell you instead, and keep the request short.
When a call fails
429 rate_limited: too many calls this minute for your plan; wait theRetry-Afterseconds.402 quota_exceeded: this month's marks are used up; upgrade or wait for the 1st.400 invalid_request: its message names what's missing;markets=us-caneedsprovider,app_nameandapp_version.
Every code is on the errors page. To see the result, drop a marked image on the checker. Using the Vercel AI SDK? Its guide uses the same helper.