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 https://api.kennmarq.com/v1/verify -F file=@picture.jpg -F signed=true{
"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 }
}c2paisnullwhen the file carries no manifest.stateisTrustedwhen the signature chains to our roots (ours),Validfor another signer whose signature checks out, andInvalidwhen the file changed after signing;failureslists the codes.watermark.idis 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_filesays whether this is the very file we signed, and isnullfor a file marked in local mode, which never reached us.log: the manifest's entry in our transparency log.- Songs and clips:
filehasseconds(and a clip its size);watermarkshas the clip's Video Seal reading (id, read from 64 frames, andagreement, the share of its 256 bits that agree) and AudioSeal's (detected, the share of the sound that reads marked, andbitsof 16);c2paaddsclaims, the watermarks the manifest says the file carries, and a clip'swatermark_record.watermarkis 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.
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 "https://api.kennmarq.com/v1/credentials/1d455a804e9802a6?format=c2pa" -o credential.c2paGET /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.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | No 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. |
| 400 | unknown_field | A field other than file (or upload) and signed. |
| 401 | missing_api_key, invalid_api_key, api_key_revoked | An upload was named without the key that made it. |
| 400 | option_not_available | url: checking a file by URL isn't available yet. |
| 404 | not_found | No credential under that watermark ID, or no checker key with that key_id. |
| 404 | upload_not_found | The upload named isn't one this key can use: unknown, used, expired, or another key's. |
| 413 | payload_too_large | The request is over 4 MB: check larger pictures on the checker page, or send files through an upload (up to 25 MB). |
| 415 | unsupported_media_type | Not multipart, or not a picture, song or clip we read. |
| 429 | rate_limited | Over 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. |
| 503 | marker_unavailable, storage_unavailable | The checker (or uploads) is down, or a signed result didn't verify before it went out. Retry. |