Skip to content

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.

Mark a file
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

Node.js · npm install markproof
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 …
Python · pip install markproof
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).

markproof-ai
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.

claude_desktop_config.json · .cursor/mcp.json
{  "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.

LangChain · langchain[mcp] 1.4+
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."}]})
LlamaIndex · llama-index-tools-mcp
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 LLM

ComfyUI

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.

FieldDescription
fileThe image (PNG, JPEG, WebP) or audio (WAV, MP3) file. The type is detected automatically. Multipart only.
urlPublic URL of the file, instead of file. JSON only.
generator_nameOptional. Name of the system that generated the file. Recorded in the C2PA manifest.
model_nameOptional. Name of the model that generated the file.
response_formatfile (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.

Mark from a URL
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"}'
Response · 200
{  "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.

FieldMeaning
watermark.detectedWhether a Markproof watermark is present.
watermark.watermark_codeThe decoded code, for images. Always null for audio, where detection is presence-only.
watermark.confidenceFor audio, the detector's probability from 0 to 1. null for images.
c2pa.signature_validfalse means the signature doesn't match, usually because the file was changed after signing.
c2pa.trustedWhether the signing certificate is on the public C2PA Trust List. Currently always false for files marked by Markproof.
matched_assetThe asset record, when the watermark code matches one in Markproof.
summaryA one-line, human-readable result.
Verify a file
curl https://markproof.co/v1/verify \  -H "Authorization: Bearer $MARKPROOF_KEY" \  -F file=@render.marked.png
Response · 200
{  "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.

Look up an asset
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

LimitValue
Monthly quota250 calls on Free, 10,000 on Indie, 100,000 on Pro. Calls to /v1/mark and /v1/verify both count.
Rate limit60 requests per minute, per API key.
Data locationFiles are processed in the EU: the API runs in Frankfurt and marking runs on EU servers. The database is in Frankfurt.
Upload sizeAbout 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)."}.

StatusMeaning
400Malformed request, such as no file or url, or a URL that can't be fetched.
401Missing, malformed or revoked API key.
404Asset not found, or it belongs to another project.
413File too large.
422Unsupported or unrecognised file type.
429Rate limit or monthly quota reached. Rate-limit responses include a Retry-After header.
500Something went wrong on our side. Try again shortly.