Skip to content

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
# .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
// 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
// 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
// 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 the Retry-After seconds.
  • 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-ca needs provider, app_name and app_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.