Docs
API reference
Three endpoints: mark a file, verify a file, and look up a file you marked. Every request and response is plain HTTP, so any language works.
Quickstart
Create an account to get an API key, then mark your first file. The marked file is written to render.marked.png, and the proof details print as response headers.
curl https://markproof.co/v1/mark \ -H "Authorization: Bearer $MARKPROOF_KEY" \ -F file=@render.png \ -F generator_name="your-app" \ -F model_name="your-model-v2" \ -o render.marked.png -D -Authentication
Send your key as a bearer token on every request: Authorization: Bearer lt_live_…. Keys are shown once, when they're created. If you lose one, generate a new key from your dashboard. The old key stops working immediately.
Keep keys on your server. Never ship one in browser or mobile app code.
SDKs & integrations
Official packages wrap the API below, so you don't have to build requests by hand.
Node.js and Python
npm install markproof import { readFileSync } from "node:fs";import { Markproof } from "markproof"; const markproof = new Markproof({ apiKey: process.env.MARKPROOF_API_KEY });const result = await markproof.mark( { data: readFileSync("render.png"), filename: "render.png" }, { generatorName: "your-app" },);// result.file is the marked PNG; result.watermarkCode, result.verifyUrl …pip install markproof import osfrom markproof import Markproof markproof = Markproof(os.environ["MARKPROOF_API_KEY"])result = markproof.mark("render.png", generator_name="your-app")# result.file is the marked PNG; result.watermark_code, result.verify_url …Vercel AI SDK
Wrap your image model once and every generateImage result is marked before it reaches your code. The model's provider and ID are recorded in the C2PA manifest automatically. If marking fails, the call fails too, so an unmarked image is never returned by mistake. For audio from generateSpeech, use markGeneratedFile(audio).
npm install markproof-ai import { generateImage, wrapImageModel } from "ai";import { openai } from "@ai-sdk/openai";import { markproofImageMiddleware } from "markproof-ai"; const model = wrapImageModel({ model: openai.image("gpt-image-1"), middleware: markproofImageMiddleware({ generatorName: "your-app" }),}); // Every image comes back already marked.const { images, providerMetadata } = await generateImage({ model, prompt: "…" });// providerMetadata.markproof.images[i] → watermarkCode, verifyUrl, …AI agents (MCP)
markproof-mcp is a Model Context Protocol server. It gives agents in Claude, Cursor and other MCP clients three tools: mark_file (writes name.marked.png beside the original), verify_file and get_asset. Add it to your client's MCP config, or with Claude Code run claude mcp add markproof --env MARKPROOF_API_KEY=lt_live_… -- npx -y markproof-mcp.
{ "mcpServers": { "markproof": { "command": "npx", "args": ["-y", "markproof-mcp"], "env": { "MARKPROOF_API_KEY": "lt_live_…" } } }}LangChain and LlamaIndex
Both frameworks load MCP servers as agent tools, so markproof-mcp works with them directly. Pass your key through env: MCP clients often don't forward your whole environment to the server process.
pip install "langchain[mcp]" import osfrom langchain.agents import create_agentfrom langchain.mcp import MCPAdapter config = {"mcpServers": {"markproof": { "command": "npx", "args": ["-y", "markproof-mcp"], "env": {"MARKPROOF_API_KEY": os.environ["MARKPROOF_API_KEY"]},}}} async with MCPAdapter(config) as adapter: tools = await adapter.list_tools() # mark_file, verify_file, get_asset agent = create_agent("anthropic:claude-sonnet-5-5", tools) await agent.ainvoke({"messages": [{"role": "user", "content": "Mark render.png with Markproof."}]})pip install llama-index-tools-mcp import osfrom llama_index.core.agent.workflow import FunctionAgentfrom llama_index.tools.mcp import BasicMCPClient, McpToolSpec client = BasicMCPClient( "npx", args=["-y", "markproof-mcp"], env={"MARKPROOF_API_KEY": os.environ["MARKPROOF_API_KEY"]}, timeout=120, # the default 30s can be too short for a first, cold-start call)tools = await McpToolSpec(client=client).to_tool_list_async()agent = FunctionAgent(tools=tools, llm=llm) # any LlamaIndex LLMComfyUI
The Markproof node pack adds Mark & Save, which you use in place of Save Image (re-encoding would strip the C2PA manifest), and Verify Image. Clone it into ComfyUI/custom_nodes; a ComfyUI-Manager listing is in review. Set MARKPROOF_API_KEY in the environment before starting ComfyUI.
Mark a file
POST/v1/mark
Adds an invisible watermark and a signed C2PA manifest to an image or audio file. Send the file as multipart/form-data, or send JSON with a url for Markproof to fetch it from.
| Field | Description |
|---|---|
| file | The image (PNG, JPEG, WebP) or audio (WAV, MP3) file. The type is detected automatically. Multipart only. |
| url | Public URL of the file, instead of file. JSON only. |
| generator_name | Optional. Name of the system that generated the file. Recorded in the C2PA manifest. |
| model_name | Optional. Name of the model that generated the file. |
| response_format | file (default for uploads) returns the marked file in the body. url returns JSON with a download link instead. |
Response
With response_format=file, the body is the marked file and the details come back as headers: X-Asset-Id, X-Watermark-Code, X-Verify-Url, X-C2pa-Manifest-Label and, for images, X-Watermark-Robust. Images are returned as PNG and audio as WAV, so the watermark isn't degraded by re-compression.
With response_format=url, you get JSON. The download link is temporary, and the stored file is deleted after 30 days.
Watermark robustness
Every image is checked after marking: the watermark must still decode after JPEG re-compression, a 50% resize, a 10% crop and WebP conversion. Markproof uses the faintest watermark that passes. For rare, very smooth or featureless images no setting passes, and watermark_robust comes back false. The file is still marked and signed, but its watermark may not survive editing, so rely on the C2PA manifest for those files.
curl https://markproof.co/v1/mark \ -H "Authorization: Bearer $MARKPROOF_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://cdn.example.com/render.png", "response_format": "url"}'{ "asset_id": "5e847be7-7d96-4808-85c3-7cbeb5233351", "watermark_code": "ARN26QVH", "kind": "image", "verify_url": "https://markproof.co/v/ARN26QVH", "c2pa_manifest_label": "urn:c2pa:c600893b-eab4-4ae3-919f-e5da47cc7f50", "watermark_robust": true, "file_url": "https://…signed-download-url…"}Verify a file
POST/v1/verify
Checks any image or audio file for a Markproof watermark and a C2PA manifest. Accepts file or url, like /v1/mark. A file with neither is still a 200, because “not marked” is a valid answer, not an error.
| Field | Meaning |
|---|---|
| watermark.detected | Whether a Markproof watermark is present. |
| watermark.watermark_code | The decoded code, for images. Always null for audio, where detection is presence-only. |
| watermark.confidence | For audio, the detector's probability from 0 to 1. null for images. |
| c2pa.signature_valid | false means the signature doesn't match, usually because the file was changed after signing. |
| c2pa.trusted | Whether the signing certificate is on the public C2PA Trust List. Currently always false for files marked by Markproof. |
| matched_asset | The asset record, when the watermark code matches one in Markproof. |
| summary | A one-line, human-readable result. |
curl https://markproof.co/v1/verify \ -H "Authorization: Bearer $MARKPROOF_KEY" \ -F file=@render.marked.png{ "watermark": { "detected": true, "watermark_code": "ARN26QVH", "confidence": null }, "c2pa": { "present": true, "signature_valid": true, "trusted": false, "manifest_label": "urn:c2pa:c600893b-eab4-4ae3-919f-e5da47cc7f50" }, "matched_asset": { "id": "5e847be7-7d96-4808-85c3-7cbeb5233351", "watermark_code": "ARN26QVH", "kind": "image", "generator_name": null, "model_name": null, "c2pa_manifest_label": "urn:c2pa:c600893b-eab4-4ae3-919f-e5da47cc7f50", "created_at": "2026-10-27T00:52:35.390961+00:00" }, "summary": "Watermark detected code ARN26QVH, matches our record. C2PA manifest present and valid, signed by an untrusted certificate."}Look up an asset
GET/v1/assets/{id}
Returns the record for a file you marked, using the asset_id from /v1/mark. Any key on the same project works. Assets from other projects return 404.
curl https://markproof.co/v1/assets/5e847be7-7d96-4808-85c3-7cbeb5233351 \ -H "Authorization: Bearer $MARKPROOF_KEY"Public verify page
Every marked file gets a public page at https://markproof.co/v/{watermark_code}, returned as verify_url. Anyone can open it without an account. It shows the media type, generator, marking date and C2PA manifest label, and nothing else about your account. Anyone can also check a file directly with the free checker.
Limits
| Limit | Value |
|---|---|
| Monthly quota | 250 calls on Free, 10,000 on Indie, 100,000 on Pro. Calls to /v1/mark and /v1/verify both count. |
| Rate limit | 60 requests per minute, per API key. |
| Data location | Files are processed in the EU: the API runs in Frankfurt and marking runs on EU servers. The database is in Frankfurt. |
| Upload size | About 4.5 MB for direct uploads. For larger files, up to 25 MB, pass a url instead. |
Errors
Errors return JSON with an error message, for example {"error": "Monthly quota exceeded (250/250)."}.
| Status | Meaning |
|---|---|
| 400 | Malformed request, such as no file or url, or a URL that can't be fetched. |
| 401 | Missing, malformed or revoked API key. |
| 404 | Asset not found, or it belongs to another project. |
| 413 | File too large. |
| 422 | Unsupported or unrecognised file type. |
| 429 | Rate limit or monthly quota reached. Rate-limit responses include a Retry-After header. |
| 500 | Something went wrong on our side. Try again shortly. |