# Kennmarq.com docs > All of Kennmarq.com's documentation, page after page, as Markdown. The index is https://kennmarq.com/llms.txt; any docs page is also at its address plus .md. --- Source: https://kennmarq.com/docs # Quickstart Five minutes from sign-up to your first marked image: a free key and one request. You get the same image back with an invisible watermark and signed, time-stamped Content Credentials, the marks the EU AI Act and California ask for. > Using a coding agent? Give Cursor or Claude Code [the prompt on our homepage](https://kennmarq.com/#agents); it reads these docs at [kennmarq.com/llms.txt](https://kennmarq.com/llms.txt). ## Base URL Every endpoint lives under `https://api.kennmarq.com`, over HTTPS only. Requests and errors are described in the [API reference](https://kennmarq.com/docs/api/mark). ## 1. Create an account [Sign up](https://kennmarq.com/signup) with your work email and confirm it with the link we send. Your organisation and a project called *Default* are created for you. Keys can only be created once your email is confirmed. ## 2. Create a test key In the dashboard, open **API keys** and create a key in the *Default* project, environment **Test**, scope `mark`. Copy it straight away: we store only a hash, so it's shown once. Keep it on your server, out of browsers and apps. ```bash export KENNMARQ_API_KEY="sk_test_…" ``` Test keys have their own monthly allowance (100 marks on Free, as many as live on paid plans), and sign with a test certificate. See [keys, scopes and environments](https://kennmarq.com/docs/keys). ## 3. Mark a picture Send a PNG, JPEG or WebP file as multipart form data, up to 4 MB in the request; a larger one, up to 25 MB, goes through an [upload](https://kennmarq.com/docs/api/uploads): ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@image.png \ -F source=created \ -F model="FLUX.1 schnell" \ -F app_name="Acme Studio" \ -D headers.txt \ -o image.marked.png ``` Add `-F provider="Acme Inc."` and `-F app_version="2.1"` and the mark carries all five items of [California's latent disclosure](https://kennmarq.com/docs/laws/california); add `-F markets=eu,us-ca` for a [receipt](https://kennmarq.com/docs/api/marks#receipts). Songs (MP3, WAV, M4A) go the same way. A clip (MP4) is marked as a job: the call answers `202` with its ID, and [GET /v1/marks/{id}](https://kennmarq.com/docs/api/marks) gives the marked clip when it's done ([clips](https://kennmarq.com/docs/api/mark#clips)). The response body is the marked file. `headers.txt` holds what you'll want to keep: ``` HTTP/2 200 content-type: image/png content-disposition: attachment; filename="image.marked.png" x-mark-id: mk_4Q8oT1n6mWc2fLr0aZ9eYb x-watermark-id: 1a2b3c4d5e6f7081 x-marks-used: 1 x-marks-limit: 1000 x-request-id: req_7Hc2kQm0Zt1L x-ratelimit-limit: 10 x-ratelimit-remaining: 9 x-ratelimit-reset: 1792663260 ``` ## 4. Check the marks Any C2PA reader, such as the open-source `c2patool`, shows the signed manifest: the AI source type, your model and app name, the watermark action and the time-stamp. ```bash c2patool image.marked.png ``` Or drop it on our [free checker](https://kennmarq.com/check), which reads both marks in your browser, and finds the Content Credentials of a copy whose metadata was stripped through its watermark. > Until our C2PA conformance and publicly trusted certificate are in place, readers report the signature as valid from an **unknown signer**, and our time-stamps as untrusted. Our certificate chains are public at [GET /v1/certificate](https://kennmarq.com/docs/api/certificate). ## 5. Go live Create a key in the **Live** environment for production and limit it to your servers with an IP allowlist. Live and test keys count against separate allowances, so testing never uses up production marks. Watch your usage in the dashboard, in the `X-Marks-Used` header, or with [GET /v1/usage](https://kennmarq.com/docs/api/usage). ## Next - [All options of POST /v1/mark](https://kennmarq.com/docs/api/mark): output format, IPTC, keeping input credentials. - [Errors](https://kennmarq.com/docs/errors) and [rate limits](https://kennmarq.com/docs/rate-limits). - [Our RFC 3161 time-stamp server](https://kennmarq.com/docs/api/tsa), for signing locally. --- Source: https://kennmarq.com/docs/authentication # Authentication Every request carries a secret API key in the `Authorization` header, as a bearer token. Keys belong to a project in your organisation, and each one is limited to what its scopes allow. ```bash curl https://api.kennmarq.com/v1/usage \ -H "Authorization: Bearer $KENNMARQ_API_KEY" ``` ## Key format A key is `sk_live_` or `sk_test_` followed by 43 letters and digits (256 random bits). The prefix tells you, and us, which environment it works in. The dashboard shows each key's first characters and last four, so you can tell keys apart without the secret. ## Keeping keys secret - Use keys from your servers only: never in a browser, a mobile app or a public repository. - We store a SHA-256 of each key and show the key once, when it's created. We can't show it again. - To rotate, create the new key, deploy it, then revoke the old one in the dashboard. A revoked key stops working at once. - Limit live keys to your servers' addresses with an [IP allowlist](https://kennmarq.com/docs/keys#ip-allowlists). - Every key created, changed or revoked is recorded in your organisation's audit log. ## Keys in URLs Only one endpoint takes its key in the URL: [POST /v1/tsa/{key}](https://kennmarq.com/docs/api/tsa), because the C2PA library that calls it can't send headers. A URL ends up in logs, so that endpoint accepts only keys whose single scope is `tsa`, and refuses any other key with `403 key_not_allowed_in_url`. If that happens, rotate the key you sent. ## Authentication errors | Status | Code | Meaning | | --- | --- | --- | | 401 | `missing_api_key` | No `Authorization: Bearer …` header. | | 401 | `invalid_api_key` | The key isn't one of ours, or is malformed. | | 401 | `api_key_revoked` | The key was revoked, or its project deleted. | | 403 | `insufficient_scope` | The key lacks the scope this endpoint needs. | | 403 | `ip_not_allowed` | The request came from an address outside the key's IP allowlist. | | 403 | `key_not_allowed_in_url` | A key with scopes beyond `tsa` was sent in a URL. | --- Source: https://kennmarq.com/docs/keys # Keys, scopes and environments Keys live in projects, work in one environment, and can do only what their scopes allow. Owners, admins and developers manage them in the dashboard; viewers can see them. ## Test and live | | Test | Live | | --- | --- | --- | | Key prefix | `sk_test_` | `sk_live_` | | Signing certificate | Our test chain | Our live chain | | Monthly allowance | Its own: 100 marks on Free, as many as live on paid plans | Your plan's | | Use for | Development, CI, trying options | Files you publish | Both environments run the same code and the same checks. Files marked with a test key are really marked, but signed with the test chain, so a checker tells them apart. ## Scopes Give each key the fewest scopes it needs. Scopes for endpoints still to come can already be set. | Scope | Allows | Available | | --- | --- | --- | | `mark` | [POST /v1/mark](https://kennmarq.com/docs/api/mark): marking files in the cloud | Now | | `tsa` | [POST /v1/tsa/{key}](https://kennmarq.com/docs/api/tsa): time-stamps for local mode. Alone on its key, the only kind of key allowed in a URL | Now | | `sign` | [POST /v1/sign](https://kennmarq.com/docs/api/sign): local mode's signature, for the SDK and the ComfyUI nodes | Now | | `check` | [POST /v1/check](https://kennmarq.com/docs/api/check): the safety check of a picture. The `check` option of POST /v1/mark needs only `mark` | Now | | `verify` | Nothing yet: [POST /v1/verify](https://kennmarq.com/docs/api/verify) and its signed results need no key | Reserved | [GET /v1/usage](https://kennmarq.com/docs/api/usage) works with any valid key. ## Projects Projects group keys, for example per app or per customer. Every organisation starts with a *Default* project. Deleting a project revokes its keys at once; its usage history stays. ## IP allowlists A key can be limited to IPv4 and IPv6 ranges in CIDR notation, such as `203.0.113.0/24` or `2001:db8::/32`. A single address is the same as a `/32` or `/128`. Up to 100 ranges per key; an empty list allows any address. Requests from elsewhere get `403 ip_not_allowed`. - We match the address your request reaches our edge from: behind NAT or a proxy, that's its public egress address. - An IPv4 address that arrives as IPv6 (`::ffff:192.0.2.1`) matches IPv4 ranges. - Ranges are stored in canonical form: host bits are cleared, so `10.1.2.3/8` becomes `10.0.0.0/8`. - Every change to a key's allowlist is recorded in the audit log, with the list before and after. ## Creating and revoking - Creating keys needs a confirmed email address. - The key is shown once, when created. We keep only its SHA-256, its first characters and its last four. - The dashboard shows when each key was last used (updated at most once a minute). - Revoking is immediate and can't be undone. > Lost a key? Revoke it and create a new one. No one, including us, can recover a key from what we store. --- Source: https://kennmarq.com/docs/laws # AI content laws The laws that ask for marks on AI-generated images, video and audio: when each applies, who it covers, what it asks, and what one Kennmarq.com call adds. In short - **In force:** the EU AI Act's Article 50 (2 December 2026 for systems already on the market), California's AI Transparency Act (since 30 September 2026), South Korea's AI Basic Act and India's IT Rules 2026. - **Next:** Utah from 1 January 2027, Washington from 1 February 2027. - **What they share:** provenance data in the file that's hard to remove; the EU and California also ask for a free way to check it. - **Kennmarq.com:** both, in [one API call](https://kennmarq.com/docs); name your markets and each file gets a receipt per law. > Checked on 24 October 2026. This page explains what Kennmarq.com does; it isn't legal advice. ## The laws | Law | When | Who | Asks for | Kennmarq.com | | --- | --- | --- | --- | --- | | [EU AI Act, Article 50(2)](https://kennmarq.com/docs/laws/eu-ai-act) | Since 2 August 2026; systems already on the market by 2 December 2026 | Providers of generative AI, wherever they're based, when the output is used in the EU | Machine-readable marks that make the output detectable as AI-generated | Both marks, and a free checker | | [EU Code of Practice on AI-generated content](https://kennmarq.com/docs/laws/eu-code-of-practice) | Final since 10 June 2026; voluntary | Providers who sign it (and deployers, for labels) | At least two marking techniques, a free detector, terms that forbid removing the marks | Both marks and the detector; a terms clause to copy | | [California AI Transparency Act](https://kennmarq.com/docs/laws/california) | Every public generative AI system since 30 September 2026 | Anyone whose generative AI system is publicly accessible in California | A latent disclosure with five items, and a way to verify it | All five items, and the checker as a verification tool | | [Washington, HB 1170](https://kennmarq.com/docs/laws/washington-utah) | From 1 February 2027 | Generative AI providers with over a million monthly users in Washington | Provenance data in the images, video and audio they make, hard to remove; a C2PA-style standard counts as compliant | C2PA provenance data and a watermark in every mark | | [Utah, HB 276](https://kennmarq.com/docs/laws/washington-utah) | From 1 January 2027 | Generative AI providers with over a million users, and large platforms | A latent disclosure with the time, the date and digital signatures; platforms detect and show it | Signed, time-stamped provenance data in every mark | | [South Korea, AI Basic Act](https://kennmarq.com/docs/laws/south-korea) | Since 22 January 2026; fines suspended for at least a year | Businesses offering generative AI in South Korea, foreign ones included | Users told in advance; output marked as AI-generated; deep fakes clearly recognisable | Both marks on every file, and the visible label in Korean for deep fakes | | [India, IT Rules 2026](https://kennmarq.com/docs/laws/india) | Since 20 February 2026 | Intermediaries offering tools that make or change synthetic content | A prominent visible label (a spoken notice for audio) and permanent metadata identifying the tool | The visible label on every picture and clip, and signed metadata naming your tool; songs not yet | ## One call, every market you name Send `markets=eu,us-ca` (or `us-wa`, `us-ut`, `us` for all three states, `kr` and `in`) and we refuse a call that lacks what those laws need, before anything is counted, and keep a [receipt](https://kennmarq.com/docs/api/marks#receipts) of what each file carries for each law. ## Switzerland Switzerland has no AI act yet: it signed the Council of Europe's AI Convention in 2025 and is drafting its rules. Swiss companies whose AI output reaches the EU fall under the AI Act today. Kennmarq.com is run from Baar, under Swiss law, with customer data in AWS Zurich. ## Visible labels Some rules ask people to see a label, not only machines: the EU's rule for deep fakes (Art. 50(4)), South Korea's and India's. Send `label=true`, or `deepfake=true`, and we draw the EU's "AI" icon into the picture or every frame of a clip, in English, Korean or Chinese, and the manifest records it ([labels](https://kennmarq.com/docs/api/mark#labels)). China and Vietnam ask for formats we don't add yet. --- Source: https://kennmarq.com/docs/laws/eu-ai-act # EU AI Act Article 50: marking AI-generated content If your app generates images, video or audio that people in the EU see, Article 50(2) asks you to mark every output in a machine-readable way: from 2 August 2026, or by 2 December 2026 if your system was already on the market. One Kennmarq.com call adds both layers the Code of Practice names. In short - **Who:** providers of generative AI systems, wherever they're based, when the output is used in the EU (Art. 2(1)(c)). - **When:** since 2 August 2026; systems already on the market by 2 December 2026. - **What:** machine-readable marks. The Code of Practice asks for at least two techniques (signed metadata and a watermark), a free detector, and terms that forbid removing the marks. - **Kennmarq.com:** both marks and the free checker in one call; a [terms clause to copy](#terms) is below. - **Fines:** up to EUR 15 million or 3% of worldwide turnover (Art. 99). > Checked on 24 October 2026. This page explains what Kennmarq.com does; it isn't legal advice. ## Who and when - **Providers** of generative AI systems, wherever they are based: the Act covers providers outside the EU when their system's output is used in the EU (Art. 2(1)(c)). - **Since 2 August 2026** for systems placed on the market from that day. Systems already on the market before it must mark by **2 December 2026** (Regulation (EU) 2026/1744). - **Fines** for breaching Article 50 reach EUR 15 million or 3% of worldwide annual turnover, whichever is higher; for small and medium companies, whichever is lower (Art. 99). ## What the Code of Practice asks, and what we add The Code of Practice on transparency of AI-generated content was finalised on 10 June 2026. Signing it is voluntary; the Commission found it adequate, so signatories are supervised mainly on whether they follow it. Each measure, and what we cover: [the Code, measure by measure](https://kennmarq.com/docs/laws/eu-code-of-practice). | The Code asks | Kennmarq.com | | --- | --- | | At least two machine-readable techniques, such as signed metadata and an imperceptible watermark | A C2PA manifest, signed and time-stamped, and an invisible watermark: TrustMark in pictures, Video Seal in clips, AudioSeal in songs | | A free way for anyone to detect the marks | The [checker](https://kennmarq.com/check) for pictures, in the browser, and [POST /v1/verify](https://kennmarq.com/docs/api/verify) | | Terms that forbid users to remove the marks | Yours: a clause to adapt is below | | Evidence that the marks hold up | Our [robustness report](https://kennmarq.com/docs/robustness) | Every picture and clip we return carries both layers, read back from the finished file before it leaves; if either fails, the call fails and the mark isn't counted. Songs always carry the credential, and the watermark whenever the sound can hold it. ## One call ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@render.png \ -F source=created \ -F provider="Acme Inc." \ -F app_name="Acme Studio" \ -F model="FLUX.1 schnell" \ -o render.marked.png ``` Add `-F markets=eu` to keep a [signed receipt](https://kennmarq.com/docs/api/marks#receipts) of what each file carries for the Act, and what stays with you. ## A terms clause to adapt The Code asks providers to forbid users to strip the marks. A starting point, to adapt with your counsel: > You may not remove, alter or obscure the watermarks, Content Credentials or other markings that identify outputs of the service as generated or altered by artificial intelligence. ## Deep fakes: the EU's visible label (Art. 50(4)) Deployers who publish deep fakes must disclose them so people can see it. The Code's Section 2 sets the label: the capitalised “AI” as its main element, with “GENERATED” or “MODIFIED” beside it, in a corner such as the top right, and on video at the start and at intervals, or throughout. Its Measure 1.4 encourages providers to offer their users that label. Send `deepfake=true` and we draw the label into the picture, or every frame of the clip, before the watermark, then sign it: “AI GENERATED” for `created`, “AI MODIFIED” for `composite` and `edited`. With `markets=eu` it can't be turned off, and the receipt lists it. `label=true` adds it to any picture or clip. See [visible labels](https://kennmarq.com/docs/api/mark#labels) for the sizes, corners and styles. ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@portrait.jpg \ -F source=edited \ -F markets=eu \ -F deepfake=true \ -o portrait.marked.jpg ``` Deep fakes in sound need a spoken notice, which we don't add yet: such a song is refused. ## What stays with you - Being the provider of your AI system. - Declaring the right source for each file: made by AI, or edited with it. - Deciding which files are deep fakes, and telling people when they talk to an AI system (Art. 50(1)). ## Sources - [The Commission's FAQ on signing the Code](https://digital-strategy.ec.europa.eu/en/faqs/signing-code-practice-transparency-ai-generated-content) - [Bird & Bird on the final Code](https://www.twobirds.com/en/insights/2026/taking-the-eu-ai-act-to-practice-the-final-transparency-code-of-practice) - [Future of Privacy Forum on the AI Omnibus timeline](https://fpf.org/blog/the-ai-act-implementation-timeline-what-changes-under-the-ai-omnibus/) --- Source: https://kennmarq.com/docs/laws/eu-code-of-practice # The EU Code of Practice on AI-generated content The Code of Practice on Transparency of AI-Generated Content turns Article 50 into concrete measures. For providers, it asks for two marking layers, a free detector, tested quality and documentation. Here is each measure, and what one Kennmarq.com call covers. In short - **What:** the Commission-backed code for Article 50: Section 1 for providers (four commitments, 17 measures), Section 2 for deployers' labels. - **Status:** voluntary. The Commission found it adequate on 8 July 2026: signatories are supervised mainly on whether they follow it; others must show other adequate means. - **Section 1's core:** signed, time-stamped metadata plus an imperceptible watermark, free detection, robustness tests, and interoperable watermark detection by 2 February 2027. - **Kennmarq.com:** both layers, free detection with signed results, input marks kept, and public documentation you may reuse. > Checked on 24 October 2026. This page explains what Kennmarq.com does; it isn't legal advice. ## Who it's for, and what signing does - **Providers** of generative AI systems sign Section 1; deployers who publish deepfakes, Section 2. Vendors of marking and detection tools may sign too, so their customers can rely on them. - **Signing is voluntary**, and following the Code isn't conclusive proof of compliance. But after the Commission's positive Opinion, signatories are supervised mainly on whether they follow it, and following it can lower a fine (Art. 99(7)). - **Who has signed:** about 190 organisations by the Commission's count, among them OpenAI, Google, Meta, Microsoft, Anthropic, Mistral and Black Forest Labs. - **Relying on a vendor** is allowed: the marks may be added by the provider, an upstream model or a specialised vendor, and the provider stays responsible (Measure 1.1). ## Section 1, measure by measure | Measure | The Code asks | Kennmarq.com | | --- | --- | --- | | 1.1.1 Signed metadata (required) | Record that the content is AI-generated or manipulated, signed and time-stamped so tampering shows; keep the keys secure; leave out sensitive data | [C2PA Content Credentials](https://kennmarq.com/docs/c2pa), signed with keys kept in AWS KMS and time-stamped (RFC 3161); nothing personal in them | | 1.1.2 Imperceptible watermark (required) | A mark that is hard to separate from the content, the robust complement to the metadata | [TrustMark, Video Seal or AudioSeal](https://kennmarq.com/docs/watermarks), read back from the finished file before it leaves | | 1.1.3 Fingerprinting or logging (optional) | A supplement to the two layers, never enough alone | Not needed for the two-layer rule | | 1.2(a) Keep the marks of inputs | Keep the metadata marks of content your system transforms, as far as feasible | Kept as an ingredient (`keep_input_credentials`, on by default) | | 1.2(b) Terms against removal | Your terms forbid deployers and others to remove or tamper with the marks | Yours: a [clause to adapt](https://kennmarq.com/docs/laws/eu-ai-act#terms) | | 1.3 Richer provenance (encouraged) | The system's name, the provider's company name and a time; the model and its version may be added | All of them: `provider`, `app_name`, `app_version`, `model`, `model_version`, and the signed time | | 1.4 Visible-label option (encouraged) | Let users add the EU's visible label when they generate | `label=true` draws the EU's icon from the Code's Annex 1 into the picture or every frame ([labels](https://kennmarq.com/docs/api/mark#labels)) | | 2.1 Free detection | A detector for every technique used, free of charge, open to the public when the public may see the content | Images: free and public at [kennmarq.com/check](https://kennmarq.com/check). Images, video and audio: [POST /v1/verify](https://kennmarq.com/docs/api/verify) (video and audio with a free key) | | 2.1.2 Signed results | On request, a signed result with the content's hash, the detector's identity and the time | [POST /v1/verify](https://kennmarq.com/docs/api/verify) with `signed=true` | | 2.1.3 Private detection | Uploads kept confidential and deleted right after the check | The checker reads images in the browser; /v1/verify reads in memory and keeps nothing | | 2.3 Clear, accessible results | Say which mark a result rests on, and include what the marks carry; accessibility (WCAG 2.1 AA encouraged) | The checker shows each mark separately, with what it says | | 3.2–3.3 Reliability and robustness | Measure error rates, and test the marks against edits, cropping, rescaling and attacks | Our [robustness report](https://kennmarq.com/docs/robustness): survival and false-reading rates per edit, for images | | 3.4 Interoperability | Established metadata standards from the start; watermark detection made interoperable by 2 February 2027 | C2PA from the start; TrustMark is on the C2PA's list of soft bindings | | 4.1–4.2 Documentation and testing | A documented process, and tests before launch and after; others' documentation and test results may be reused | Our docs, robustness report and changelog are public, for you to reference | The two-layer rule (Measure 1.1) applies to images, video and audio: no single technique meets the Code's requirements alone yet, so metadata and a watermark are both required. A video with sound needs its sound marked too, as every Kennmarq.com clip's is. ## Dates - **2 August 2026:** Article 50 applies. - **2 December 2026:** systems placed on the market before 2 August 2026 must mark their output (Art. 111(4), as amended by Regulation (EU) 2026/1744). - **2 February 2027:** watermark detection made interoperable (Measure 3.4(c)). ## What stays with you - Deciding whether to sign, and signing. - Your terms against removing the marks (Measure 1.2(b)). - Your documented process, training and cooperation with authorities (Commitment 4). - Deciding which files are deep fakes, and labelling those you publish (Section 2, Art. 50(4)): `deepfake=true` adds the label. Start with the [EU AI Act guide](https://kennmarq.com/docs/laws/eu-ai-act), or mark your first file in the [Quickstart](https://kennmarq.com/docs). ## Sources - [The Code of Practice on Transparency of AI-Generated Content (final, 10 June 2026)](https://ec.europa.eu/newsroom/dae/redirection/document/129555) - [Commission Opinion C(2026) 4839 on the Code's adequacy (8 July 2026)](https://ec.europa.eu/newsroom/dae/redirection/document/130913) - [Commission Guidelines on Article 50, C(2026) 5054 (20 July 2026)](https://ec.europa.eu/newsroom/dae/redirection/document/131215) - [How to sign the Code](https://digital-strategy.ec.europa.eu/en/library/how-sign-code-practice-transparency-ai-generated-content) - [The Code's signatories](https://digital-strategy.ec.europa.eu/en/news/strong-backing-code-practice-transparency-ai-generated-content) --- Source: https://kennmarq.com/docs/laws/california # California AI Transparency Act Since 30 September 2026, every generative AI system publicly available in California, whatever its size, must embed a latent disclosure in the images, video and audio it makes or alters. One call to `POST /v1/mark` embeds all five items, and anyone can check them in an image, free, at [kennmarq.com/check](https://kennmarq.com/check). In short - **Who:** anyone whose generative AI system is publicly accessible in California; SB 1000 removed the 1 million-user threshold. - **When:** since 30 September 2026. - **What:** a latent disclosure with the provider's name, the system's name and version, the time and date, a unique ID, and whether the content was created or altered; plus a way to verify it, which may be a third party's tool. - **Kennmarq.com:** all five in the signed credential when you send `provider`, `app_name` and `app_version`; our free checker shows them. - **Penalties:** up to USD 5,000 per violation, each day a new one. > Checked on 24 October 2026. This page explains what Kennmarq.com does; it isn't legal advice. ## Who it covers The Act is Chapter 25 of California's Business and Professions Code (SB 942, amended by AB 853 and SB 1000). SB 1000, signed and in force on 30 September 2026, removed the old threshold of one million monthly users. Anyone who produces a generative AI system that is publicly accessible in California is now a covered provider: startups, open-source developers and small SaaS products included. ## What the latent disclosure must carry A latent disclosure is machine-readable and travels with the file. Ours is the signed C2PA manifest, an industry standard, backed by an invisible watermark that leads back to it when platforms strip metadata. | Item | You send | Where it lives | Checker row | | --- | --- | --- | --- | | The provider's name | `provider`, e.g. Acme Inc. | `com.kennmarq.generator`, provider | Provider | | The AI system's name and version | `app_name` and `app_version` | The created action's software agent | AI system | | The time and date it was made or altered | Added by us | The created action's time, and our RFC 3161 time-stamp | Created at, Signed at | | A unique identifier | Added by us | The manifest's ID, and the watermark's ID in the pixels | Manifest ID, Watermark ID | | Whether it was created or altered | `source`: `created`, or `edited` | The IPTC digital source type | How it was made | ## One call ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@render.png \ -F source=created \ -F provider="Acme Inc." \ -F app_name="Acme Studio" \ -F app_version="2.1" \ -F model="FLUX.1 schnell" \ -o render.marked.png ``` The same fields work for songs and clips, in the [Python SDK](https://kennmarq.com/docs/sdk/python) (local mode too) and in the [ComfyUI nodes](https://kennmarq.com/docs/comfyui). Without `app_name`, the manifest names no AI system. Add `-F markets=us-ca` and we refuse a call that lacks the provider or the system's name and version, and keep a [signed receipt](https://kennmarq.com/docs/api/marks#receipts) of what the file carries. ## Letting people verify it The Act asks covered providers for a disclosure verification tool. Since SB 1000, they may meet that by directing people to a third party's tool. - **Pictures:** anyone can check one at [kennmarq.com/check](https://kennmarq.com/check). It shows all five items, and the picture never leaves their browser. - **Songs and clips:** [POST /v1/verify](https://kennmarq.com/docs/api/verify) reads them with an API key, so offer that check in your own app. Whether a tool meets the Act for your system is for you and your counsel to decide. ## Penalties Up to USD 5,000 per violation, each day counting as a separate violation, enforced by the Attorney General, city attorneys and county counsel. ## Coming next - **1 January 2027:** large online platforms, over 2 million monthly users, must detect provenance data and show whether content was made or altered by AI or captured by a device (AB 853, as rewritten by AB 2713). - **Washington and Utah:** HB 1170 (from 1 February 2027) and HB 276 (from 1 January 2027) ask large generative AI providers for similar provenance data. ## What stays with you - Being the covered provider of your AI system. - Sending an accurate provider, system and source with each file. - If you license your system to others, the Act's rules for licensees. ## Sources - [Latham & Watkins on SB 1000](https://www.lw.com/en/insights/newest-slate-of-ai-laws-keeps-california-at-the-forefront-of-us-regulation) - [WilmerHale on California's 2026 AI laws](https://www.wilmerhale.com/en/insights/blogs/wilmerhale-privacy-and-cybersecurity-law/20261002-california-finalizes-next-wave-of-state-ai-and-privacy-regulation) - [The Assembly's analysis of AB 2713](https://apcp.assembly.ca.gov/system/files/2026-04/ab-2713-wicks-apcp-analysis.pdf) --- Source: https://kennmarq.com/docs/laws/washington-utah # Washington and Utah Two states follow California in 2027: Utah from 1 January, Washington from 1 February. Both ask generative AI providers with over a million users to embed provenance data in the images, video and audio their systems make. Washington's law names the C2PA standard as a way to comply, and every Kennmarq.com mark is C2PA. In short - **Washington, HB 1170:** from 1 February 2027: provenance data, hard to remove; a C2PA-style standard is considered compliant. - **Utah, HB 276:** from 1 January 2027: a latent disclosure with the time, the date and digital signatures; large platforms must show it. - **Who:** providers whose systems have over a million users in the state. - **Kennmarq.com:** signed, time-stamped C2PA provenance data and a watermark in every mark. Send `markets=us-wa` or `us-ut` for a receipt. > Checked on 24 October 2026 against the enacted texts. This page explains what Kennmarq.com does; it isn't legal advice. ## The two laws | | Washington | Utah | | --- | --- | --- | | Law | E2SHB 1170, chapter 167, Laws of 2026 | H.B. 276, the Digital Content Provenance Standards Act (Utah Code 13-72c) | | Signed | 24 March 2026 | 24 March 2026 | | Applies from | 1 February 2027 | 1 January 2027; capture devices made for sale from 1 January 2028 | | Providers covered | Generative AI systems for images, audio or video with over 1,000,000 monthly users, open to the public in Washington | Generative AI systems with over 1,000,000 monthly visitors or users, open to the public in Utah | | Providers must | Include provenance data in images, video and audio the system creates or materially alters, as far as commercially and technically reasonable, hard to remove or tamper with, showing it came from the provider's system | Embed a latent disclosure in images, video and audio the system creates or substantially modifies, with the time and date and digital signatures that prove AI made or altered it, consistent with widely accepted industry standards | | Standards | A commonly supported standard for watermarking or metadata, "such as the coalition for content provenance and authenticity specification", is considered compliant (sec. 2(2)) | Widely accepted industry standards | | Platforms | — | Platforms with over 2,000,000 monthly users detect provenance data, let users inspect it, and don't strip it | | Enforced by | The Attorney General: up to USD 7,500 per violation (RCW 19.86.140) | The Division of Consumer Protection: up to USD 2,500 per violation | Washington leaves out minor edits (brightness, colour, filters, resizing, cropping, format changes, noise removal), business-to-business uses, and systems used only to upscale, denoise or compress. Utah leaves out systems used only inside a business. ## What one call adds - **Provenance data, signed:** [C2PA Content Credentials](https://kennmarq.com/docs/c2pa) saying the file was made or edited by AI, with your company, your AI system and its version, signed and time-stamped. That's the standard Washington names, and the time, date and digital signatures Utah asks for. - **Hard to remove:** an [invisible watermark](https://kennmarq.com/docs/watermarks) that leads back to the credential when platforms strip the metadata. - **Nothing personal:** marks say how a file was made, never who made it or with which prompt. - **A receipt:** name the market and each file gets a [receipt](https://kennmarq.com/docs/api/marks#receipts) of what it carries for that law. `us-wa` needs your provider, app name and version. ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@image.png -F source=created \ -F provider="Acme Inc." -F app_name="Acme Studio" -F app_version="2.1" \ -F markets=us \ -o image.marked.png ``` `markets=us` stands for California, Washington and Utah together. ## What stays with you - Being the provider, and knowing whether your user numbers bring you under each law. - Large platforms in Utah: detecting and showing provenance data on what users upload. ## Sources - [Washington: E2SHB 1170, chapter 167, Laws of 2026](https://lawfilesext.leg.wa.gov/biennium/2025-26/Pdf/Bills/Session%20Laws/House/1170-S2.SL.pdf) - [Washington: RCW 19.86.140, civil penalties](https://app.leg.wa.gov/RCW/default.aspx?cite=19.86.140) - [Utah: H.B. 276, enrolled copy](https://le.utah.gov/Session/2026/bills/enrolled/HB0276.pdf) - [Utah: the governor's list of signed bills, 24 March 2026](https://governor.utah.gov/press/gov-cox-signs-47-bills-in-the-2026-general-legislative-session/) --- Source: https://kennmarq.com/docs/laws/south-korea # South Korea's AI Basic Act Since 22 January 2026, businesses offering generative AI in South Korea must mark what it makes, and make deep fakes clearly recognisable. Every file Kennmarq.com marks carries machine-readable marks; send `deepfake=true` and it also carries a visible Korean label, AI 생성, drawn in and signed. In short - **Who:** businesses that offer generative AI products or services in South Korea, foreign ones included; some must name a domestic representative. - **When:** since 22 January 2026. Fines, up to KRW 30 million, are suspended for at least a year. - **What:** tell users in advance that the service runs on AI; mark its output as AI-generated, machine-readably if users are told so; make deep fakes clearly recognisable to people. - **Kennmarq.com:** a watermark and signed Content Credentials on every file; with `markets=kr` and `deepfake=true`, the visible label in Korean, which can't be turned off; a signed receipt. > Checked on 24 October 2026. This page explains what Kennmarq.com does; it isn't legal advice. ## What the Act asks The Framework Act on the Development of Artificial Intelligence and the Establishment of a Foundation for Trust, the AI Basic Act for short, took effect on 22 January 2026 with its Enforcement Decree. Its transparency article asks three things of a business offering generative AI: - tell users in advance that the product or service runs on AI; - mark its output as made by generative AI. The mark may be machine-readable, such as a watermark or metadata, if users are told the output carries one. The guidelines are stricter for output that leaves your service, downloaded or shared, than for output shown inside it; - for content that is hard to tell from reality, deep fakes, tell people in a way they clearly recognise, and for artistic work, in a way that doesn't spoil it. ## Marks on every file Every picture, song and clip we mark leaves your service carrying a signed, time-stamped C2PA manifest that says it was made with AI, and an invisible watermark that leads back to it when platforms strip the metadata. Anyone can check a picture at [kennmarq.com/check](https://kennmarq.com/check). ## Deep fakes: AI 생성 With `deepfake=true` we draw the visible label into the picture, or every frame of the clip, before the watermark, and sign it: AI 생성 (AI-generated) for `created`, AI 편집 (AI-edited) for `composite` and `edited`. With `markets=kr` it's in Korean by default and can't be turned off. The capitalised “AI” is the EU's icon too, so the same file serves both. ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@portrait.jpg \ -F source=edited \ -F markets=kr \ -F deepfake=true \ -o portrait.marked.jpg ``` Deep fakes in sound need a notice people can hear, which we don't add yet: such a song is refused. See [visible labels](https://kennmarq.com/docs/api/mark#labels) for sizes, corners and styles, and the [receipt](https://kennmarq.com/docs/api/marks#receipts) that lists them. ## What stays with you - Telling users in advance that your service runs on generative AI, and once that its output carries marks. - Deciding which files are deep fakes. - A domestic representative, if the Act requires one of you. ## Sources - [Cooley: South Korea's AI Basic Act, overview and key takeaways](https://www.cooley.com/news/insight/2026/2026-01-27-south-koreas-ai-basic-act-overview-and-key-takeaways) - [Stimson Center on the Act and its guidelines](https://www.stimson.org/2026/south-koreas-ai-basic-act-seeking-balance-between-industry-innovation-and-social-risk/) - [Decent Law: the AI Basic Act six months on](https://decentlaw.io/en/news/1180) --- Source: https://kennmarq.com/docs/laws/india # India's IT Rules 2026 Since 20 February 2026, intermediaries whose tools make or change pictures, video or audio must label that content prominently and embed permanent metadata that identifies it. Send `markets=in` and every picture and clip gets both: the visible “AI” label, and signed Content Credentials naming your tool. In short - **Who:** intermediaries that offer a tool for creating or changing synthetic content; large social media platforms have more duties. - **When:** since 20 February 2026. - **What:** a prominent visible label, or a spoken notice before synthetic audio; permanent metadata with a unique identifier of the tool that made it; nothing that lets users remove either. - **Kennmarq.com:** with `markets=in`, the visible label on every picture and clip, signed Content Credentials with unique IDs and your tool's name (`app_name`), a watermark, and a signed receipt. > Checked on 24 October 2026. This page explains what Kennmarq.com does; it isn't legal advice. ## What the rules ask The Information Technology (Intermediary Guidelines and Digital Media Ethics Code) Amendment Rules, 2026 (G.S.R. 120(E)), notified on 10 February 2026, cover “synthetically generated information”: content made or changed by a computer so that it looks real. An intermediary that offers a tool for it must: - label it prominently: visibly on pictures and video, with a spoken notice before audio; - embed permanent metadata or other provenance, as far as technically feasible, with a unique identifier of the tool; - not let users change, hide or remove the label or the metadata. The draft of October 2025 asked for a label covering 10% of the picture; the notified rules ask for a prominent one instead. ## What we add - **The label:** with `markets=in`, every picture and clip gets the visible “AI GENERATED” label (or “AI MODIFIED” for edits), drawn in before the watermark and signed, at size medium or large. It can't be turned off for India. See [visible labels](https://kennmarq.com/docs/api/mark#labels). - **The metadata:** the signed, time-stamped C2PA manifest carries the manifest's unique ID, your tool's name (`app_name`, required for India) and the time; the watermark in the pixels leads back to it when platforms strip the metadata. - **The record:** each file gets a [signed receipt](https://kennmarq.com/docs/api/marks#receipts) listing what it carries. ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@render.png \ -F source=created \ -F app_name="Acme Studio" \ -F markets=in \ -o render.marked.png ``` Songs need a spoken notice, which we don't add yet: a song with `markets=in` is refused. ## What stays with you - Being the intermediary that offers the tool. - Not letting your users remove or hide the label or the metadata, in your product and your terms. - Your other duties as an intermediary or platform: user declarations on large platforms, and takedown deadlines. ## Sources - [Chambers: the government notifies the IT Amendment Rules 2026](https://chambers.com/articles/government-notifies-information-technology-amendment-rules-2026) - [HLC: mandatory labelling for AI and 3-hour takedowns](https://www.hlc.com/en/publications/india-introduces-mandatory-labelling-for-ai-and-3hour-takedown-for-illegal-content) - [Nishith Desai Associates: unpacking India's amendments](https://www.nishithdesai.com/research-and-articles/hotline/technology-law-analysis/deepfakes-disclosure-and-due-diligence-unpacking-indias-sgi-amendments-15532) --- Source: https://kennmarq.com/docs/guides/ai-coding-agents # AI coding agents Building with Cursor, Claude Code, Windsurf or Codex? Give your agent one prompt, or a standing rule, and it marks every image, video and audio file your app generates for the EU AI Act and California. It reads our docs as plain text. In short - **The prompt:** below, ready to paste. - **Docs for agents:** [kennmarq.com/llms.txt](https://kennmarq.com/llms.txt) (the index), [llms-full.txt](https://kennmarq.com/llms-full.txt) (all of it), and any docs page as Markdown at its address plus `.md`. - **The key:** a free one from the dashboard, kept in `KENNMARQ_API_KEY` on the server. - **Checking:** drop a result on [kennmarq.com/check](https://kennmarq.com/check). ## 1. Get a key [Sign up](https://kennmarq.com/signup) for free, confirm your email, and create a key in the dashboard: a test key while you build, a live one for production. Put it in your server's environment: ```bash export KENNMARQ_API_KEY="sk_test_…" ``` ## 2. Give your agent the prompt ``` Mark every AI image, video and audio file this app generates with Kennmarq.com before it's stored or shown. The API docs are at https://kennmarq.com/llms.txt. Send each file to POST https://api.kennmarq.com/v1/mark with the key from the KENNMARQ_API_KEY environment variable, and pass provider, app_name, app_version, source and markets=eu,us-ca. For deep fakes, realistic depictions of real people, places or events, also send deepfake=true: it adds the visible "AI" label. Keep only the marked file. ``` ## 3. Or make it a standing rule A rule keeps the marks in place as your app grows: every new feature that makes images, video or audio gets them too. Add this to `AGENTS.md`, which many coding agents read, or to `CLAUDE.md` for Claude Code: ``` ## AI content marking (Kennmarq.com) - Every image, video or audio file this app generates with AI is marked before it's stored or shown: send it to POST https://api.kennmarq.com/v1/mark and keep only the file that comes back. - Call the API from server code only, with the key in the KENNMARQ_API_KEY environment variable. The key never reaches a browser or a mobile app. - Send provider (our legal name), app_name and app_version (this app), source (created, composite or edited) and markets=eu,us-ca; add kr or in when the app serves South Korea or India. - Deep fakes (realistic depictions of real people, places or events) also get deepfake=true, which draws the visible "AI" label into the picture or every frame of the video. label=true adds it to any picture or video. - If marking fails, the request fails: never show or store an unmarked file. - Files over 4 MB, and every video, go up first through POST https://api.kennmarq.com/v1/uploads. Videos are marked as jobs: 202, then GET the mark's url until it's completed. - Docs: https://kennmarq.com/llms.txt; all of them in one file: https://kennmarq.com/llms-full.txt. ``` For Cursor, as a project rule in `.cursor/rules/kennmarq.mdc`: ``` --- description: Mark AI-generated images, video and audio with Kennmarq.com alwaysApply: true --- - Every image, video or audio file this app generates with AI is marked before it's stored or shown: send it to POST https://api.kennmarq.com/v1/mark and keep only the file that comes back. - Call the API from server code only, with the key in the KENNMARQ_API_KEY environment variable. The key never reaches a browser or a mobile app. - Send provider (our legal name), app_name and app_version (this app), source (created, composite or edited) and markets=eu,us-ca; add kr or in when the app serves South Korea or India. - Deep fakes (realistic depictions of real people, places or events) also get deepfake=true, which draws the visible "AI" label into the picture or every frame of the video. label=true adds it to any picture or video. - If marking fails, the request fails: never show or store an unmarked file. - Files over 4 MB, and every video, go up first through POST https://api.kennmarq.com/v1/uploads. Videos are marked as jobs: 202, then GET the mark's url until it's completed. - Docs: https://kennmarq.com/llms.txt; all of them in one file: https://kennmarq.com/llms-full.txt. ``` ## What your agent should send | Field | What it is | | --- | --- | | `provider` | Your legal name, as the provider of the AI system. | | `app_name, app_version` | Your app: the AI system California's latent disclosure names. | | `source` | created (made by AI), edited (an existing file changed with AI) or composite. | | `markets` | eu,us-ca, or the markets you serve: a call missing what their laws need is refused, and each file gets a receipt. | | `model (optional)` | The model's name, e.g. FLUX.1 schnell. | ## 4. Check what it built - The key appears only in server code: search the project for `KENNMARQ_API_KEY`, and look for it in nothing a browser loads. - Every place that makes an image, a video or a song calls the API before storing or showing the file. - When marking fails, the request fails too: no unmarked file is shown or saved. - Files over 4 MB and videos go through [an upload](https://kennmarq.com/docs/api/uploads), and videos wait for their job ([GET /v1/marks/{id}](https://kennmarq.com/docs/api/marks)). - A marked image dropped on [the checker](https://kennmarq.com/check) shows your provider, your AI system and when it was made. For the code an agent might write, see the [Next.js](https://kennmarq.com/docs/guides/nextjs) and [Vercel AI SDK](https://kennmarq.com/docs/guides/vercel-ai-sdk) guides. --- Source: https://kennmarq.com/docs/guides/nextjs # Next.js Mark every AI file your Next.js app makes with one server-side helper. The key stays on the server, big files and videos are handled, and a file that can't be marked is never served. In short - **Where:** a route handler or a server action, never client code. - **What:** one helper, `lib/kennmarq.ts`, for images, songs and videos. - **Fields:** your company, your app and the markets you serve: California's five items, and a receipt per file. - **If it fails:** the request fails, and the unmarked original is never stored or shown. ## 1. The key Create a key in the dashboard ([Quickstart](https://kennmarq.com/docs)) and put it in your environment: on Vercel, in the project's environment variables; locally, in `.env.local`. ``` # .env.local: never NEXT_PUBLIC_, so it never reaches the browser KENNMARQ_API_KEY=sk_test_… ``` ## 2. The helper One function, `mark()`, for images, songs and videos. Files over 4 MB go up first through [an upload](https://kennmarq.com/docs/api/uploads), and a video is marked as a job it waits for. It needs nothing but `fetch`. ```ts // lib/kennmarq.ts: server code only (npm install server-only) import "server-only"; const API = "https://api.kennmarq.com/v1"; const auth = { Authorization: `Bearer ${process.env.KENNMARQ_API_KEY}` }; // Your company and your AI system: with them, every mark carries California's five items. const fields: Record = { source: "created", // or "edited", "composite" provider: "Acme Inc.", app_name: "Acme Studio", app_version: "2.1", markets: "eu,us-ca", }; async function failed(res: Response): Promise { const { error } = await res.json(); throw new Error(`Kennmarq.com ${res.status} ${error.code}: ${error.message}`); } /** The marked copy of an image, song or video. Throws rather than hand back the original. */ export async function mark(file: Blob, filename: string, extra: Record = {}): Promise { const form = new FormData(); for (const [k, v] of Object.entries({ ...fields, ...extra })) form.append(k, v); // A request carries at most 4 MB: a bigger file goes up first. if (file.size < 3_900_000) form.append("file", file, filename); else form.append("upload", await upload(file)); const res = await fetch(`${API}/mark`, { method: "POST", headers: auth, body: form }); if (res.status === 202) return finished(await res.json()); // a video: marked as a job if (!res.ok) return failed(res); return res.blob(); } async function upload(file: Blob): Promise { const res = await fetch(`${API}/uploads`, { method: "POST", headers: { ...auth, "Content-Type": "application/json" }, body: JSON.stringify({ bytes: file.size }), }); if (!res.ok) return failed(res); const { id, url } = await res.json(); // No key here: the URL is the permission. fetch sends the Content-Length it's signed for. const put = await fetch(url, { method: "PUT", body: file }); if (!put.ok) throw new Error(`Kennmarq.com upload: ${put.status}`); return id; } async function finished(job: { url: string }): Promise { for (;;) { await new Promise((r) => setTimeout(r, 5_000)); const res = await fetch(job.url, { headers: auth }); if (!res.ok) return failed(res); const m = await res.json(); if (m.status === "failed") throw new Error(`Kennmarq.com: ${m.error?.code}: ${m.error?.message}`); if (m.status === "completed") return (await fetch(m.download.url)).blob(); } } ``` ## 3. Mark what your model makes In a route handler, right after generating: ```ts // app/api/generate/route.ts import { mark } from "@/lib/kennmarq"; import { generate } from "@/lib/model"; // your model: Replicate, fal, OpenAI, your own ComfyUI… // Marking takes a second or two; leave room for a video, or for a slow first call. export const maxDuration = 60; export async function POST(request: Request) { const { prompt } = await request.json(); const image: Blob = await generate(prompt); const marked = await mark(image, "image.png", { model: "FLUX.1 schnell" }); // Store or return the marked copy only. If mark() threw, this request fails instead. return new Response(marked, { headers: { "Content-Type": marked.type || "image/png" } }); } ``` Or in a server action that stores the result: ```ts // app/actions.ts "use server"; import { put } from "@/lib/storage"; // S3, R2, Vercel Blob… import { mark } from "@/lib/kennmarq"; import { generate } from "@/lib/model"; export async function createImage(prompt: string) { const marked = await mark(await generate(prompt), "image.png"); return put(`images/${crypto.randomUUID()}.png`, marked); // never the unmarked original } ``` ## Videos A video is marked as a job: the call answers `202`, and the helper asks every five seconds until it's done, usually a minute or two. In production, let a [webhook](https://kennmarq.com/docs/webhooks) (`mark.completed`) tell you instead, and keep the request short. ## When a call fails - `429 rate_limited`: too many calls this minute for your plan; wait the `Retry-After` seconds. - `402 quota_exceeded`: this month's marks are used up; [upgrade](https://kennmarq.com/pricing) or wait for the 1st. - `400 invalid_request`: its message names what's missing; `markets=us-ca` needs `provider`, `app_name` and `app_version`. Every code is on the [errors](https://kennmarq.com/docs/errors) page. To see the result, drop a marked image on [the checker](https://kennmarq.com/check). Using the Vercel AI SDK? Its [guide](https://kennmarq.com/docs/guides/vercel-ai-sdk) uses the same helper. --- Source: https://kennmarq.com/docs/guides/vercel-ai-sdk # Vercel AI SDK The AI SDK's `generateImage` gives you the image's bytes; one more call marks them for the EU AI Act and California. It works with any image model the SDK reaches, through the AI Gateway or a provider package. In short - **Step:** after `generateImage`, pass each image to `mark()`, then use only what it returns. - **Helper:** the one from the [Next.js guide](https://kennmarq.com/docs/guides/nextjs#helper), in `lib/kennmarq.ts`. - **Models:** any, including FLUX, GPT Image, Imagen and Recraft; credentials a provider already added are kept. - **Written for:** AI SDK 7 (the ai package), checked on 24 October 2026. ## Generate, then mark ```ts // app/api/image/route.ts import { generateImage } from "ai"; import { mark } from "@/lib/kennmarq"; // the helper from the Next.js guide export const maxDuration = 60; export async function POST(request: Request) { const { prompt } = await request.json(); const { image } = await generateImage({ model: "bfl/flux-3-image", prompt }); const file = new Blob([image.uint8Array], { type: image.mediaType }); const marked = await mark(file, `image.${image.mediaType.split("/")[1]}`, { model: "FLUX.3" }); // Only the marked copy leaves this route. If mark() threw, the request fails instead. return new Response(marked, { headers: { "Content-Type": image.mediaType } }); } ``` The same two lines work in a server action, or wherever your server calls `generateImage`. The model here comes from the AI Gateway; with a provider package, pass its model instead. ## Several images at once ```ts const { images } = await generateImage({ model: "bfl/flux-3-image", prompt, n: 4 }); // One call each. One after another keeps a Free plan (10 calls a minute) well within its limit. const marked: Blob[] = []; for (const image of images) { marked.push(await mark(new Blob([image.uint8Array], { type: image.mediaType }), "image.png")); } ``` ## Images that already carry credentials Some providers sign their images with Content Credentials of their own. We keep those as an ingredient of ours, as the EU's Code of Practice asks, and add your company and app, the watermark and California's five items on top. ## Next - Videos and audio go through the same helper: see [videos](https://kennmarq.com/docs/guides/nextjs#video). - What each mark carries: [Content Credentials](https://kennmarq.com/docs/c2pa) and [invisible watermarks](https://kennmarq.com/docs/watermarks). - Let your coding agent do it: [the prompt](https://kennmarq.com/docs/guides/ai-coding-agents). --- Source: https://kennmarq.com/docs/c2pa # Content Credentials (C2PA) Content Credentials are a signed record, inside the file, of how it was made: the open standard of the Coalition for Content Provenance and Authenticity (C2PA), which Adobe, OpenAI, Google and camera makers use. Every Kennmarq.com mark adds one, signed and time-stamped, saying the file was made or edited by AI. In short - **What it is:** a manifest embedded in the file: claims about its origin, signed so any change shows. - **What ours says:** made or edited by AI, by which provider's AI system and model, when, and that a watermark was added. - **What it never says:** who made the file, or with which prompt. - **Laws:** the signed metadata layer the [EU AI Act's Code of Practice](https://kennmarq.com/docs/laws/eu-ai-act) names, and the carrier of [California's latent disclosure](https://kennmarq.com/docs/laws/california). ## What a Kennmarq.com credential carries | Part | What it says | | --- | --- | | `c2pa.actions.v2` | How the file came to be: `c2pa.created` (or `c2pa.opened` when it already had credentials), with the IPTC digital source type (`trainedAlgorithmicMedia` for made by AI, `compositeWithTrainedAlgorithmicMedia` for edited or combined with AI), the time, and your app's name and version as the software agent. Then the watermark: `c2pa.watermarked.bound` for an image's, `c2pa.watermarked.unbound` for a video's or a song's. | | `c2pa.soft-binding` | An image's watermark algorithm and ID, so a reader can find this credential from the pixels alone. | | `com.kennmarq.generator` | Your company (the provider), the model and its version, when you send them. | | `com.kennmarq.watermark` | A video's watermark ID. | | `Ingredients` | Credentials the file already carried, from a model provider's API for example, kept as they were. | | `Signature and time-stamp` | Our signature, with our [published certificate chain](https://kennmarq.com/docs/api/certificate), and an RFC 3161 time-stamp from our own server. | Images also carry IPTC's digital source type in their XMP metadata, the plain field many platforms read to label AI content. Every signature and time-stamp is in our public [transparency log](https://kennmarq.com/log) before the file reaches you. ## Reading one - In a browser: drop an image on [kennmarq.com/check](https://kennmarq.com/check). It reads the credential and the watermark, and finds the credential through the watermark when the metadata is gone. - On the command line, with the C2PA's open-source `c2patool`: ```bash c2patool image.marked.png ``` - Through the API, with a signed result: [POST /v1/verify](https://kennmarq.com/docs/api/verify). ## Why a watermark too Metadata is easy to lose: most platforms strip it when they re-encode an upload. The [invisible watermark](https://kennmarq.com/docs/watermarks) carries an ID in the content itself, and the ID leads back to the credential we keep for the file ([durable credentials](https://kennmarq.com/docs/api/verify#credentials)). That's why the EU's Code of Practice asks for both. ## Adding one ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@image.png -F source=created \ -F provider="Acme Inc." -F app_name="Acme Studio" -F app_version="2.1" \ -o image.marked.png ``` > Until our C2PA conformance and publicly trusted certificate are in place, C2PA readers report our signature as valid from an unknown signer. Our certificate chains are public at [GET /v1/certificate](https://kennmarq.com/docs/api/certificate). --- Source: https://kennmarq.com/docs/watermarks # Invisible watermarks An invisible watermark hides an ID in the content itself, so it stays when the metadata goes: through re-encoding, resizing and re-uploads. Kennmarq.com uses three open-source models, one for each kind of file, and reads every mark back before the file leaves. In short - **Images:** TrustMark, by Adobe. - **Video:** Video Seal, by Meta, in every frame. - **Audio:** AudioSeal, by Meta, under the music. - **Why:** it leads back to the file's [Content Credentials](https://kennmarq.com/docs/c2pa) when platforms strip them, as the EU's Code of Practice asks. ## What each one carries | File | Watermark | Carries | Checked | | --- | --- | --- | --- | | Images | TrustMark (variant Q), open source, by Adobe; on the C2PA's list of soft bindings | A random 61-bit ID that leads to the image's credential | All 78 test images after JPEG at quality 50, a social-network-style resize and a resize to 25% ([report](https://kennmarq.com/docs/robustness)) | | Video | Video Seal, open source, by Meta | The same kind of 61-bit ID, in every frame | Read back from 64 frames of every clip before it leaves | | Audio | AudioSeal, open source, by Meta | 16 bits that say the sound was marked by us; the song's credential travels in its manifest | Read back from the encoded file; shaped under the music, so no one hears it | ## What an image's watermark survives From our [robustness report](https://kennmarq.com/docs/robustness), measured on 78 AI images with the code the API runs: the ID read back after JPEG at quality 50 in 100% of them, after a social network's kind of upload (long side 1080 px, JPEG 85) in 100%, after a resize to 25% in 100%, after a 2-pixel blur in 100%, and after a crop to the centre 90% in 100%. Harder edits cost more: a crop to the centre 75% kept 53%, and a 5° rotation 41%. The report has every edit, with error bars, and how often unmarked images read as marked. ## Reading them - Images: [kennmarq.com/check](https://kennmarq.com/check), free, in the browser; it also reads quarter-turned images. - Images, video and audio: [POST /v1/verify](https://kennmarq.com/docs/api/verify) (video and audio with a key). ## What a watermark is not A watermark says a file was marked; it isn't an AI detector, and its absence doesn't prove a file is human-made. Very quiet recordings may not hold an audio mark: their credential then says so. --- Source: https://kennmarq.com/docs/guides/c2pa-yourself # Doing C2PA yourself Content Credentials and invisible watermarks are open standards and open-source models: you can run them yourself. Here is what that takes, piece by piece, so you can choose. In short - **Yourself:** a signing library, a certificate and its key, time-stamps, three watermark models, read-back, a manifest store, a public checker, and the laws to follow. - **With us:** one API call, free up to 1,000 marks a month. - **Either way:** the marks are open standards: anyone's tools can read them. ## The pieces | Piece | Yourself | Kennmarq.com | | --- | --- | --- | | A C2PA manifest | c2pa-rs, c2pa-python or c2pa-node, and a manifest design: actions, IPTC source types, your provider and system, ingredients | Done, with California's five items | | A signing certificate | A certificate chain readers accept, its private key in a hardware module or a cloud KMS, and renewals | Ours, in AWS KMS; Enterprise can sign as you | | Time-stamps | An RFC 3161 time-stamp authority you trust and pay per stamp, or run | Ours, every stamp in a public log | | Watermarks | TrustMark, Video Seal and AudioSeal running on your servers (CPU or GPU), their strength tuned, and every file read back | Done, read back before the file leaves | | Lost metadata | A store of manifests by watermark ID, and an endpoint to look them up | Done: [durable credentials](https://kennmarq.com/docs/api/verify#credentials) | | A free detector | A public page or API that reads both marks, which the EU's Code and California ask for | [kennmarq.com/check](https://kennmarq.com/check) and [POST /v1/verify](https://kennmarq.com/docs/api/verify) | | The laws | Following the EU, California, Washington, Utah and those to come, and changing your marks when they change | Receipts per law; we keep the formats current | | Proof | Robustness tests, logs, and answers for an auditor | [Published tests](https://kennmarq.com/docs/robustness) and a [transparency log](https://kennmarq.com/log) | ## When doing it yourself makes sense If you already run GPUs and a certificate authority, mark millions of files a day offline, or must keep every byte in your own network, running the open-source pieces can pay off. For that last case, our [local mode](https://kennmarq.com/docs/sdk/python#local) (coming soon) makes the watermark and the manifest on your machine and sends us only the manifest to sign, and Enterprise offers private deployment. ## Or one call ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@image.png -F source=created \ -F provider="Acme Inc." -F app_name="Acme Studio" -F app_version="2.1" \ -F markets=eu,us-ca \ -o image.marked.png ``` [Get a free key](https://kennmarq.com/signup), or read the [Quickstart](https://kennmarq.com/docs). --- Source: https://kennmarq.com/docs/api/mark # Mark a picture, song or clip POST `https://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}](https://kennmarq.com/docs/api/marks) 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`). | Field | Type | Description | | --- | --- | --- | | `file` | file, required (or upload) | The file: a PNG, JPEG or WebP picture; an MP3, WAV or M4A [song](#songs); or an MP4 [clip](#clips). 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](https://kennmarq.com/docs/api/uploads), named in `upload`: up to 25 MB, and a clip up to 1 GB. | | `upload` | text, instead of file | An 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. | | `source` | text, default created | How 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. | | `provider` | text, optional | Your 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](https://kennmarq.com/docs/laws/california). At most 200 characters. | | `model` | text, optional | The model's name, e.g. FLUX.1 schnell. At most 200 characters. | | `model_version` | text, optional | The model's version. At most 200 characters. | | `app_name` | text, optional | Your 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_version` | text, optional | Your AI system's version. Needs app_name. At most 200 characters. | | `markets` | text, optional | The markets the mark is made for, separated by commas: `eu`, `us-ca`, `us-wa`, `us-ut` (or `us` for all three states), `kr` and `in`. A call missing what those laws need is refused (`us-ca` and `us-wa` need `provider`, `app_name` and `app_version`; `in` needs `app_name`), and the mark gets a [signed receipt](https://kennmarq.com/docs/api/marks#receipts). `in` puts the [visible label](#labels) on every picture and clip. China and Vietnam, India's songs, and deep fakes in songs for `eu` and `kr` are refused with `option_not_available`: they need formats we don't add yet. | | `deepfake` | flag, default false | The file is a deep fake: it resembles real people, places or events and could pass as real (the AI Act's Art. 50(4)). It turns the [visible label](#labels) on; with `eu` or `kr` in `markets` it can't be turned off. Not for songs yet. | | `label` | flag | Pictures and clips: draw the EU's [visible AI label](#labels) into the file, before the watermark. Off by default; on for deep fakes and for `in`. | | `label_language` | text, default en | The word beside “AI”: `en` (AI GENERATED), `ko` (AI 생성) or `zh` (AI生成). `ko` by default with `kr` in `markets`. | | `label_position` | text, default top-right | `top-right`, `top-left`, `bottom-right` or `bottom-left`, where a viewer sees it (a photo's EXIF turn is followed). | | `label_size` | text, default medium | `small`, `medium` or `large`: 5, 7 or 10% of the shorter side, at least 20 pixels. `in` needs medium or large. | | `label_style` | text, default auto | `auto` (black on a bright corner, white on a dark one), `dark`, `light`, `dark-translucent` or `light-translucent`: the Code's four variants. | | `title` | text, optional | A title for the file. At most 200 characters. | | `watermark` | flag, default true | Add the invisible watermark. Turn it off only if the file already carries ours, e.g. watermarked locally. | | `iptc` | flag, default true | Pictures: write the IPTC DigitalSourceType into the file's XMP metadata. Songs and clips state it in their manifest. | | `output_format` | text, default keep | A 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_credentials` | flag, default true | Keep 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. | | `input` | file, optional | For 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_upload` | text, instead of input | The input's upload ID, for an input over 4 MB (or a file and input over 4 MB together). | | `check` | text, default off | The [safety check](https://kennmarq.com/docs/api/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. | Planned options (`metadata`, `webhook`, `ai_disclosure`, `categorize`, `describe`, marking by `url`) are refused with `400 option_not_available` rather than ignored, so a file never appears to carry something it doesn't. ## Example ```bash 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 ``` ```js 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 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: | Header | Value | | --- | --- | | `Content-Type` | The marked file's type: image/png, image/jpeg, image/webp, audio/mpeg, audio/wav or audio/mp4. | | `Content-Disposition` | attachment, with the file's name plus .marked and the right extension (image.marked.png for an uploaded picture). | | `X-Mark-Id` | This mark's ID (mk_…). Quote it to support. | | `X-Watermark-Id` | The ID carried by the watermark (hex). Absent when watermark=false. | | `X-Log-Index` | Where this file's signature sits in our public transparency log: GET /v1/log/proof/{index} proves it's there. | | `X-Marks-Used` | Marks used this month in this key's environment, this one included. | | `X-Marks-Limit` | The month's allowance for this key's environment. | | `X-Marks-Counted` | Songs: the marks this one counted, five per started minute. | | `X-Audio-Watermark` | Songs: 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-Reason` | With check on: allow or flag, and a flag's reason (sexual_realistic_face: get the person's consent). | | `X-Request-Id` | This request's ID (req_…), also in every error body. | | `X-RateLimit-Limit, -Remaining, -Reset` | See rate limits; Reset is a Unix time in seconds. | ## Visible labels With `label` on, we draw the EU icon of the Code of Practice on AI-generated content into the picture or every frame of the clip: the capitalised “AI”, and GENERATED for `created` or MODIFIED for `composite` and `edited`, as its Annex 1 shows. It goes in before the watermark, so the watermark and the signature cover it, and the manifest records it (a `c2pa.edited` action and a `com.kennmarq.label` assertion): [the checker](https://kennmarq.com/check) and `/v1/verify` show it. It sits in the top-right corner by default, where the Code's example puts it, sized from the shorter side, and on every frame of a clip, as the Code encourages. `auto` picks black or white from the pixels behind it, so it stays readable on any background. A picture too narrow for the pill gets the round “AI” icon. `deepfake=true` turns it on, and the markets decide when it must stay on: India always, the EU and South Korea for deep fakes. The receipt lists it for each market. ```bash curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@portrait.jpg \ -F source=edited \ -F app_name="Acme Studio" \ -F markets=eu,in \ -F deepfake=true \ -o portrait.marked.jpg ``` ### The same label in your own interface Next to a file, in a feed, or where a picture can't carry it, show the same label with our web component: one script, no requests, no cookies, and screen readers read it as “AI-generated content”. The Code asks for exactly that kind of disclosure beside artistic works, and South Korea for content shown inside your service. ``` ``` `kind`: `generated` or `modified`. `lang`: `en`, `ko` or `zh`. `theme`: `dark`, `light`, `dark-translucent` or `light-translucent`. `size`: its height in pixels. `href` makes it a link, for example to where the file can be checked. It's MIT-licensed: copy it into your app if you'd rather not load it from us. ## 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](https://kennmarq.com/docs/rate-limits#clips)). A clip is marked as a job: the call answers `202` at once with the mark's ID, and [GET /v1/marks/{id}](https://kennmarq.com/docs/api/marks) says when it's done, with a link to download it. Endpoints you add in the dashboard get `mark.completed` or `mark.failed` ([webhooks](https://kennmarq.com/docs/webhooks)). ```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](https://kennmarq.com/docs/rate-limits). ## 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](https://kennmarq.com/docs/api/check) as well. A clip is read on 12 frames. A block answers with the check in `safety`, and `picture` says which one it was: ```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 | Status | Code | When | | --- | --- | --- | | 400 | `invalid_request` | No file, a field sent twice, or an option out of range. | | 400 | `unknown_field` | A field this endpoint doesn't know: a typo never passes silently. | | 400 | `option_not_available` | A planned option, such as metadata or webhook, or a market whose rules need what we don't add yet (China, Vietnam, India's songs). | | 401 | `missing_api_key, invalid_api_key, api_key_revoked` | See authentication. | | 402 | `quota_exceeded` | The month's marks or data allowance is used up, or a song needs more marks than are left (nothing is marked). | | 403 | `insufficient_scope, ip_not_allowed` | The key lacks the mark scope, or the address isn't allowlisted. | | 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 (send larger files through an upload), or the file is over our limits: songs 15 minutes, clips 4,500 frames at up to 1920x1080. | | 415 | `unsupported_media_type` | Not a picture, song or clip we take, or not sent as multipart/form-data. | | 422 | `content_blocked` | The safety check blocked the file or the input. The body's safety says why; nothing was marked. | | 429 | `rate_limited` | Too 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. | | 502 | `mark_failed` | The file couldn't be marked and verified; no file is returned. Nothing is charged. | | 503 | `marker_unavailable, storage_unavailable` | The marking service or uploads are down for a moment. Nothing is charged; retry. | Error bodies follow the [common format](https://kennmarq.com/docs/errors). ## 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. --- Source: https://kennmarq.com/docs/api/marks # A mark's status GET `https://api.kennmarq.com/v1/marks/{id}` A clip is marked as a job ([POST /v1/mark](https://kennmarq.com/docs/api/mark#clips) answers `202` with its ID): ask here until it's done, then download the marked clip. Asking counts no marks. Needs a key with the `mark` scope, of the mark's organisation and environment. ## Example ```bash curl https://api.kennmarq.com/v1/marks/mk_… -H "Authorization: Bearer $KENNMARQ_API_KEY" ``` ```json { "object": "mark", "id": "mk_…", "status": "completed", "kind": "clip", "created_at": "2026-11-12T10:00:00.000Z", "finished_at": "2026-11-12T10:01:12.000Z", "file": { "type": "video/mp4", "bytes": 18734211, "sha256": "…", "seconds": 9.6, "width": 1920, "height": 1080, "frames": 230 }, "watermark_id": "1d455a804e9802a6", "watermarks": { "video": { "alg": "Meta Video Seal 1.0", "id": "1d455a804e9802a6", "read_back": 1, "frames": 64, "strength": 0.2 }, "audio": { "alg": "Meta AudioSeal (16 bits)", "claimed": true, "detected": 0.71, "bits": 16 } }, "manifest": "urn:c2pa:…", "log_index": 1234, "marks": 10, "safety": null, "download": { "url": "https://…", "expires_at": "2026-11-12T10:06:12.000Z" }, "download_until": "2026-11-13T10:01:12.000Z", "download_links_left": 4, "download_error": null } ``` While the job runs, the answer is `{"object": "mark", "id": "mk_…", "status": "running", …}`. Most clips take a minute or two; ask every few seconds, or let a [webhook](https://kennmarq.com/docs/webhooks) tell you. The download link needs no key: it is the permission, so share it with care. A picture or song was marked in its own call: here it is `completed`, with no download. ## Downloads A finished clip's file is kept for a day. Each link to it works for 5 minutes: start the download within them (a download that has started finishes). Asking again within those 5 minutes gives the same link's time; after them, a new link, up to 5 per clip. The file's size counts against your month's [data](https://kennmarq.com/pricing) once when the job finishes, which covers the first link, and again for each later link. Once the month's data is used up, no new link is made until the 1st (UTC) or a bigger plan. ## Fields | Field | Description | | --- | --- | | `status` | running, completed or failed. | | `kind` | picture, song or clip (an MP4 holding only sound comes back as a song). | | `file` | The marked file: type, bytes, sha256, seconds, and a clip's width, height and frames. | | `watermark_id` | A clip's Video Seal ID (16 hex digits), the key to its durable credential; null for a song. | | `watermarks` | video: the ID and how much of it read back; audio: whether the sound's mark was claimed. | | `manifest, log_index` | The manifest's label, and where its signature sits in the transparency log. | | `marks` | The marks it counted: five a second, and two per checked frame. | | `safety` | With check on: the policy and the verdict (allow or flag, with a flag's reason). | | `download` | url and expires_at: a link to the marked file that works for 5 minutes (see Downloads); null when there's none, with the reason in download_error. | | `download_until` | The file is kept a day after the job finished. | | `download_links_left` | How many more links the clip can get: 5 in all. | | `download_error` | Why download is null: gone (past its day), download_limit (its 5 links are used), quota_exceeded (the month's data is used up), storage_unavailable (retry). Otherwise null. | | `error` | A failed job's code and message (as an error body's), and safety for a blocked frame. | ## Receipts GET `https://api.kennmarq.com/v1/marks/{id}/receipt` A mark made with `markets` has a receipt: for each market, the law, what the file carries for it, anything it lacks, and what stays with you as the provider. We sign it with our checker key when you ask for it, so you can keep it as evidence; `GET /v1/verify/key` serves the same key. Marks made without markets, or in local mode, have none (`404 not_found`). ```bash curl https://api.kennmarq.com/v1/marks/mk_4Q8oT1n6mWc2fLr0aZ9eYb/receipt \ -H "Authorization: Bearer $KENNMARQ_API_KEY" ``` ``` { "receipt": { "object": "receipt", "mark_id": "mk_4Q8oT1n6mWc2fLr0aZ9eYb", "made_at": "2026-10-24T10:00:00Z", "file": { "type": "image/png", "sha256": "…" }, "marks": { "credential": { "manifest": "urn:c2pa:…", "time_stamp": { "serial": "…", "time": "2026-10-24T10:00:00Z" } }, "watermark": { "alg": "com.adobe.trustmark.Q", "id": "1a2b3c4d5e6f7081" }, "sound_watermark": null }, "origin": { "provider": "Acme Inc.", "system": "Acme Studio", "system_version": "2.1", "source": "created", … }, "markets": [ { "market": "us-ca", "law": "California AI Transparency Act …", "applied": ["a latent disclosure: …", …], "missing": [], "yours": ["being the covered provider", …] } ], "rules_checked": "2026-10-24", "guarantee": "…" }, "jws": "eyJhbGciOiJFUzI1NiIs…", "key_id": "…", "public_key": "-----BEGIN PUBLIC KEY-----…" } ``` ## When a job fails `status: failed`, with the error a call would have answered: `content_blocked` (a frame the safety check blocked, in `safety`, with its `frame`), `quota_exceeded` (the clip needs more marks than were left), `payload_too_large` (over our limits), `invalid_request` (a file we can't read), `mark_failed` or `marker_unavailable`. A failed job counts nothing, but a block's checks when you asked for them. After a failure on our side, the upload works again until its hour is up. Clips that fail with `mark_failed` after we worked on them have a daily cap ([clips](https://kennmarq.com/docs/rate-limits#clips)). > Errors of the call itself (a key without the scope, a mark of another organisation, a typo in the ID) follow the [common format](https://kennmarq.com/docs/errors): `401`, `403`, `404 not_found`, `429`. --- Source: https://kennmarq.com/docs/api/uploads # Large files POST `https://api.kennmarq.com/v1/uploads` A request to the API carries at most 4 MB. For a larger file, up to 25 MB (a clip to mark, up to 1 GB), upload it first, straight to our storage in Zurich, then name the upload instead of sending the file. The [Python SDK](https://kennmarq.com/docs/sdk/python) does this by itself. ## How it works 1. Ask for an upload with the file's size: `{"bytes": 12345678}`. 2. `PUT` the file to the `url` you get back, with its `headers`, within 15 minutes. 3. Name the upload in `upload` instead of `file` on [/v1/mark](https://kennmarq.com/docs/api/mark), [/v1/check](https://kennmarq.com/docs/api/check) or [/v1/verify](https://kennmarq.com/docs/api/verify), or in `input_upload` instead of `input` on /v1/mark. ```bash # 1. The upload, for the file's size curl https://api.kennmarq.com/v1/uploads \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"bytes\": $(wc -c < render.png)}" # → {"object":"upload","id":"up_…","url":"https://…","method":"PUT","headers":{"Content-Length":"…"},…} # 2. The file, to the url (no Authorization header: the URL is the permission) curl -X PUT --upload-file render.png "$UPLOAD_URL" # 3. The call, naming the upload curl https://api.kennmarq.com/v1/mark \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F upload=up_… -F source=created \ -o render.marked.png ``` ## Request `application/json` with one field, `bytes`: the file's size, from 1 byte to 1 GB (1,073,741,824 bytes). Each call holds the files it reads to its own limit: 25 MB (26,214,400 bytes), and 1 GB for a clip to mark; a call naming a larger upload answers `413 payload_too_large`. Needs a key with the `mark` or `check` scope. Asking for an upload counts toward the organisation's calls a minute, never toward its marks; a key may have 100 uploads waiting at once. ## Response `201`, `application/json`: | Field | Value | | --- | --- | | `object` | "upload" | | `id` | The upload's ID (up_…): name it in the call's upload field. | | `bytes` | The size you declared; the file must be exactly this long. | | `url` | Where to PUT the file: our storage in Zurich. The URL itself is the permission, so send no Authorization header. | | `method` | "PUT" | | `headers` | Headers the PUT must carry: Content-Length, which the URL is signed for. | | `url_expires_at` | The URL works for 15 minutes. | | `expires_at` | The upload can be used for an hour; then it's deleted unused. | ## The rules - An upload works once, within the hour, with the key that made it. Another key, a second call, or a later one gets `404 upload_not_found`. - The PUT must send exactly the declared size; anything else is refused. A call naming an upload whose file hasn't arrived yet gets `400 invalid_request`. - A call that fails for a reason of its own (a typo in an option, our service down for a moment) leaves the upload for the retry. A call that used it, or refused what it holds (not a file it takes, or blocked by the safety check), uses it up. A clip's job keeps its upload until the job is done; after a failure on our side it works again for its hour. - On `/v1/verify`, which needs no key for a file sent in the request, naming an upload takes the key that made it. > An upload's file is deleted as soon as the call has used it, or when it expires unused, an hour after it was made. Like every file you send us, it's used for that call only. ## Errors | Status | Code | When | | --- | --- | --- | | 400 | `invalid_request` | bytes is missing, not a whole number, or under 1. | | 400 | `unknown_field` | A field other than bytes. | | 401 | `missing_api_key, invalid_api_key, api_key_revoked` | See authentication. | | 403 | `insufficient_scope, ip_not_allowed` | The key has neither the mark nor the check scope, or the address isn't allowlisted. | | 413 | `payload_too_large` | bytes is over 1 GB. | | 415 | `unsupported_media_type` | The body isn't sent as application/json. | | 429 | `rate_limited` | Too many calls this minute, or 100 uploads already waiting for this key; retry after Retry-After seconds. | | 503 | `storage_unavailable` | Uploads are down for a moment. Retry. | Error bodies follow the [common format](https://kennmarq.com/docs/errors). --- Source: https://kennmarq.com/docs/api/check # The safety check POST `https://api.kennmarq.com/v1/check` From 2 December 2026 the AI Act bans AI systems that make sexual pictures of anyone appearing to be a child, or of a real person without their consent (Art. 5(1)(ba) and (bb)). The safety check screens a picture for both, as a block, flag or allow. It's a risk signal with published error rates, not a guarantee: you decide, and you stay responsible. ## Request A key with the `check` scope, and `multipart/form-data` with the picture in `file` (PNG, JPEG or WebP; the request is at most 4 MB, and a larger picture, up to 25 MB, goes through an [upload](https://kennmarq.com/docs/api/uploads) named in `upload`) and, optionally, `policy`: `ai-act` (the default) or `safe-only`. Each check counts two marks. To check while marking, send `check` to [POST /v1/mark](https://kennmarq.com/docs/api/mark#safety) instead. A clip (MP4, up to 25 MB here; check a larger one while marking it) is read on 12 frames spread over it, two marks each: the answer is its worst frame's (a block over a flag over an allow), with that frame's place in `frame` (`index` and `seconds`) and `frames`, how many were read. ```bash curl https://api.kennmarq.com/v1/check \ -H "Authorization: Bearer $KENNMARQ_API_KEY" \ -F file=@render.png -F policy=ai-act ``` ```json { "object": "safety_check", "checked_at": "2026-11-12T10:00:00Z", "checker": { "name": "Kennmarq.com", "version": "0.5.0" }, "file": { "type": "image/png", "bytes": 1482113, "width": 1024, "height": 1024, "sha256": "…" }, "policy": "ai-act", "verdict": "flag", "reason": "sexual_realistic_face", "nudity": { "label": "explicit", "score": 0.991 }, "apparent_minor": false, "realistic_face": true } ``` - `verdict`: `allow`, `flag` or `block`. A block is still a `200`: the check worked, and its answer is the block. - `reason`: `sexual_apparent_minor`, `sexual_realistic_face`, `explicit`, or `null` for an allow. - `nudity`: `none`, `suggestive` or `explicit`, and the classifier's score, 0 to 1. - `apparent_minor` and `realistic_face`: read only for a picture with nudity, and `null` otherwise. The check never estimates ages on their own, and never returns an age or an identity. ## Policies | Policy | Blocks | Flags | | --- | --- | --- | | `ai-act` | Sexual or suggestive content with an apparent minor (Art. 5(1)(bb)) | Explicit content with a realistic face: get the person's consent before you publish it (Art. 5(1)(ba)) | | `safe-only` | The same, and anything explicit | Nothing | | our floor | Explicit content with an apparent minor, on every `POST /v1/mark` whatever its `check`. You can't turn it off | Nothing | ## How a picture is read In steps, each only when the one before calls for it, so a picture without nudity costs one read. Nudity first: `explicit` when the classifier is sure, or fairly sure and the detector finds an exposed part; `suggestive` from a score of 0.3. Then, for a picture with nudity: is it a photo? For a photo, each face large enough to read (32 px or more) is read for apparent age, at stricter lines on an explicit picture than on a suggestive one. A drawing goes to the tagger instead, whose tags say when a drawn character is a child. | Read | Model | Licence | | --- | --- | --- | | Nudity | Falconsai's NSFW classifier (ViT-B/16), with NudeNet's 320n detector as a second opinion on borderline pictures | Apache-2.0; MIT | | Photo or drawing | Google's SigLIP (base, 224 px), zero-shot against photo and drawing descriptions | Apache-2.0 | | Faces | OpenCV Zoo's YuNet | MIT | | Apparent age, photos | Two models on six cuts of each face: SigLIP 2 fine-tuned on age bands, and Civitai's age-vit | Apache-2.0 | | Apparent age, drawings | SmilingWolf's wd-vit-tagger-v3 (its child tags) | Apache-2.0 | All open models, run on our servers in Zurich, pinned to the exact files we tested. ## Error rates Share of real photos whose face was read as a minor, measured in October 2026 with these models and lines on FairFace's validation photos (5,980 faces): | Age | Explicit picture | Suggestive picture | | --- | --- | --- | | 0–2 | 98.5% | 97% | | 3–9 | 92% | 85% | | 10–19 | 47% | 34% | | 20–29 | 5.9% | 2.3% | | 30 and over | 0.9% | 0.6% | Adults read as minors in explicit pictures are not spread evenly: in their twenties, women 8.0% and men 3.2%; Black and Southeast Asian faces about 12.5%, White faces 1.7%. FairFace is one model's own training set, so real pictures will do worse. On everyday pictures without nudity, the check raised no false alarm in 78 under either policy (the [robustness report](https://kennmarq.com/docs/robustness#safety)). Known limits: - a face it can't find (turned away, hidden, or under 32 px tall) gets no age read, so such a photo passes; - apparent age is a guess near 18, both ways: most older teens pass; - a near-photoreal 3D render counts as a photo; - the tagger reads anime-style drawings best; other styles may hide a drawn child. > The check is a risk signal that helps you meet the Art. 5 safeguards; it doesn't prevent abuse or detect abuse material. Keep your own review for flags, and for what any automated filter misses. ## Privacy The picture is read in memory and never kept: no copy, no score, no hash. The answer goes only to you, never into a file's marks. ## Errors | Status | Code | When | | --- | --- | --- | | 400 | `invalid_request` | No file, a field sent twice, a policy other than ai-act or safe-only, or a picture that can't be read. | | 400 | `unknown_field` | A field other than file (or upload) and policy. | | 400 | `option_not_available` | url, categorize or describe: not available yet. | | 401 | `missing_api_key, invalid_api_key, api_key_revoked` | See authentication. | | 402 | `quota_exceeded` | Fewer than two marks left this month (24 for a clip). | | 403 | `insufficient_scope, ip_not_allowed` | The key lacks the check scope, or the address isn't allowlisted. | | 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 (send larger files through an upload), or a clip over 25 MB or our limits. | | 415 | `unsupported_media_type` | Not a PNG, JPEG or WebP picture or an MP4 clip, or not sent as multipart/form-data. | | 429 | `rate_limited` | Too many calls this minute; retry after Retry-After seconds. | | 503 | `marker_unavailable, storage_unavailable` | The check (or uploads) can't run right now. It never skips: nothing is charged; retry. | Error bodies follow the [common format](https://kennmarq.com/docs/errors). --- Source: https://kennmarq.com/docs/api/sign # Local mode's signature POST `https://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](https://kennmarq.com/docs/sdk/python) and the [ComfyUI nodes](https://kennmarq.com/docs/comfyui) do all of this for you; this page is for building it yourself. ## How it works 1. 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. 2. Send us the manifest store it embedded: about 13 KB of claim and assertions, never pixels. Turn thumbnails off. 3. 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. 4. 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](https://kennmarq.com/docs/api/certificate)) before you use it: it should read Trusted, with a validated time-stamp. ```bash 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.c2pa ``` ## What we sign, and what we don't We sign only what a cloud mark of ours would carry, so our signature can't be attached 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](https://kennmarq.com/docs/api/log) as a `c2pa.local_signature` entry. We never see your file, so the entry holds its hard binding (the hash in the manifest's `c2pa.hash.data` assertion) instead of its SHA-256. - With a watermark, the signed store is kept as the file's [durable credential](https://kennmarq.com/docs/api/verify#credentials), 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](https://kennmarq.com/docs/errors). --- Source: https://kennmarq.com/docs/api/tsa # Time-stamps (RFC 3161) POST `https://api.kennmarq.com/v1/tsa/{key}` Our own RFC 3161 time-stamp server, for local mode: when your machine signs a C2PA manifest, it asks this URL to time-stamp the signature. The key sits in the URL because the C2PA library can't add headers. ## A tsa-only key Create a key whose **only** scope is `tsa` and put it in the URL. Any key with more scopes is refused here with `403 key_not_allowed_in_url`: URLs end up in logs, and a powerful key must never be one of them. If that happens, rotate the key you sent. ``` https://api.kennmarq.com/v1/tsa/sk_live_… ``` ## Request - `Content-Type: application/timestamp-query` - The body: a DER-encoded `TimeStampReq`, at most 4 KB. - Message imprints in SHA-256, SHA-384 or SHA-512. A SHA-1 imprint gets a rejection status inside the reply. - `certReq` is honoured (our TSA chain goes into the token) and your nonce is echoed. ## Response `200` with `Content-Type: application/timestamp-reply` exactly, and the DER `TimeStampResp` as the body, byte for byte as our server produced it. Each token states an accuracy of 1 second, carries a random 128-bit serial and our own policy OID, and is signed with ECDSA P-256 / SHA-256 including`signingCertificateV2` (RFC 5816). Every token goes into our public [transparency log](https://kennmarq.com/docs/api/log) before it's returned; the `X-Log-Index` header says where, and `GET /v1/log/proof/{index}` proves it. A rejection carries no token and no index. ## Example with OpenSSL ```bash # A query for a file's SHA-256, asking for our certificate in the reply openssl ts -query -data artwork.png -sha256 -cert -out request.tsq curl https://api.kennmarq.com/v1/tsa/$TSA_KEY \ -H "Content-Type: application/timestamp-query" \ --data-binary @request.tsq \ -o response.tsr openssl ts -reply -in response.tsr -text ``` To verify a token, fetch our TSA chain from [GET /v1/certificate?chain=tsa](https://kennmarq.com/docs/api/certificate). ## Counting Each time-stamp counts once in the month's time-stamp allowance, which is the same size as your plan's marks (and your test marks for test keys), kept apart for live and test keys. A request that fails on our side isn't counted. The plan's calls a minute apply to the organisation, all its keys together; the key's IP allowlist applies to the key. > C2PA validators report our time-stamps as `timeStamp.untrusted` until our publicly trusted certificate is in place; the file still validates. ## Errors | Status | Code | When | | --- | --- | --- | | 400 | `invalid_request` | An empty body, or a query our server couldn't parse. | | 401 | `missing_api_key, invalid_api_key, api_key_revoked` | No key, an unknown key, or a revoked one. | | 402 | `quota_exceeded` | The month's time-stamps are used up. | | 403 | `key_not_allowed_in_url` | The key has scopes beyond tsa. | | 403 | `ip_not_allowed` | The address isn't on the key's IP allowlist. | | 413 | `payload_too_large` | The query is over 4 KB. | | 415 | `unsupported_media_type` | The Content-Type isn't application/timestamp-query. | | 429 | `rate_limited` | Too many calls this minute. | | 503 | `tsa_unavailable` | The time-stamp server is down for a moment. Nothing is charged; retry. | --- Source: https://kennmarq.com/docs/api/certificate # Certificate chains GET `https://api.kennmarq.com/v1/certificate` Our public certificate chains, in PEM: the live and test signing chains, and the time-stamp server's. No key needed. ## All chains, as JSON ```bash curl https://api.kennmarq.com/v1/certificate ``` ```json { "object": "certificates", "signing": { "live": "-----BEGIN CERTIFICATE-----\n…", "test": "-----BEGIN CERTIFICATE-----\n…" }, "tsa": "-----BEGIN CERTIFICATE-----\n…" } ``` ## One chain, as a PEM file Add `?chain=live`, `?chain=test` or `?chain=tsa` for one chain as `application/x-pem-file`: ```bash curl "https://api.kennmarq.com/v1/certificate?chain=tsa" -o kennmarq.com-tsa-chain.pem ``` ## Caching Answers may be cached for 5 minutes (`Cache-Control: public, max-age=300`). Chains change rarely: when we rotate a certificate, the changelog says so in advance. > Our signing certificates come from our own certificate authority (`O=Neuramare, C=CH`) until C2PA conformance and a publicly trusted certificate are in place. Until then C2PA readers show signatures as valid from an unknown signer; these chains let you check them yourself. --- Source: https://kennmarq.com/docs/api/usage # Usage GET `https://api.kennmarq.com/v1/usage` This month's marks, data returned and time-stamps for the key's environment, with your plan's limits. Works with any valid key: a test key sees the test allowance. ```bash curl https://api.kennmarq.com/v1/usage -H "Authorization: Bearer $KENNMARQ_API_KEY" ``` ```json { "object": "usage", "plan": "free", "environment": "live", "period": { "id": "2026-10", "start": "2026-10-01T00:00:00.000Z", "end": "2026-11-01T00:00:00.000Z" }, "marks": { "used": 412, "limit": 1000, "ceiling": 1000 }, "data_out": { "bytes": 389214772, "limit_bytes": 1000000000 }, "time_stamps": { "used": 0, "limit": 1000 }, "rate_limit": { "calls_per_minute": 10 } } ``` | Field | Meaning | | --- | --- | | `plan` | Your organisation's plan: free, indie, startup, scale or enterprise. | | `environment` | The key's environment: live or test. Each has its own allowances. | | `period` | The calendar month counted, in UTC. Counts start again at its end. | | `marks` | Marks made this month, the month's allowance (limit), and how far they may run (ceiling): the allowance, or ten times it on a monthly paid plan, which bills the extras. | | `data_out` | Bytes of marked files returned this month, and the allowance (1 GB = 10⁹ bytes). | | `time_stamps` | Time-stamps from POST /v1/tsa this month, and their allowance. | | `rate_limit` | Calls a minute each key may make on your plan. | Usage calls count toward the key's calls per minute, but never against your marks. --- Source: https://kennmarq.com/docs/api/verify # Check a file POST `https://api.kennmarq.com/v1/verify` GET `https://api.kennmarq.com/v1/credentials/{watermark_id}` GET `https://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](https://kennmarq.com/check) 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](https://kennmarq.com/docs/api/uploads) named in `upload`, which takes the key that made it; or check it on the [checker page](https://kennmarq.com/check), 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](#media). ```bash curl https://api.kennmarq.com/v1/verify -F file=@picture.jpg -F signed=true ``` ```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](https://kennmarq.com/docs/api/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`. ```js 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. ```bash 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 | 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. | --- Source: https://kennmarq.com/docs/api/log # Transparency log GET `https://api.kennmarq.com/v1/log/root` GET `https://api.kennmarq.com/v1/log/proof/{entry}` GET `https://api.kennmarq.com/v1/log/consistency` GET `https://api.kennmarq.com/v1/log/entries` GET `https://api.kennmarq.com/v1/log/roots` GET `https://api.kennmarq.com/v1/log/key` Every signature and time-stamp we hand out is first appended to a public, append-only log. A root over the whole log is signed every hour while it grows, and at least once a day. With a root, its signature and our public key, anyone can check that an entry is in the log, and that the log was never rewritten. No key needed. The [transparency log page](https://kennmarq.com/log) shows the latest root. ## What's in an entry Every entry is a JSON object with `v` (1), `kind` and `logged_at` (UTC, to the second), plus the fields of its kind. They're all hashes, times and our own identifiers: never an organisation's name or ID, a file name, a watermark ID, a model or an app name. Whoever holds a file or a token can recompute its fields; nobody else learns anything from them. | kind | Records | Fields | | --- | --- | --- | | `c2pa.signature` | A file marked through POST /v1/mark | environment, manifest (its label), asset_sha256 (the file as we returned it), claim_signature_sha256 (the COSE signature: the CBOR in the manifest's c2pa.signature box), signer_sha256 (the signing certificate, DER), tsa_serial, tsa_time, tsa_token_sha256 (the time-stamp token in the signature, DER) | | `c2pa.local_signature` | A manifest signed through POST /v1/sign (local mode) | environment, manifest, binding_sha256 (the hash in the manifest's c2pa.hash.data assertion: we never see the file), claim_signature_sha256, signer_sha256, tsa_serial, tsa_time, tsa_token_sha256 | | `rfc3161.time_stamp` | A token from POST /v1/tsa/{key} | environment, tsa_serial, tsa_time, tsa_token_sha256 (the TimeStampToken, DER), imprint_alg and imprint (the hash you sent) | | `audit.head` | The head of an organisation's audit log, anchored when it moves | org (SHA-256 of "audit-org:" followed by the organisation's ID), seq and head (the audit chain's last entry and hash) | | `checker.result` | A signed result from POST /v1/verify | jws_sha256 (the compact JWS, as returned), file_sha256 (the checked file), key_id (the checker key: SHA-256 of its DER SubjectPublicKeyInfo) | The log is an RFC 9162 Merkle tree (the hashing of Certificate Transparency): an entry's leaf hash is SHA-256 of a 0x00 byte followed by the entry's canonical JSON (keys sorted, no whitespace; every value is ASCII). API answers carry that exact text as `entry_json`. Your `POST /v1/mark` and `POST /v1/tsa` responses name the entry they made in `X-Log-Index`. ## The latest root ```bash curl https://api.kennmarq.com/v1/log/root ``` ```json { "object": "log_root", "seq": 412, "tree_size": 1834467, "timestamp": 1794067200000, "signed_at": "2026-11-12T00:00:00.000Z", "root_hash": "9c8d5e0a4b7f…", "key_id": "5b2e7a19c0d4…", "signature": "MEUCIQDx…" } ``` `signature` is ECDSA P-256 with SHA-256 (DER, base64) over RFC 6962's tree head: the bytes `00 01`, then `timestamp` (milliseconds) and `tree_size` as 8-byte big-endian numbers, then the 32 bytes of `root_hash`. `GET /v1/log/roots` lists every root, newest first (`?before=` pages back, `?limit=` up to 100). ## Inclusion proof Name the entry by its index (`X-Log-Index`), its leaf hash, or, for a marked file, its manifest's label (`urn:c2pa:…`, readable with any C2PA tool). Add `?tree_size=` to prove it against a particular signed root; otherwise the latest one is used. ```bash curl https://api.kennmarq.com/v1/log/proof/urn:c2pa:3fa85f64-5717-4562-b3fc-2c963f66afa6 ``` ```json { "object": "log_proof", "status": "included", "leaf_index": 1834001, "leaf_hash": "d1f0…", "entry": { "v": 1, "kind": "c2pa.signature", "manifest": "urn:c2pa:3fa85f64-…", "asset_sha256": "…", … }, "entry_json": "{\"asset_sha256\":\"…\",…,\"v\":1}", "tree_size": 1834467, "audit_path": ["7a3c…", "0e91…", …], "root": { "object": "log_root", "seq": 412, … } } ``` An entry newer than the latest root answers `"status": "pending"`, with the entry and no proof yet: the next root covers it within the hour. ## Consistency proof Proof that the tree of `second` entries starts with the tree of `first`: nothing was removed, changed or reordered in between. Both must be sizes of signed roots; `second` defaults to the latest. Keep the roots you've seen and check each new one against them. ```bash curl "https://api.kennmarq.com/v1/log/consistency?first=1790012&second=1834467" ``` ```json { "object": "log_consistency", "first": { "object": "log_root", "tree_size": 1790012, … }, "second": { "object": "log_root", "tree_size": 1834467, … }, "proof": ["44be…", "c07d…", …] } ``` ## Every entry For auditors who rebuild the tree themselves: `?start=` and an optional `?end=` (exclusive), up to 1,000 entries a call, oldest first, each with `leaf_index`, `leaf_hash`, `entry` and `entry_json`. ```bash curl "https://api.kennmarq.com/v1/log/entries?start=0&end=1000" ``` ## The public key ```bash curl "https://api.kennmarq.com/v1/log/key?format=pem" -o kennmarq.com-log-key.pem ``` Without `format=pem` it answers JSON: `key_id` (SHA-256 of the key's DER SubjectPublicKeyInfo, as each root names it), `algorithm` and `public_key`. An older key stays available by `?key_id=`. ## Checking it yourself The root's signature, with OpenSSL (and jq): ```bash curl -s "https://api.kennmarq.com/v1/log/key?format=pem" -o log-key.pem curl -s "https://api.kennmarq.com/v1/log/root" -o root.json printf '0001%016x%016x%s' "$(jq .timestamp root.json)" "$(jq .tree_size root.json)" \ "$(jq -r .root_hash root.json)" | xxd -r -p > tree-head.bin jq -r .signature root.json | base64 --decode > signature.der openssl dgst -sha256 -verify log-key.pem -signature signature.der tree-head.bin # Verified OK ``` An inclusion proof (RFC 9162 §2.1.3.2), in Python: ```python import hashlib, json, urllib.request proof = json.load(urllib.request.urlopen("https://api.kennmarq.com/v1/log/proof/urn:c2pa:3fa85f64-5717-4562-b3fc-2c963f66afa6")) sha256 = lambda data: hashlib.sha256(data).digest() r = sha256(b"\x00" + proof["entry_json"].encode()) assert r.hex() == proof["leaf_hash"] fn, sn = proof["leaf_index"], proof["tree_size"] - 1 for step in map(bytes.fromhex, proof["audit_path"]): assert sn > 0 if fn % 2 == 1 or fn == sn: r = sha256(b"\x01" + step + r) while fn % 2 == 0 and fn != 0: fn, sn = fn >> 1, sn >> 1 else: r = sha256(b"\x01" + r + step) fn, sn = fn >> 1, sn >> 1 assert sn == 0 and r.hex() == proof["root"]["root_hash"] print("in the log, under root", proof["root"]["seq"]) ``` Then check that root's signature as above, and that the entry's fields match your file: `asset_sha256` is the SHA-256 of the file we returned, and `manifest` its manifest's label. For a time-stamp, `tsa_token_sha256` is the SHA-256 of the token in your reply. ## Limits and caching 120 calls a minute per address, with the usual `X-RateLimit-*` headers. Answers about the latest root may be cached for a minute; answers about a tree size you named never change and may be cached for a day. > Our time-stamps come from our own RFC 3161 server, so every one of them is in this log, where none can be backdated or removed without the roots disagreeing. ## Errors | Status | Code | When | | --- | --- | --- | | 400 | `invalid_request` | A parameter isn't a whole number, a tree size isn't one of our signed roots, or an entry name is malformed. | | 404 | `not_found` | No such entry or key, or no root signed yet. | | 429 | `rate_limited` | Over 120 calls a minute from one address. Retry-After says when to retry. | | 500 | `internal_error` | Something failed on our side, including a proof that didn't verify before it was sent. Retry. | --- Source: https://kennmarq.com/docs/sdk/python # Python SDK Mark AI images, songs and clips from Python in one line: in the cloud, or images on your own machine, where they never leave it. The safety check and verification are included. MIT-licensed; the cloud client needs no dependencies. > Coming to PyPI soon. ## Install ```bash pip install kennmarq # the cloud API: no dependencies pip install "kennmarq[local]" # local mode pip install "kennmarq[safety]" # the safety check on your machine too ``` The SDK reads your key from `KENNMARQ_API_KEY`, or takes `api_key=`. Python 3.10 or later. ## In the cloud ```python from kennmarq import Kennmarq k = Kennmarq() result = k.mark("render.png", "render.marked.png", source="created", model="FLUX.1 schnell") print(result.mark_id, result.watermark_id, result.marks_used) ``` The picture goes to [POST /v1/mark](https://kennmarq.com/docs/api/mark): our servers in Zurich mark it in memory and keep nothing. A file over 4 MB, up to 25 MB, goes up first through [POST /v1/uploads](https://kennmarq.com/docs/api/uploads), by itself; `check()` and `verify()` do the same (an upload takes a key). ## Songs and clips ```python song = k.mark("track.mp3", "track.marked.mp3", model="Song Model 2") print(song.marks_counted, song.audio_watermark) # 15 (five per started minute), "claimed" clip = k.mark("clip.mp4", "clip.marked.mp4", check="ai-act") # waits for the job, then downloads print(clip.watermark_id, clip.marks_counted) job = k.mark("long.mp4", wait=False) # or start it now… clip = k.wait_for(job.mark_id, "long.marked.mp4") # …and fetch it later ``` `mark()` takes MP3, WAV and M4A songs and MP4 clips too, in the cloud: a clip is a job, which `mark()` waits for (up to 40 minutes) before downloading the marked clip; a clip up to 1 GB goes up first by itself. A blocked frame raises `ContentBlocked`, with the frame in `safety`. Local mode marks pictures only, for now. `mark(..., label=True)` draws the EU's [visible label](https://kennmarq.com/docs/api/mark#labels) (“AI GENERATED”) into a picture or every frame of a clip; `deepfake=True` turns it on, and `label_language`, `label_position`, `label_size` and `label_style` shape it. Labels are drawn in the cloud: `mark_file()` doesn't draw them yet. ## On your machine ```python result = k.mark_file("render.png", "render.marked.png", source="created", model="FLUX.1 schnell") assert result.mode == "local" ``` - The watermark and the Content Credentials are made on your machine. Only the manifest goes to [POST /v1/sign](https://kennmarq.com/docs/api/sign), about 13 KB and never pixels, to be signed and time-stamped. Each file counts one mark. - Before writing anything, the SDK reads the file with our roots: it must read Trusted, with a validated time-stamp and its watermark in place. Otherwise it raises `InvalidSignedFile` and writes nothing. - Credentials the file already carries are kept as an ingredient, as the Code of Practice asks, and travel with the manifest; `keep_input_credentials=False` keeps none. - The watermark's model (65 MB) downloads once, to `~/.cache/kennmarq/models` or `KENNMARQ_MODELS_DIR`, checked against its pinned SHA-256. - Where the C2PA library has no build (Windows, for now), the watermark is made on your machine and the manifest in the cloud: the watermarked picture is sent to `POST /v1/mark` (`mode == "hybrid"`). - Without a key, `watermark_file()` writes the watermark and the IPTC AI label alone, with no Content Credentials. ## The safety check ```python answer = k.check("render.png", policy="ai-act") # in the cloud: two marks answer = k.check("render.png", policy="ai-act", local=True) # on your machine: nothing sent, nothing counted result = k.mark_file("render.png", "out.png", check="ai-act") # a block raises ContentBlocked ``` The same check as [POST /v1/check](https://kennmarq.com/docs/api/check), with the same models and the same error rates. On your machine its models download once, about 2.3 GB. ## Verify a file ```python Kennmarq().verify("render.marked.png") # free, no key needed ``` ## Errors API errors raise `KennmarqError`, with the API's `code`, `status` and `request_id` ([errors](https://kennmarq.com/docs/errors)). `ContentBlocked` carries the check's answer in `safety`; `MissingApiKey`, `LocalModeUnavailable` and `InvalidSignedFile` say what's missing. --- Source: https://kennmarq.com/docs/comfyui # ComfyUI nodes Swap Save Image for Kennmarq.com Mark & Save Image and every picture leaves ComfyUI marked for the EU AI Act and California: an invisible watermark and signed, time-stamped Content Credentials, made on your machine. Only the manifest comes to us to be signed, never pixels. A free key marks 1,000 files a month. > Coming to the ComfyUI Registry soon. ## Set up 1. Install them from ComfyUI-Manager (search "Kennmarq.com"). 2. Create a key in the dashboard (a free one marks 1,000 files a month) with the `sign` scope, and start ComfyUI with it in `KENNMARQ_API_KEY`. The key is never a widget: a widget's value is saved in the workflow, and the workflow goes into every PNG you share. Without a key, the node still writes the watermark and the IPTC AI label, and says how to add Content Credentials. ## Kennmarq.com Mark & Save Image | Option | What it does | | --- | --- | | `format` | PNG, JPEG or WebP. | | `source` | `auto` reads the graph: a Load Image node means the picture was made from another one (`composite`), otherwise `created`. Or say `edited`. | | `keep_workflow` | Keeps ComfyUI's prompt and workflow in a PNG, as Save Image does, inside the signed file. They reveal your prompt: turn it off to leave them out. | | `safety_check` | ai-act or safe-only runs the safety check before saving; a blocked picture isn't written. | | `provider, model, app_name, app_version` | Recorded in the Content Credentials: your legal name, the model, and your AI system's name and version (California's latent disclosure). | It runs the [Python SDK's local mode](https://kennmarq.com/docs/sdk/python#local): each picture counts one mark, and the file is checked against our roots before it's written. On Windows, where the C2PA library has no build yet, the watermark is made on your machine and the manifest in the cloud. ## Kennmarq.com Mark & Save Audio and Mark & Save Video **Mark & Save Audio** takes an AUDIO and saves it as MP3, M4A or WAV with our inaudible AudioSeal mark and signed Content Credentials. **Mark & Save Video** takes frames (an IMAGE batch), their frame rate and an optional AUDIO, and saves an MP4 with Video Seal in every frame (and AudioSeal in its sound); its `safety_check` reads 12 frames, and a blocked frame leaves the clip unsaved. Both send the file to [POST /v1/mark](https://kennmarq.com/docs/api/mark#songs): songs and clips are marked in the cloud, so they need `KENNMARQ_API_KEY`. A song counts five marks per started minute, a clip five per started second; the video node waits for the clip's job. On the video node, `visible_label` (or `deepfake`) draws the EU's [visible label](https://kennmarq.com/docs/api/mark#labels), “AI GENERATED”, on every frame, in English, Korean or Chinese and the corner you pick. ## Kennmarq.com Safety Check Screens pictures for sexual content with someone who appears to be a child, and with a realistic face: the AI Act's Art. 5 bans, the same check as [POST /v1/check](https://kennmarq.com/docs/api/check). It runs on your machine: nothing is sent and nothing counted. It stops the run on a block, or passes its answers to Mark & Save Image, which then leaves the blocked pictures unwritten. Its models download once, about 2.3 GB, and it needs onnxruntime. --- Source: https://kennmarq.com/docs/webhooks # Webhooks Add an endpoint in the dashboard and we POST each event your keys cause to it, signed with the endpoint's own secret. Every plan has them. ## Events | Type | When | data holds | | --- | --- | --- | | `mark.completed` | A file marked by POST /v1/mark (a clip when its job is done), or a manifest signed by POST /v1/sign (local mode). | mark_id, mode (cloud or local), project_id, kind (picture, song or clip), format, bytes, sha256, watermark_id, manifest, log_index, safety; a song's and a clip's seconds and marks | | `mark.failed` | A clip whose job failed: blocked by the safety check, over the marks left, a file we can't read, or a failure on our side. Nothing was marked. | mark_id, project_id, kind, error (code and message), and a blocked frame's safety | | `check.blocked` | A picture or a clip's frame the safety check blocked, on POST /v1/check or with check on POST /v1/mark. | request_id, endpoint, project_id, policy, reason, picture (file or input), and a clip's frame. Never the picture or its hash: we keep none | | `ping` | A test, sent from the dashboard to one endpoint. | message | An endpoint takes the events of either live or test keys, never both, as keys do ([environments](https://kennmarq.com/docs/keys)). The body: ```json { "created": 1793182831, "data": { "bytes": 1208033, "format": "image/png", "log_index": 4127, "manifest": "urn:c2pa:…", "mark_id": "mk_4Q8oT1n6mWc2fLr0aZ9eYk", "mode": "cloud", "project_id": "prj_…", "kind": "picture", "safety": null, "sha256": "…", "watermark_id": "1cfc5f99ffce3db9" }, "id": "evt_9w1KQ0p3aZ8xLm2Tn4Rb6c", "livemode": true, "object": "event", "type": "mark.completed" } ``` | Header | Value | | --- | --- | | `Kennmarq-Signature` | t=,v1=: what to verify before trusting the body. | | `Kennmarq-Event-Id` | The event's ID (evt_…), the same on every retry: use it to handle each event once. | | `Kennmarq-Event-Type` | The event's type. | | `Content-Type` | application/json | | `User-Agent` | Kennmarq-Webhooks/1 | ## Verify each delivery The signature is an HMAC-SHA256 of the time, a full stop and the raw body, under the secret the dashboard showed when you added the endpoint (`whsec_…`). It follows Stripe's webhook signature scheme, so the same verification code works for both. Compare in constant time, and refuse a time more than five minutes off, so an old delivery can't be replayed. ```js import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(secret, rawBody, header, now = Date.now() / 1000) { const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("="))); const t = Number(parts.t); if (!Number.isFinite(t) || Math.abs(now - t) > 300) return false; const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest(); const given = Buffer.from(parts.v1 ?? "", "hex"); return given.length === expected.length && timingSafeEqual(given, expected); } ``` ```python import hashlib, hmac, time def verify(secret: str, raw_body: bytes, header: str) -> bool: parts = dict(p.strip().split("=", 1) for p in header.split(",")) t = int(parts.get("t", "0")) if abs(time.time() - t) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) ``` > Verify the raw body as it arrived, before any JSON parser touches it: a re-serialised body won't match. ## Delivery and retries - A delivery counts when your endpoint answers 2xx within 10 seconds. Answer first, then do the work. - Otherwise we try again after 1 and 5 minutes, half an hour, then 2, 6, 12 and 24 hours, and then give up. Events can arrive more than once and out of order: handle each `Kennmarq-Event-Id` once. - Endpoints are HTTPS on the standard port, on the public internet: we never follow a redirect, and never send to a private, loopback or link-local address, whatever a name resolves to. - The dashboard shows each organisation's recent deliveries; we keep them 30 days. --- Source: https://kennmarq.com/docs/errors # Errors Errors use HTTP status codes and a JSON body with a stable, machine-readable `code`, a message for people, and the request's ID. ```json { "error": { "code": "quota_exceeded", "message": "This organisation has used all 1,000 marks of this month on its plan. Upgrade the plan for more, or wait for the 1st of next month (UTC).", "request_id": "req_7Hc2kQm0Zt1L" } } ``` Branch on `code`, never on `message`: codes are part of API v1 and won't change; messages may get clearer. Every response, success or not, carries the same ID in its `X-Request-Id` header. Quote it when you write to us. ## Codes | Status | Code | Meaning | What to do | | --- | --- | --- | --- | | 400 | `invalid_request` | The request is malformed or a field is out of range. | Fix the request; the message names the field. | | 400 | `unknown_field` | A field the endpoint doesn't know. | Check the field's spelling against the reference. | | 400 | `option_not_available` | A planned option that isn't available yet. | Leave it out for now; the changelog says when it arrives. | | 401 | `missing_api_key` | No bearer key in the Authorization header. | Send Authorization: Bearer sk_…. | | 401 | `invalid_api_key` | The key isn't one of ours. | Check the key; create a new one if it's lost. | | 401 | `api_key_revoked` | The key was revoked or its project deleted. | Use another key. | | 402 | `quota_exceeded` | The month's allowance is used up. | Wait for the 1st of the month (UTC), or upgrade. | | 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope, or create one. | | 403 | `ip_not_allowed` | The address isn't on the key's IP allowlist. | Call from an allowed address, or update the allowlist. | | 403 | `key_not_allowed_in_url` | A key with scopes beyond tsa was sent in a URL. | Use a tsa-only key, and rotate the one you sent. | | 404 | `not_found` | No endpoint at this path. | Check the path and the API version. | | 404 | `upload_not_found` | The upload named in "upload" isn't one this key can use: unknown, used, older than an hour, or made by another key. | Make a new upload with this key (POST /v1/uploads). | | 405 | `method_not_allowed` | The endpoint doesn't take this method. | See the Allow header. | | 413 | `payload_too_large` | The body is over the endpoint's limit. | 4 MB per request: send larger files (up to 25 MB) through POST /v1/uploads. 4 KB for time-stamp queries. | | 415 | `unsupported_media_type` | The file or the request's Content-Type isn't accepted. | Send PNG, JPEG or WebP as multipart/form-data. | | 422 | `content_blocked` | POST /v1/mark: the safety check blocked the picture; nothing was marked. The body's "safety" says why. | Don't retry the same picture. The checks it ran count; the mark doesn't. | | 429 | `rate_limited` | Too many calls this minute for this key. | Wait Retry-After seconds, then retry. | | 500 | `internal_error` | Something failed on our side. | Retry with backoff; nothing was charged. | | 502 | `mark_failed` | The file couldn't be marked and verified; no file was returned. | Retry once; if it persists, contact support with the request ID. | | 503 | `marker_unavailable` | The marking service is briefly unavailable. | Retry with backoff; nothing was charged. | | 503 | `tsa_unavailable` | The time-stamp server is briefly unavailable. | Retry with backoff; nothing was charged. | | 503 | `storage_unavailable` | Uploads are briefly unavailable. | Retry with backoff; nothing was charged, and the upload is kept. | ## Retrying - `429`: wait the number of seconds in `Retry-After`. - `500`, `502`, `503`: retry with exponential backoff (1 s, 2 s, 4 s…), up to a few times. Failed calls are never counted against your allowance. - Other `4xx`: don't retry unchanged; fix the request first. --- Source: https://kennmarq.com/docs/rate-limits # Rate limits and quotas Calls a minute, clips at a time, failed calls a day and allowances a month, per organisation: all its keys together, following your plan. Calls without a key count per address. | Plan | Calls a minute | Marks a month | Data returned a month | Clips at a time | | --- | --- | --- | --- | --- | | Free | 10 | 1,000 | 1 GB | 1 | | Indie | 60 | 20,000 | 20 GB | 2 | | Startup | 300 | 150,000 | 150 GB | 3 | | Scale | 1,000 | 1,000,000 | 500 GB | 5 | | Enterprise | Set in your contract | | | | ## Calls a minute Every authenticated call counts toward its organisation's limit, all its keys together: marks, time-stamps and usage alike. Windows are whole minutes on the clock (UTC), so the count starts again at the top of each minute. Every response says where you stand: ``` X-RateLimit-Limit: 10 # calls this minute, for this organisation X-RateLimit-Remaining: 7 # left this minute X-RateLimit-Reset: 1792663260 # Unix time (s) when the minute ends ``` Past the limit the API answers `429 rate_limited` with `Retry-After` in seconds. Spread work across the minute, or move to a bigger plan. The Python SDK waits out a `Retry-After` of up to a minute by itself. ## Failed calls A mark or check that fails after we worked on it (`mark_failed`, or a check the checker couldn't finish) counts nothing, so these failures have a daily cap per organisation: Free 100, Indie 300, Startup 1,000, Scale 3,000 a day (UTC). Past it, `POST /v1/mark` and `POST /v1/check` answer `429` until 00:00 UTC. Refusals before any work (a file we can't read, a block, the marks left) don't count. ## Clips A clip is marked as a job, and each plan marks a few at a time (Free 1, Indie 2, Startup 3, Scale 5). A clip sent while they're all running answers `429 rate_limited` with `Retry-After: 30`. A clip that fails after we worked on it (its watermark doesn't read back, or it can't be marked in time: `mark_failed`) counts nothing, so these failures have a daily cap: Free 3, Indie 10, Startup 10, Scale 20 in 24 hours. Past it, new clips answer `429` until the first of those failures is a day old. [GET /v1/marks/{id}](https://kennmarq.com/docs/api/marks#failures) says why each failed. Refusals before any work (a clip over our limits, a block, the marks left) don't count. ## Calls without a key The public endpoints count per address instead, with the same headers. An IPv6 address counts by its /64, the block one connection usually comes with. - [POST /v1/verify](https://kennmarq.com/docs/api/verify): 10 files a minute and 200 a day. Without a key, the whole service also answers at most 5,000 checks a day (UTC), and 500 with `signed=true`; with a key (free), checks count toward your organisation's calls a minute instead. The [checker page](https://kennmarq.com/check) reads files in your browser and doesn't count here. - The [transparency log](https://kennmarq.com/docs/api/log), `GET /v1/credentials` and `GET /v1/verify/key`: 120 calls a minute. ## Monthly allowances - **Marks**: one per successful `POST /v1/mark`, and two per picture the [safety check](https://kennmarq.com/docs/api/check) reads (`POST /v1/check`, or `check` on a mark). Test keys have their own allowance: 100 marks a month on Free, as many as live on paid plans. Free's 1,000 marks are one allowance per person: their first organisation has them, and any other they create starts with none until it has a paid plan. - **Data returned**: the bytes of marked files we send back in cloud mode (1 GB = 10⁹ bytes). - **Time-stamps**: one per `POST /v1/tsa`, up to your plan's marks, again live and test apart. Allowances run by calendar month in UTC. When one is used up the API answers `402 quota_exceeded` until the 1st of the next month, or until you upgrade. A mark is reserved when a request is accepted and given back if anything fails, so only delivered marks count. Your current numbers are in the dashboard, in the `X-Marks-Used` and `X-Marks-Limit` headers, and at [GET /v1/usage](https://kennmarq.com/docs/api/usage). Monthly paid plans continue past their allowance at the published extra prices, billed on the 1st, up to ten times the allowance; annual plans stop at it, like Free. --- Source: https://kennmarq.com/docs/robustness # Robustness report How well our marks hold up, measured on our own machines and published, as the Code of Practice's Commitment 3 asks. Measured 2026-10-23, on CPU, with the code and models the API runs. ## Two layers, two kinds of strength - **The C2PA manifest** is metadata, signed: it proves who signed what, and breaks loudly if the file changes. Most platforms strip metadata when they re-encode an upload, so it rarely survives a re-post. - **The watermark** (TrustMark, variant Q) is in the pixels and carries a 61-bit ID. It survives the usual re-encoding and resizing, and its ID leads to the file's [durable credential](https://kennmarq.com/docs/api/verify#credentials), the manifest we keep. This report measures it. ## How it was measured 78 AI-generated pictures (everyday scenes; median 0.7 megapixels), each watermarked with a random ID as the API does, then edited each way below, and read back. A success is the exact ID; anything else, including no watermark found, counts as a miss. The same edits on the pictures without a watermark show how often the reader claims a mark that isn't there. Intervals are Wilson's 95%. ## The watermark after each edit | Edit | ID read back | 95% interval | Unmarked pictures read as marked | | --- | --- | --- | --- | | Unchanged (PNG) | 100% (78/78) | 95.3%–100% | 0 | | JPEG, quality 95 | 100% (78/78) | 95.3%–100% | 0 | | JPEG, quality 85 | 100% (78/78) | 95.3%–100% | 0 | | JPEG, quality 75 | 100% (78/78) | 95.3%–100% | 0 | | JPEG, quality 60 | 100% (78/78) | 95.3%–100% | 0 | | JPEG, quality 50 | 100% (78/78) | 95.3%–100% | 0 | | WebP, quality 80 | 100% (78/78) | 95.3%–100% | 0 | | Long side 1080 px, JPEG 85 (a social network's upload) | 100% (78/78) | 95.3%–100% | 0 | | Resized to 75% | 100% (78/78) | 95.3%–100% | 0 | | Resized to 50% | 100% (78/78) | 95.3%–100% | 0 | | Resized to 25% | 100% (78/78) | 95.3%–100% | 0 | | Cropped to the centre 90% of each side | 100% (78/78) | 95.3%–100% | 0 | | Cropped to the centre 75% of each side | 52.6% (41/78) | 41.6%–63.3% | 0 | | Cropped to the centre 50% of each side | 0% (0/78) | 0%–4.7% | 0 | | Brightness +20% | 88.5% (69/78) | 79.5%–93.8% | 0 | | Brightness −20% | 100% (78/78) | 95.3%–100% | 1 | | Contrast +20% | 100% (78/78) | 95.3%–100% | 0 | | Contrast −20% | 100% (78/78) | 95.3%–100% | 0 | | Saturation +30% | 100% (78/78) | 95.3%–100% | 0 | | Greyscale | 98.7% (77/78) | 93.1%–99.8% | 1 | | Gaussian blur, radius 1 px | 100% (78/78) | 95.3%–100% | 0 | | Gaussian blur, radius 2 px | 100% (78/78) | 95.3%–100% | 0 | | Gaussian noise, σ 5 | 100% (78/78) | 95.3%–100% | 0 | | Gaussian noise, σ 10 | 100% (78/78) | 95.3%–100% | 0 | | Rotated 2° and cropped straight | 100% (78/78) | 95.3%–100% | 0 | | Rotated 5° and cropped straight | 41.0% (32/78) | 30.8%–52.1% | 0 | | Mirrored left to right | 100% (78/78) | 95.3%–100% | 0 | The watermark costs little: a median PSNR of 42.2 dB against the original (the lowest 39.0 dB). On a laptop CPU, embedding took a median 0.15 s and reading 0.06 s. > Where it weakens: crops that keep less than about 80% of each side (half the IDs survive at 75%, none at 50%), rotations of a few degrees (41% at 5°), and strong brightening. Edits made on purpose to remove it weren't tested. A missing watermark proves nothing about a picture. A found one is worth what its credential says: the reader claimed a mark on an unmarked picture 2 times in 2,106, each time with a random 61-bit ID that all but certainly leads to no credential, so always look the ID up. ## The safety check's false alarms The same 78 everyday pictures through the [safety check](https://kennmarq.com/docs/api/check), under both policies: everyday pictures should all be allowed. | Policy | Allowed | Flagged or blocked | 95% interval of false alarms | | --- | --- | --- | --- | | `ai-act` | 78/78 | 0 | 0%–4.7% | | `safe-only` | 78/78 | 0 | 0%–4.7% | A check took a median 0.12 s on a laptop CPU. How often explicit pictures are caught isn't measured here: no test of ours makes or holds a sexual picture. The nudity classifier's own published evaluation stands in for it, and the apparent-age error rates measured on FairFace's faces are on the [safety check's page](https://kennmarq.com/docs/api/check#accuracy), with its known limits. --- Source: https://kennmarq.com/docs/versioning # Versioning API v1 is frozen from the launch. Code written against it keeps working: it gains endpoints and fields, and loses nothing. ## What may change in v1 - New endpoints, and new optional request fields and options, off by default. - New fields in responses and in webhook events. Ignore fields you don't know. - New error codes, for new features only; an existing request never gets a new code for an old reason. - New webhook event types, sent only to endpoints that ask for them. - Error messages, which are for people: branch on the code, never on the message. - Higher limits, more formats, faster answers. ## What never changes in v1 - An endpoint's path, method, or the meaning of its fields and answers. - A field's name or type, a default, or an error code's status and meaning. - What counts as a mark, as the pricing page states it, without notice to existing customers. - The signing scheme of webhooks ([Kennmarq-Signature](https://kennmarq.com/docs/webhooks)) and the transparency log's formats. ## A breaking change It would come as v2, at its own paths, announced at least 12 months ahead in the [changelog](https://kennmarq.com/docs/changelog) and by email to every organisation's owners. v1 keeps answering for those 12 months at least. A fix for a security problem is the one exception: if a request is unsafe as it is, we change it at once and say so in the changelog. ## OpenAPI v1 is described as an OpenAPI 3.1 document at [`https://api.kennmarq.com/v1/openapi.json`](https://api.kennmarq.com/v1/openapi.json), for client generators and API tools. It lists every endpoint, field and error code this reference does. > The Python SDK and the ComfyUI nodes follow semantic versioning: a breaking change in them is a new major version. --- Source: https://kennmarq.com/docs/changelog # Changelog Changes to the API and the dashboard, newest first. API v1 is frozen: breaking changes come only in a new version, announced 12 months ahead. ## 2026-10-24: Visible labels, South Korea and India - `label=true` on [POST /v1/mark](https://kennmarq.com/docs/api/mark#labels) draws the EU's visible label into a picture or every frame of a clip, before the watermark: “AI GENERATED”, or “AI MODIFIED” for edits, in English, Korean or Chinese, in any corner, in three sizes and the Code's four styles. The manifest records it, and the checker shows it. - `deepfake=true` turns the label on, as the AI Act's Art. 50(4) asks of deep fakes. - Two new markets: `kr` ([South Korea's AI Basic Act](https://kennmarq.com/docs/laws/south-korea), the label for deep fakes) and `in` ([India's IT Rules 2026](https://kennmarq.com/docs/laws/india), the label on every picture and clip, and `app_name` required). Receipts list the label. ## 2026-10-24: The checker reads wide pictures - [Check a picture](https://kennmarq.com/check) reads the watermark of a picture more than twice as wide as it is high, or twice as high as it is wide, such as a banner or a phone screenshot: it looks in the centre square, where TrustMark puts the mark. ## 2026-10-24: POST /v1/verify reads turned pictures - [POST /v1/verify](https://kennmarq.com/docs/api/verify) also reads our watermark at each quarter turn: a picture saved on its side or upside down still leads to its Content Credentials. - A TrustMark watermark that isn't ours shows only when the picture's centre reads the very same one, so a picture without a mark no longer shows one by chance, in results and signed results alike. ## 2026-10-24: Markets and receipts - A new `markets` field on [POST /v1/mark](https://kennmarq.com/docs/api/mark) and in the Python SDK: `eu`, `us-ca`, `us-wa`, `us-ut`, or `us`. A call missing what those laws need is refused, before anything is marked or counted. - A mark made with markets has a [receipt](https://kennmarq.com/docs/api/marks#receipts): what its file carries for each law, and what stays with you, signed with our checker key when you ask for it. ## 2026-10-24: The checker reads turned pictures - [Check a picture](https://kennmarq.com/check) also reads our watermark at each quarter turn, as TrustMark's own decoder can: a picture saved on its side still leads to its Content Credentials. - A TrustMark watermark that isn't ours shows only when the picture's centre reads the very same one, so a picture without a mark no longer shows one by chance. ## 2026-10-24: California's latent disclosure - Two new fields on [POST /v1/mark](https://kennmarq.com/docs/api/mark), the Python SDK and the ComfyUI nodes: `provider`, your legal name, and `app_version`, your AI system's version beside `app_name`. With the time, the unique IDs and the source we already record, a mark carries all five items of [California's latent disclosure](https://kennmarq.com/docs/laws/california). - Without `app_name`, a manifest no longer names us as the AI system that made the content: it names none. - The [checker](https://kennmarq.com/check) and `POST /v1/verify` show the provider, the AI system and its version, the model and the creation time: a new `origin` object in the result. - New guides: the [EU AI Act](https://kennmarq.com/docs/laws/eu-ai-act) and the [California AI Transparency Act](https://kennmarq.com/docs/laws/california). ## 2026-10-24: The Free plan: one per person - The Free plan's 1,000 marks a month are one allowance per person: your first organisation has them, and any other you create starts with none until it has a paid plan (`402 quota_exceeded` says so). - Test keys on Free have 100 marks and 100 MB a month; on paid plans, as many as live. New accounts are capped per day across the service; past it, sign-up opens again at 00:00 UTC. ## 2026-10-24: Limits per organisation - The plan's [calls a minute](https://kennmarq.com/docs/rate-limits) count for the whole organisation, all its keys together. Marks and checks that fail after we worked on them have a [daily cap](https://kennmarq.com/docs/rate-limits#failed-calls) per organisation. - Limits per address count an IPv6 address by its /64. Without a key, the whole service answers at most 5,000 checks a day (500 signed); with a key (free), checks count toward your organisation's calls a minute. The Python SDK waits out a `429` that asks for a minute or less. ## 2026-10-23: Clip limits - A [clip](https://kennmarq.com/docs/api/mark#clips) holds up to 4,500 frames (2.5 minutes at 30 frames a second, 75 seconds at 60): what we can mark within a job's time. A flat clip's watermark strength is now chosen from a sample before every frame is marked, so it takes one pass. - Each plan marks a few [clips at a time](https://kennmarq.com/docs/rate-limits#clips) (Free 1, Indie 2, Startup 3, Scale 5), and clips that fail after we worked on them have a daily cap. Past either, `429 rate_limited` with `Retry-After`. ## 2026-10-23: Clip downloads - A finished clip's file is kept for a day, and each [download link](https://kennmarq.com/docs/api/marks#downloads) works for 5 minutes, up to 5 links per clip. Asking again within a link's 5 minutes gives the same link's time. Each link after the first counts the file's size against the month's data. New fields: `download_links_left` and `download_error`. - Marked clips come back at up to 20 Mbit/s of video. ## 2026-10-23: Songs and clips - [POST /v1/mark](https://kennmarq.com/docs/api/mark#songs) marks songs: MP3, WAV and M4A up to 15 minutes, with Meta's AudioSeal in the sound, shaped under the music so no one hears it, and signed, time-stamped Content Credentials. A song counts five marks per started minute. New headers: `X-Marks-Counted` and `X-Audio-Watermark`. - And clips: MP4 up to 5 minutes at 1080p, with Meta's Video Seal in every frame, carrying an ID that leads to the clip's durable credential, and AudioSeal in its sound. A clip is a job: `202`, then [GET /v1/marks/{id}](https://kennmarq.com/docs/api/marks) for its status and a download link. A clip counts five marks per started second; its safety check reads 12 frames, two marks each. - [Uploads](https://kennmarq.com/docs/api/uploads) take a clip of up to 1 GB. [POST /v1/verify](https://kennmarq.com/docs/api/verify) reads songs and clips, and [POST /v1/check](https://kennmarq.com/docs/api/check) clips. A new webhook event, `mark.failed`, for a clip whose job failed. - The [Python SDK](https://kennmarq.com/docs/sdk/python) 0.2.0 marks songs and clips (it waits for a clip's job), and the [ComfyUI nodes](https://kennmarq.com/docs/comfyui) add Mark & Save Audio and Mark & Save Video. ## 2026-10-23: Launch: plans, uploads, webhooks, and API v1 frozen - Paid plans: Indie, Startup and Scale, monthly or yearly, bought and changed from the dashboard's [Billing](https://kennmarq.com/dashboard/billing) page through Stripe. Monthly plans run past their allowance into extras (USD 0.0003 a mark, USD 0.10 a GB of data), billed on the 1st, up to ten times the allowance; Free and annual plans stop at it. `GET /v1/usage` adds `marks.ceiling`. - [POST /v1/uploads](https://kennmarq.com/docs/api/uploads): a request now carries at most 4 MB; files up to 25 MB go up first, straight to our storage in Zurich, and calls name them in `upload` (or `input_upload`). The Python SDK does it by itself. New error codes: `upload_not_found`, `storage_unavailable`. - [Webhooks](https://kennmarq.com/docs/webhooks): `mark.completed` and `check.blocked`, signed (Kennmarq-Signature, Stripe's scheme), retried for a day. Endpoints are added in the dashboard. - API v1 is frozen: additions only, 12 months' notice before anything breaking ([versioning](https://kennmarq.com/docs/versioning)), and an OpenAPI 3.1 document at `/v1/openapi.json`. - The [trust center](https://kennmarq.com/trust), the [status page](https://status.kennmarq.com), the [robustness report](https://kennmarq.com/docs/robustness) for picture marks, and the legal pages: terms, privacy, the DPA and our two sub-processors. ## 2026-10-23: Local mode, the Python SDK and the ComfyUI nodes - [POST /v1/sign](https://kennmarq.com/docs/api/sign): local mode's signature. The watermark and the Content Credentials are made on your machine, and only the manifest (about 13 KB, never pixels) comes here to be signed and time-stamped. We sign only what a cloud mark carries: an AI source type, no thumbnails, every assertion as the claim hashes it. Keys need the `sign` scope; each file counts one mark. - The [Python SDK](https://kennmarq.com/docs/sdk/python): the cloud API, local mode, the safety check on your machine, and verify. And the [ComfyUI nodes](https://kennmarq.com/docs/comfyui): Mark & Save Image and Safety Check. Both are coming to PyPI and the ComfyUI Registry. - A new kind of transparency-log entry, `c2pa.local_signature`, which holds the file's hard binding where a cloud signature's holds its hash. Local marks with a watermark keep a durable credential too, without a file hash: `same_file` is then `null`. ## 2026-10-23: The safety check - [POST /v1/check](https://kennmarq.com/docs/api/check): screens a picture for sexual content with an apparent minor or a realistic face, the AI Act's Art. 5 bans, under the policy `ai-act` or `safe-only`. It answers allow, flag or block, with the reason; never an age or an identity. Two marks per picture; keys need the new `check` scope. - `check` on `POST /v1/mark`: the file and any input are checked before marking. A block answers `422 content_blocked` with the check, and nothing is marked; a flag marks, with `X-Safety-Verdict` and `X-Safety-Reason`. - Our own floor on every mark, whatever the policy: explicit content with someone who appears to be a child is never marked. - The check's models, their licences and their measured error rates are [published](https://kennmarq.com/docs/api/check#accuracy). ## 2026-10-22: The free checker - [Check a picture](https://kennmarq.com/check): whether it carries Content Credentials and a TrustMark watermark, ours or anyone's, and what they say. The check runs in the browser; the picture never leaves the device. - `POST /v1/verify`: the same check through the API, no key needed, 10 files a minute and 200 a day per address. With `signed=true`, a result signed by our checker key (JWS, ES256), written to the transparency log before it's returned, with `X-Log-Index`; the key is at `GET /v1/verify/key`. See [POST /v1/verify](https://kennmarq.com/docs/api/verify). - Durable Content Credentials: every watermarked file's manifest store is kept under its watermark ID, so a copy whose metadata was stripped leads back to it. `GET /v1/credentials/{watermark_id}` returns it, as JSON or as a `.c2pa` file. - A new kind of transparency-log entry, `checker.result`: the hashes of each signed result and of the file it's about. ## 2026-10-22: The transparency log - Every signature from `POST /v1/mark` and every token from `POST /v1/tsa/{key}` is appended to a public, append-only log before it's returned. Both responses carry its place in `X-Log-Index`. - `GET /v1/log/root`, `/roots`, `/proof/{entry}`, `/consistency`, `/entries` and `/key`: signed roots and the proofs to check them, no key needed. See the [transparency log](https://kennmarq.com/log). - Each organisation's audit-log head is anchored in the log whenever it moves; the dashboard's audit page shows the latest anchor. ## 2026-10-22: API v1 preview: marking pictures - `POST /v1/mark`: an invisible watermark and signed, time-stamped C2PA Content Credentials for PNG, JPEG and WebP pictures up to 25 MB. - `POST /v1/tsa/{key}`: our RFC 3161 time-stamp server, with tsa-only keys. - `GET /v1/certificate` and `GET /v1/usage`. - Organisations with roles, projects, test and live keys with scopes and IP allowlists, and a hash-chained audit log with CSV and JSON export.