Local mode's signature
POSThttps://api.kennmarq.com/v1/sign
In local mode the watermark and the Content Credentials are made on your machine, and the file never leaves it. Only its manifest comes here, to be signed with our key and time-stamped by our server. The Python SDK and the ComfyUI nodes do all of this for you; this page is for building it yourself.
How it works
- Make the manifest and embed it with the C2PA library (c2pa-python 0.38.1), signed with a throwaway certificate chain you make for the purpose. C2PA checks every signature against its certificate, so it can't sign with a key it doesn't hold.
- Send us the manifest store it embedded: about 13 KB of claim and assertions, never pixels. Turn thumbnails off.
- We check it (below), sign the same claim with our key, time-stamp the signature, and send the store back, the same size, with our signature in place of the throwaway one.
- Put it back in your file where the old one was. The file's hard binding leaves the manifest out, so it still holds. Then read the file with our roots (GET /v1/certificate) before you use it: it should read Trusted, with a validated time-stamp.
curl
curl https://api.kennmarq.com/v1/sign \
-H "Authorization: Bearer $KENNMARQ_API_KEY" \
-H "Content-Type: application/c2pa" \
--data-binary @manifest.c2pa -D - -o signed.c2paWhat we sign, and what we don't
We sign only what a cloud mark of ours would carry, so our signature can't be lent to other claims:
| Check | In detail |
|---|---|
| Every assertion the claim lists is in the manifest, with the hash the claim gives it | and the manifest holds none it doesn't list |
| Only the assertions a mark carries | the actions, the hard binding, the watermark's soft binding, the generator, ingredients: no thumbnail, which would hold pixels |
| An AI source type | c2pa.created or c2pa.opened with trainedAlgorithmicMedia or compositeWithTrainedAlgorithmicMedia: we sign only AI-made files |
| A watermark comes with its action | c2pa.watermarked.bound and a TrustMark soft binding, both or neither |
| Our generator | the claim names Kennmarq.com as its generator, and its hash algorithm is SHA-256 |
Each manifest is signed once: make a new one for each file.
Response
200, application/c2pa: the signed store, with X-Mark-Id, X-Watermark-Id (when the manifest has a watermark), X-Log-Index, X-Marks-Used, X-Marks-Limit and X-Request-Id. Each signature counts one mark.
- The signature goes into our transparency log as a
c2pa.local_signatureentry. We never see your file, so the entry holds its hard binding (the hash in the manifest'sc2pa.hash.dataassertion) instead of its SHA-256. - With a watermark, the signed store is kept as the file's durable credential, without a file hash: a copy whose metadata was stripped still leads to it.
Credentials your file already carried travel with the manifest as ingredients, with any thumbnail their signer put in them. Leave them out if they shouldn't reach us.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | Not a C2PA manifest store, a manifest signed before, or one we won't sign; the message says which check failed. |
| 401 | missing_api_key, invalid_api_key, api_key_revoked | See authentication. |
| 402 | quota_exceeded | The month's marks are used up. |
| 403 | insufficient_scope, ip_not_allowed | The key lacks the sign scope, or the address isn't allowlisted. |
| 413 | payload_too_large | The store is over 1 MB. |
| 415 | unsupported_media_type | The body isn't sent as application/c2pa. |
| 429 | rate_limited | Too many calls this minute; retry after Retry-After seconds. |
| 502 | mark_failed | The signature couldn't be made and checked. Nothing is charged. |
| 503 | marker_unavailable, tsa_unavailable | The signing service or the time-stamp server is down for a moment. Nothing is charged; retry. |
Error bodies follow the common format.