Original Pictures
Next.js server action signing an upload

C2PA Next.js Integration: Sign Every Upload in 12 Lines

Definition: The Original Pictures Next.js integration wraps the signing API in a server route so developers can add C2PA manifest generation, identity assertion, and watermarking to any upload handler without exposing API keys to the client.

TL;DR: In the Next.js App Router, a server route receives an upload, uploads it to the signing API, and returns the signing job, with the API key living only on the server. The core is about a dozen lines of TypeScript.

Why server-side signing

Calling the signing API from the browser would expose your API key in DevTools. A server route keeps the key in the server environment: the client sends bytes, the server signs, the client gets back the signing job. That separation is the whole security argument.

The implementation

A POST route reads the form data, validates the file and size, uploads it to the signing API, completes the upload, accepts a signing job with a recipe, and returns the job. The client posts a FormData upload to the route and reads the job ID from the response.

Production hardening

Enforce size limits at the edge; Vercel's default body limit is small, so stream large files directly with a presigned session. Use the file's SHA-256 as an idempotency key to avoid double-charging on retries. Map the API's timeout, validation, and rate-limit errors to 504, 422, and 429. Use the Node runtime, not Edge, for crypto.

The incident behind this

The Pentagon hoax of May 2023 is the reminder that generated content needs provenance at the API boundary, before it is distributed, which is exactly where a signing route sits.

Implementation

// app/api/sign/route.ts (Node runtime; the API key never reaches the browser)
import { createHash, randomUUID } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";

const API = "https://dev-api.originalpictures.com";

async function call(path: string, body: unknown, idempotent = false) {
  const res = await fetch(`${API}${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OP_API_KEY}`,
      "Content-Type": "application/json",
      ...(idempotent ? { "Idempotency-Key": randomUUID() } : {}),
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${path}: ${res.status}`);
  return res.json();
}

export async function POST(req: NextRequest) {
  const file = (await req.formData()).get("file");
  if (!(file instanceof File) || file.size > 50 * 1024 * 1024)
    return NextResponse.json({ error: "File required, max 50MB" }, { status: 400 });
  const bytes = new Uint8Array(await file.arrayBuffer());

  // Reserve an upload bound to these exact bytes.
  const upload = await call("/v1/uploads", {
    byte_length: bytes.byteLength,
    sha256: createHash("sha256").update(bytes).digest("hex"),
    declared_media_type: file.type, // image/jpeg, image/png, audio/wav, audio/mpeg or video/mp4
    mode: "single",
    filename: file.name,
  });
  // Storage URL: send exactly the issued headers, never the API key.
  await fetch(upload.upload.url, { method: "PUT", headers: upload.upload.headers, body: bytes });
  await call(`/v1/uploads/${upload.upload_id}/complete`, {}, true);
  const job = await call(
    "/v1/sign",
    { asset_id: upload.asset_id, recipe_id: "rcp_system_watermark_then_sign" },
    true,
  );
  // 202: poll GET /v1/jobs/{job_id}, or use webhooks, until the job completes.
  return NextResponse.json({ job_id: job.job_id, state: job.state }, { status: 202 });
}

FAQ

Does this work with the Pages Router?

Yes. Use a standard API route in pages/api; the route only needs fetch.

Can I run it on Vercel Edge?

Use the Node runtime. The route uses Node crypto for the SHA-256, so set runtime to nodejs, not edge.

Where Original Pictures stands today

Original Pictures ships three things today: a Sign API, a Verify API, and the SDKs that wrap them (not yet published to npm or PyPI). One POST /v1/sign with the watermark-then-sign recipe attaches a C2PA-format manifest and an invisible TrustMark watermark, with RFC 3161 timestamp evidence recorded alongside the signature. Any C2PA-aware validator can read the manifest without calling us.

Two things are on the near roadmap, and we name them as roadmap, not as shipped: C2PA Conformance Program recognition (target Q3 2026, until then our manifests use the published C2PA v2.2 format and any C2PA-aware validator can read them, but third-party validators will show our signer as not-yet-listed), and a consumer capture app (Q3 2026). We do not sell a capture SDK, and we do not claim Trust-List membership we do not yet hold.

Bottom line: A Next.js server route is the most secure, minimal way to add signing to any upload: a dozen lines, no client-side secrets, full marking coverage.

Related


Original Pictures is progressing through the C2PA Conformance Program; our signing certificate is not yet on the official C2PA Trust List. Target: Q3 2026. We will not describe ourselves as "C2PA-certified" until it is true.

Original Pictures provides content-provenance infrastructure. It does not by itself constitute legal compliance with the EU AI Act or any other regime; compliance depends on how you deploy it, your disclosures, and your governance. Figures are drawn from public reporting, verify against primary sources before citing in regulated materials. Nothing here is legal advice.

Last verified 2026-05-25. Author: Mahdi Kazempour, Founder, Original Pictures.