Skip to content

Mark a picture, song or clip

POSThttps://api.kennmarq.com/v1/mark

Send a picture or a song; get it back with an invisible watermark and a signed, time-stamped C2PA manifest. Send a clip; get a job, and the marked clip from GET /v1/marks/{id} when it's done. A picture counts one mark, a song five per started minute, a clip five per started second. Needs a key with the mark scope.

Request

multipart/form-data, with these fields. Text fields are trimmed; flags take true/false (or 1/0, on/off, yes/no).

Request fields of POST /v1/mark
FieldTypeDescription
filefile, required (or upload)The file: a PNG, JPEG or WebP picture; an MP3, WAV or M4A song; or an MP4 clip. Its type is read from its bytes, not its name. The whole request is at most 4 MB; a larger file goes through an upload, named in upload: up to 25 MB, and a clip up to 1 GB.
uploadtext, instead of fileAn upload's ID (up_…) from POST /v1/uploads, for a file over 4 MB. It works once, within the hour, with the key that made it.
sourcetext, default createdHow the picture was made, recorded as its IPTC digital source type: created (made by AI), composite (AI combined with other material) or edited (an existing picture changed with AI). You declare it; we sign only AI source types.
providertext, optionalYour legal name, as the provider of the AI system, e.g. Acme Inc. With app_name and app_version it completes California's latent disclosure. At most 200 characters.
modeltext, optionalThe model's name, e.g. FLUX.1 schnell. At most 200 characters.
model_versiontext, optionalThe model's version. At most 200 characters.
app_nametext, optionalYour AI system's name, recorded as the software agent of the created action. Without it, the manifest names no software agent. At most 200 characters.
app_versiontext, optionalYour AI system's version. Needs app_name. At most 200 characters.
marketstext, optionalThe markets the mark is made for, separated by commas: eu, us-ca, us-wa, us-ut, or us for all three states. A call missing what those laws need is refused (us-ca and us-wa need provider, app_name and app_version), and the mark gets a signed receipt. China, India, South Korea and Vietnam need visible labels we don't add yet: they're refused with option_not_available.
titletext, optionalA title for the file. At most 200 characters.
watermarkflag, default trueAdd the invisible watermark. Turn it off only if the file already carries ours, e.g. watermarked locally.
iptcflag, default truePictures: write the IPTC DigitalSourceType into the file's XMP metadata. Songs and clips state it in their manifest.
output_formattext, default keepA picture: keep, png, jpeg or webp. A song: keep, mp3, wav or m4a. A clip: keep or mp4 (always H.264 in MP4).
keep_input_credentialsflag, default trueKeep Content Credentials the file already carries (from an upstream model's API, say) as an ingredient, as the Code of Practice's Measure 1.2 asks.
inputfile, optionalFor edit tools: the user's own input picture (or, for a song or clip, an input song). If it carries Content Credentials, they're kept as an ingredient; nothing else of it is recorded. An input picture meets our floor, and with check on, your policy.
input_uploadtext, instead of inputThe input's upload ID, for an input over 4 MB (or a file and input over 4 MB together).
checktext, default offThe safety check before marking: ai-act, safe-only or off. On a block nothing is marked and you get 422 content_blocked with the reason. Each picture checked counts two marks; a clip is checked on 12 frames, two marks each. A song has no pictures: leave it off.

Options of later releases (metadata, webhook, ai_disclosure, categorize, describe, marking by url) are refused with 400 option_not_available rather than ignored, so you never believe a file got something it didn't.

Example

curl
curl https://api.kennmarq.com/v1/mark \
  -H "Authorization: Bearer $KENNMARQ_API_KEY" \
  -F file=@render.jpg \
  -F source=created \
  -F provider="Acme Inc." \
  -F model="FLUX.1 schnell" \
  -F model_version="1.0" \
  -F app_name="Acme Studio" \
  -F app_version="2.1" \
  -F output_format=webp \
  -D headers.txt \
  -o render.marked.webp
Node.js 20+
import { readFile, writeFile } from "node:fs/promises";

const form = new FormData();
form.append("file", new Blob([await readFile("render.png")], { type: "image/png" }), "render.png");
form.append("source", "created");
form.append("model", "FLUX.1 schnell");

const res = await fetch("https://api.kennmarq.com/v1/mark", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.KENNMARQ_API_KEY}` },
  body: form,
});
if (!res.ok) throw new Error((await res.json()).error.message);
await writeFile("render.marked.png", Buffer.from(await res.arrayBuffer()));
console.log(res.headers.get("x-mark-id"), res.headers.get("x-marks-used"));
Python (requests)
import os, requests

with open("render.png", "rb") as f:
    res = requests.post(
        "https://api.kennmarq.com/v1/mark",
        headers={"Authorization": f"Bearer {os.environ['KENNMARQ_API_KEY']}"},
        files={"file": ("render.png", f, "image/png")},
        data={"source": "created", "model": "FLUX.1 schnell"},
        timeout=60,
    )
res.raise_for_status()
open("render.marked.png", "wb").write(res.content)
print(res.headers["X-Mark-Id"], res.headers["X-Marks-Used"])

Response

200 with the marked file as the body, and these headers:

Response headers of POST /v1/mark
HeaderValue
Content-TypeThe marked file's type: image/png, image/jpeg, image/webp, audio/mpeg, audio/wav or audio/mp4.
Content-Dispositionattachment, with the file's name plus .marked and the right extension (image.marked.png for an uploaded picture).
X-Mark-IdThis mark's ID (mk_…). Quote it to support.
X-Watermark-IdThe ID carried by the watermark (hex). Absent when watermark=false.
X-Log-IndexWhere this file's signature sits in our public transparency log: GET /v1/log/proof/{index} proves it's there.
X-Marks-UsedMarks used this month in this key's environment, this one included.
X-Marks-LimitThe month's allowance for this key's environment.
X-Marks-CountedSongs: the marks this one counted, five per started minute.
X-Audio-WatermarkSongs: claimed (the mark reads back from the file, and its manifest says so), unclaimed (too quiet to read back: the file is still signed) or off.
X-Safety-Verdict, X-Safety-ReasonWith check on: allow or flag, and a flag's reason (sexual_realistic_face: get the person's consent).
X-Request-IdThis request's ID (req_…), also in every error body.
X-RateLimit-Limit, -Remaining, -ResetSee rate limits; Reset is a Unix time in seconds.

Songs

MP3, WAV or M4A, up to 15 minutes. The song comes back in the same call, in its own format (or the one output_format names), with its sample rate, channels and text tags; cover art isn't kept. Its sound carries Meta's AudioSeal mark, shaped under the music so no one hears it, and its manifest is signed and time-stamped like a picture's. AudioSeal's 16 bits say the file was marked by us, not which file: a song's credential travels in its manifest, and a song has no X-Watermark-Id.

The mark is read back from the encoded file: when it reads back, the manifest says the file carries it (X-Audio-Watermark: claimed). In a very quiet or silent recording there's nothing to hide it under; the file is then signed without that claim (unclaimed). A song longer than the marks you have left this month is refused before any work, with 402 quota_exceeded.

Clips

MP4 (or QuickTime), up to 4,500 frames (2.5 minutes at 30 frames a second, 75 seconds at 60, 5 minutes at 15), 1920x1080 either way round, 60 frames a second, 1 GB through an upload: what we can mark within a job's time. Your plan marks a few clips at a time (clips at a time). A clip is marked as a job: the call answers 202 at once with the mark's ID, and GET /v1/marks/{id} says when it's done, with a link to download it. Endpoints you add in the dashboard get mark.completed or mark.failed (webhooks).

202 application/json
{ "object": "mark", "id": "mk_…", "status": "running", "kind": "clip",
  "created_at": "2026-11-12T10:00:00.000Z", "url": "https://api.kennmarq.com/v1/marks/mk_…" }

Every frame carries Meta's Video Seal watermark, with a random ID like a picture's (watermark_id): it leads to the clip's durable credential when its metadata is stripped. Its sound carries AudioSeal, as a song does. The clip comes back as H.264 in MP4, upright (a phone's rotation is applied to the pixels), at the same size, frame times and sound, signed and time-stamped. Before it goes out, the watermark's ID is read back from 64 of its frames; a clip it doesn't read back from fails, as a picture does. Our safety floor reads 12 frames of every clip, and with check your policy too (two marks a frame); a blocked frame fails the job with the frame's answer in safety.

A clip's job reserves five marks (its first second, and its checks) when it starts, and counts the clip's other seconds when it's done. One longer than the marks you have left is refused before any work. A job that fails counts nothing, but a block's checks when you asked for them.

What we check before answering

We fail closed: a file goes back only if it was marked as asked. Before answering, the marker reads the signed file back and validates its manifest and time-stamp; then we check that the answer is for this mark, in this environment, in the format asked for, with the exact bytes the marker described. If anything is off you get 502 mark_failed and no file, and the mark isn't counted.

A mark is reserved when the request is accepted and given back if anything fails afterwards, so failed calls never count against your allowance. See rate limits and quotas.

The safety check

Every picture we mark goes through our own floor, whatever you send: explicit content with someone who appears to be a child is never marked, and the call answers 422 content_blocked. With check set, the file and the input are checked under your policy as well. A clip is read on 12 frames. A block answers with the check in safety, and picture says which one it was:

422 application/json
{
  "error": { "code": "content_blocked", "message": "The safety check blocked this picture, so nothing was marked. See \"safety\" for why.", "request_id": "req_…" },
  "safety": { "object": "safety_check", "policy": "ai-act", "verdict": "block", "reason": "sexual_apparent_minor",
              "nudity": { "label": "explicit", "score": 0.98 }, "apparent_minor": true, "realistic_face": true,
              "picture": "file", … }
}

A blocked call counts the checks it ran, not the mark. A block by our floor alone, with check off, counts nothing.

Errors

Errors of POST /v1/mark
StatusCodeWhen
400invalid_requestNo file, a field sent twice, or an option out of range.
400unknown_fieldA field this endpoint doesn't know: a typo never passes silently.
400option_not_availableAn option of a later release, such as metadata or webhook.
401missing_api_key, invalid_api_key, api_key_revokedSee authentication.
402quota_exceededThe month's marks or data allowance is used up, or a song needs more marks than are left (nothing is marked).
403insufficient_scope, ip_not_allowedThe key lacks the mark scope, or the address isn't allowlisted.
404upload_not_foundThe upload named isn't one this key can use: unknown, used, expired, or another key's.
413payload_too_largeThe request is over 4 MB (send larger files through an upload), or the file is over our limits: songs 15 minutes, clips 4,500 frames at up to 1920x1080.
415unsupported_media_typeNot a picture, song or clip we take, or not sent as multipart/form-data.
422content_blockedThe safety check blocked the file or the input. The body's safety says why; nothing was marked.
429rate_limitedToo many calls this minute (your organisation's keys together), too many calls failed today after we worked on them, or (a clip) your plan's clips at a time are running, or too many clips failed: retry after Retry-After seconds.
502mark_failedThe file couldn't be marked and verified; no file is returned. Nothing is charged.
503marker_unavailable, storage_unavailableThe marking service or uploads are down for a moment. Nothing is charged; retry.

Error bodies follow the common format.

Privacy

We record each mark's ID, watermark ID, SHA-256 and sizes, never the file, its name or its contents. A clip's marked file waits for you for a day, then is deleted. Marks say how a file was made, never who made it or the prompt. A safety check's answer is never written into the file's marks, and nothing of a blocked picture is kept. An upload is deleted as soon as the call has used it.