Add EU AI Act Article 50 marking to your AI image app in 10 minutes
A hands-on tutorial: watermark and C2PA-sign every AI-generated image with the Vercel AI SDK, Node.js, Python or plain HTTP, then verify it worked.
7 min read · Updated 6 November 2026 · Markproof
In short
- Article 50(2) of the EU AI Act asks providers of generative AI to mark output in a machine-readable way that makes it detectable as AI-generated.
- This tutorial adds that marking to an image app in about ten minutes: an invisible watermark plus signed C2PA Content Credentials on every file, then a check that it worked.
- Pick your stack: Vercel AI SDK (one wrapper), Node.js, Python or plain HTTP. Audio works the same way.
What you need
- An app that generates images (or audio) with any model: OpenAI, Stability, Flux, your own.
- Node.js 18+ or Python 3, depending on the path you pick.
- A free Markproof API key: sign up on the sign-up page and copy the key from your dashboard. The free plan includes 250 calls a month.
# .env — create a key on https://markproof.co/dashboard (250 free calls a month)MARKPROOF_API_KEY=lt_live_...Step 1, option A: Vercel AI SDK (one wrapper)
If you generate images with the Vercel AI SDK, wrap the model once with markproofImageMiddleware. The wrapped model works anywhere a normal image model does, and every image comes back marked.
npm install ai @ai-sdk/openai markproof-aiimport { writeFile } from "node:fs/promises";import { generateImage, wrapImageModel } from "ai";import { openai } from "@ai-sdk/openai";import { markproofImageMiddleware } from "markproof-ai"; // Wrap the model once. Every image it generates comes back marked.const model = wrapImageModel({ model: openai.image("gpt-image-1"), middleware: markproofImageMiddleware({ generatorName: "my-app" }),}); const { images, providerMetadata } = await generateImage({ model, prompt: "a lighthouse at dusk",}); // images[0] is already watermarked and C2PA-signed (PNG). Serve it as-is.await writeFile("lighthouse.png", images[0].uint8Array); const [proof] = providerMetadata.markproof.images;console.log(proof.watermarkCode, proof.verifyUrl);// e.g. 91EDVH9T https://markproof.co/v/91EDVH9TThe model's provider and ID (here openai/gpt-image-1) are recorded in the C2PA manifest automatically. By default, if marking fails the generation fails too, so an unmarked image is never returned by mistake. Pass onError: "passthrough" if you'd rather keep the original and log the failure.
Need a visible label too?
If the image could be mistaken for a real photo of a real person or event, Article 50(4) also expects a visible disclosure. Add visibleLabel: true to the middleware options to draw an “AI-generated” label before marking, or pass your own text (up to 40 characters).
Step 1, option B: any other stack
Not on the Vercel AI SDK? Mark the file right after your model returns it. Same result: a watermarked, C2PA-signed file, its watermark code and a public verify URL.
Node.js
npm install markproofimport { readFile, writeFile } from "node:fs/promises";import { Markproof } from "markproof"; const client = new Markproof({ apiKey: process.env.MARKPROOF_API_KEY! }); const marked = await client.mark( { data: await readFile("generated.png"), filename: "generated.png" }, { generatorName: "my-app", modelName: "my-model-v1" },); await writeFile("generated.marked.png", marked.file);console.log(marked.watermarkCode, marked.verifyUrl);Python
pip install markproofimport osfrom markproof import Markproof client = Markproof(api_key=os.environ["MARKPROOF_API_KEY"]) marked = client.mark("generated.png", generator_name="my-app", model_name="my-model-v1") with open("generated.marked.png", "wb") as f: f.write(marked.file) print(marked.watermark_code, marked.verify_url)Plain HTTP
curl https://markproof.co/v1/mark \ -H "Authorization: Bearer $MARKPROOF_API_KEY" \ -F file=@generated.png \ -F generator_name="my-app" \ -F model_name="my-model-v1" \ -o generated.marked.png -D - # The marked file is written to generated.marked.png. The headers include:# x-watermark-code: 91EDVH9T# x-verify-url: https://markproof.co/v/91EDVH9TStep 2: check that it worked
Three ways, from quickest to most automatable:
- Open the verify URL from the response, such as
markproof.co/v/91EDVH9T. It's a public page anyone can open. - Drop the file into the free checker. It reads the watermark and the C2PA manifest, with no account needed. This is also what you can point users, journalists or auditors to.
- Verify in code, for example in a test that runs on every deploy:
const result = await client.verify({ data: await readFile("generated.marked.png"), filename: "generated.marked.png",});console.log(result.summary);result = client.verify("generated.marked.png")print(result.summary)To see why the watermark matters, take a screenshot of the marked image, or convert it to JPEG, and check it again. The C2PA manifest is usually gone, but the watermark is still found. That's the point of having both layers.
Step 3: ship it without losing the marks
- Mark as the last step. Do any resizing, cropping or format conversion before marking, not after. Anything that rewrites the file can drop the manifest.
- Serve the marked file as-is. If your CDN or image pipeline re-encodes images, check the delivered file in the checker, not just the one your server produced.
- Store the watermark code or asset ID next to each generation in your database. It ties any copy found later back to your app.
- Keep evidence. Your dashboard keeps a record of every marked file. On the Pro plan you can export a signed audit report for any date range.
Sources
This guide is general information, not legal advice. Laws and guidance change; check the sources, and talk to a qualified lawyer about your situation.
Check your own output in seconds.
Drop a file into the free checker to see whether it carries a watermark and Content Credentials. Then mark your files with one API call: 250 free calls a month, no card required.