Skip to content

Check a file

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

GEThttps://api.kennmarq.com/v1/credentials/{watermark_id}

GEThttps://api.kennmarq.com/v1/verify/key

What a picture's, song's or clip's C2PA manifest and invisible watermarks say, ours or anyone's; on request, as a result we sign. Free, no key needed. The checker page does the same for pictures in your browser, without uploading anything.

POST /v1/verify

Send the picture as multipart/form-data in file (PNG, JPEG or WebP; the request is at most 4 MB). Add signed=true for a signed result. Our servers in Zurich read the file in memory and keep nothing. A larger picture, up to 25 MB, goes through an upload named in upload, which takes the key that made it; or check it on the checker page, in your browser, at any size up to 25 MB. Songs (MP3, WAV, M4A) and clips (MP4) are read on our servers the same way, up to 25 MB, with a key (any of yours, free; it counts nothing): see songs and clips.

curl
curl https://api.kennmarq.com/v1/verify -F file=@picture.jpg -F signed=true
200 application/json
{
  "object": "check",
  "checked_at": "2026-11-12T10:00:00Z",
  "checker": { "name": "Kennmarq.com", "version": "0.4.0" },
  "file": { "type": "image/jpeg", "bytes": 482113, "width": 1024, "height": 768, "sha256": "…" },
  "c2pa": {
    "state": "Trusted",
    "manifest": "urn:c2pa:3fa85f64-…",
    "signer": { "common_name": "Kennmarq.com", "issuer": "Neuramare", "ours": true, "environment": "live" },
    "signed_at": "2026-11-12T09:58:31+00:00",
    "time_stamp": { "validated": true, "trusted": true },
    "claim_generator": "Kennmarq.com 0.4.0",
    "ai": { "digital_source_type": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", "generated": true },
    "soft_binding": { "alg": "com.adobe.trustmark.Q", "value": "1*0101…" },
    "failures": []
  },
  "watermark": { "found": true, "alg": "com.adobe.trustmark.Q", "schema": "BCH_5", "id": "1d455a804e9802a6", "matches_manifest": true },
  "durable_credential": {
    "watermark_id": "1d455a804e9802a6",
    "manifest": "urn:c2pa:3fa85f64-…",
    "environment": "live",
    "asset_sha256": "…",
    "same_file": true,
    "log_index": 1834001,
    "url": "https://api.kennmarq.com/v1/credentials/1d455a804e9802a6"
  },
  "log": { "leaf_index": 1834001, "status": "included", "proof_url": "https://api.kennmarq.com/v1/log/proof/1834001" },
  "signed": { "jws": "eyJhbGciOiJFUzI1NiIs…", "key_id": "…", "log_index": 1834530 }
}
  • c2pa is null when the file carries no manifest. state is Trusted when the signature chains to our roots (ours), Valid for another signer whose signature checks out, and Invalid when the file changed after signing; failures lists the codes.
  • watermark.id is set for watermarks in our schema (BCH_5); other TrustMark watermarks show as found with their schema when the picture's centre reads the very same one, so a picture without a mark doesn't show one by chance. Ours also reads in a picture saved on its side or upside down.
  • durable_credential: what the watermark leads to in our records, when we signed the file. When a platform strips the metadata, it's how the credential is found again; same_file says whether this is the very file we signed, and is null for a file marked in local mode, which never reached us.
  • log: the manifest's entry in our transparency log.
  • Songs and clips: file has seconds (and a clip its size); watermarks has the clip's Video Seal reading (id, read from 64 frames, and agreement, the share of its 256 bits that agree) and AudioSeal's (detected, the share of the sound that reads marked, and bits of 16); c2pa adds claims, the watermarks the manifest says the file carries, and a clip's watermark_record. watermark is then the clip's Video Seal, which leads to its durable credential as a picture's does, or the song's AudioSeal, which says the file was marked by us but carries no ID.

Signed results

With signed=true, signed.jws is a JWS (RFC 7515, compact) signed with ES256 by our checker key: its payload is the result above (the file, its manifest and watermark, the time), plus iss, iat and a unique jti; its header's kid names the key. Before it's returned it goes into our transparency log as a checker.result entry (the JWS's SHA-256, the file's, and the key ID), at the index in X-Log-Index. Any JWT library checks it with the key from GET /v1/verify/key.

Node.js (jose)
import { decodeProtectedHeader, importSPKI, jwtVerify } from "jose";

const { jws } = result.signed;
const { kid } = decodeProtectedHeader(jws);
const key = await (await fetch(`https://api.kennmarq.com/v1/verify/key?key_id=${kid}`)).json();
const { payload } = await jwtVerify(jws, await importSPKI(key.public_key, "ES256"), { issuer: "Kennmarq.com" });
// payload.file.sha256 is the SHA-256 of the file that was checked; compare it with yours.
console.log(payload.file.sha256, payload.checked_at);

GET /v1/credentials/{watermark_id}

The C2PA manifest store a watermark leads to, kept from the moment we signed the file: as JSON (the store base64 in manifest_store), or with ?format=c2pa as a .c2pa file any C2PA tool reads. Anyone who can read the watermark can fetch it, which means anyone who holds the picture.

curl
curl "https://api.kennmarq.com/v1/credentials/1d455a804e9802a6?format=c2pa" -o credential.c2pa

GET /v1/verify/key

The checker's public key: key_id (SHA-256 of its DER SubjectPublicKeyInfo), algorithm and public_key (PEM). With ?key_id= and a JWS's kid, the key that signed it, including keys we no longer sign with; without it, the newest.

A check says what a file carries, not who made it or whether it was made with AI: marks can be removed, and an unmarked file proves nothing. The signer and the AI disclosure are what the file's maker declared.

Errors

Errors of the checker endpoints
StatusCodeWhen
400invalid_requestNo file, a field sent twice, signed other than true or false, a picture that can't be read, or a malformed watermark ID or key_id.
400unknown_fieldA field other than file (or upload) and signed.
401missing_api_key, invalid_api_key, api_key_revokedAn upload was named without the key that made it.
400option_not_availableurl: checking a file by URL isn't available yet.
404not_foundNo credential under that watermark ID, or no checker key with that key_id.
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: check larger pictures on the checker page, or send files through an upload (up to 25 MB).
415unsupported_media_typeNot multipart, or not a picture, song or clip we read.
429rate_limitedOver 10 checks a minute or 200 a day from one address (an IPv6 address counts by its /64; lookups: 120 a minute), today's keyless checks are used up for everyone, or, with a key, your organisation's calls a minute.
503marker_unavailable, storage_unavailableThe checker (or uploads) is down, or a signed result didn't verify before it went out. Retry.