# Detection solution: integration information

**Status:** published. Last reviewed 2026-08-06.  
**Applies to:** the imperceptible watermark layers on image, video and audio assets
labeled by this platform, and the C2PA Soft Binding Resolution API that resolves
them.  
**Answers:** Code of Practice Measure 3.4(b) in the initial stage (in force since
2 August 2026), and Measure 3.4(c)(i) as the interoperability solution for the
2 February 2027 obligation.

This document describes how a third party integrates with and accesses the
detection solution this platform implements. Reading it, or following the API
it describes, is **not conclusive evidence of compliance** with the AI Act or
the Code of Practice. It records what this deployment does and how to use it.

---

## 1. Four ways to reach the detection solution

Try these in order:

1. **Read the C2PA manifest in the asset.** Labeled assets that retain their
   metadata carry a `c2pa.soft-binding` assertion naming the algorithm (`alg`)
   and the binding value, plus a `c2pa.watermarked.bound` action (C2PA 2.4;
   manifests signed before 2026-09 carry `c2pa.watermarked`). A C2PA-aware reader
   can learn which decoder to run without calling this API.

2. **Extract the watermark yourself and resolve it at
   `/matches/byBinding`.** Run the algorithm named in the assertion (or
   discovered from the media type), obtain the soft binding value, base64-encode
   it, and query the resolution API. This is the provider-agnostic workflow
   Measure 3.4(c)(i) targets.

3. **Hand us the asset at `/matches/byContent`.** When you have no local decoder,
   POST the raw bytes with the correct `Content-Type`. We run the extractor that
   matches the media type and resolve any recovered routing code.

4. **Use the human-facing `POST /api/v1/detect`.** This endpoint returns a
   structured detection result for operators, regulators and end-users. It is not
   part of the C2PA Soft Binding Resolution API, but it completes the same
   workflow without implementing a resolver client.

---

## 2. Base URL and endpoints

**Base URL:** `https://<deployment>/api/v1/softbinding`

Append the paths below unchanged. The four routes conform to the
[C2PA Soft Binding Resolution API 1.1.0](https://github.com/c2pa-org/spec/blob/main/schemas/softbinding/partials/softbinding-resolution-api.openapi.json)
(shipped in the normative `C2PA_Schemas.zip`).

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/services/supportedAlgorithms` | Lists watermark algorithms this endpoint resolves. |
| `GET` | `/matches/byBinding` | Resolve a binding value to manifest matches. |
| `POST` | `/matches/byBinding` | Same as GET, for values too long for a URL. |
| `POST` | `/matches/byContent` | Resolve an uploaded asset to manifest matches. |
| `GET` | `/manifests/{manifestId}` | Fetch the C2PA manifest bytes for a match. |

### `GET /services/supportedAlgorithms`

No parameters. Response shape:

```json
{"watermarks": [{"alg": "<identifier>"}], "fingerprints": []}
```

### `GET /matches/byBinding`

Query parameters:

| Parameter | Required | Description |
|---|---|---|
| `value` | yes | Base64 of the ASCII soft binding string (see §4). |
| `alg` | yes | Algorithm identifier from the normative list or this document. |
| `maxResults` | no | Default 10; clamped to 100. |

Response shape (`c2pa.softBindingQueryResult`):

```json
{"matches": [{"manifestId": "<uuid>", "similarityScore": 100}]}
```

`endpoint` is omitted; the spec reads its absence as "fetch the manifest from
the same service using `/manifests/{manifestId}`", which is correct.

Returns `400` for an unsupported `alg`. Returns `200` with an empty `matches`
array for a well-formed value that resolves to no manifest (including legacy v1
audio and short clips without a full v3 cycle).

### `POST /matches/byBinding`

JSON body: `{"alg": "<identifier>", "value": "<base64>"}`. Optional query
parameter `maxResults` (same default and clamp as GET). Same response shape.

### `POST /matches/byContent`

Raw request body typed by `Content-Type` (not multipart). Accepted types:
JPEG, PNG, WebP, MP4, WebM, MP3, WAV. Optional query parameters `alg`, `hintAlg`,
`hintValue` and `maxResults` are accepted; hints are ignored because we run
the one extractor that matches the media type. WebM carries the watermark
layer inside the file when embed succeeded; its C2PA manifest remains a
sidecar (not inside Matroska).

Same `matches` response shape. Audio assets without a recovered v3 routing code
return `{"matches": []}` — see §5.

### `GET /manifests/{manifestId}`

Path parameter `manifestId` is the UUID returned in `matches[].manifestId`
(the same identifier as `/verify/{id}`). Returns `application/c2pa` manifest
bytes, or `404` if not found.

---

## 3. Algorithms

| `alg` | Modality | Registry status | Resolves to |
|---|---|---|---|
| `com.adobe.trustmark.P` | image; video per frame where VideoSeal cannot mark | **Entry 13, normative** | one manifest |
| `com.adobe.trustmark.Q` | image | **Entry 4, normative** | one manifest (images delivered before 2026-10-14) |
| `eu.trion.videoseal.1` | video | **pending** — PR to `c2pa-org/softbinding-algorithm-list` | one manifest |
| `eu.trion.audioseal.1` | audio | **pending** | one manifest when the v3 code is recovered *and verified*; presence and level only otherwise |

The normative registry is at
[`github.com/c2pa-org/softbinding-algorithm-list`](https://github.com/c2pa-org/softbinding-algorithm-list).
Of the four in this table, only `com.adobe.trustmark.P` and `.Q` appear there
today (entries 13 and 4). The video and audio
identifiers are ours, advertised because we resolve them, but a
provider-agnostic resolver that reads only the registry will not discover them
until the pending pull request merges.

Submitted registry entries are pinned in
`docs/compliance/softbinding-algorithm-entries.json`.

---

## 4. Payload layouts

Every soft binding value on the wire is an ASCII string of the form
`"<schema>*<bits>"`, then base64-encoded for API query parameters. This
convention follows Adobe's reference integration in
[`adobe/trustmark`](https://github.com/adobe/trustmark)
(`c2pa/c2pa_watermark_example.py`: `str(schema_code) + "*" + watermarkID`).

### Image: `com.adobe.trustmark.P`

TrustMark P at `WM_STRENGTH=1.5`, BCH schema 1, since 2026-10-14; TrustMark Q
before. Both carry the same card, and both are read (Q first, then P; the
decision record's addendum of 2026-10-14 says why). The card is **61 bits**:

```
[8-bit version = 1][53-bit routing code]
```

Soft binding value: `1*<61-bit string>` (schema `1`, then the card bits).

The 53-bit code indexes `Manifest.wm_code` in our database. Version must be
exactly `1`; any other version unpacks to no match.

### Video — `eu.trion.videoseal.1`

VideoSeal 1.0.1. The card is **256 bits**, in one of two layouts told apart by
the 8-bit version field. A watermark inside an asset already delivered to a
customer can never be re-embedded, so a reader has to be able to tell them
apart rather than assume the newest.

```
v1  [61-bit payload as above][195 zero bits used as presence test]
v2  [61-bit payload][3 zero bits][32 parity bits][160 zeros as presence test]
```

The first 61 bits use the same `[8-bit version][53-bit code]` layout as image.
v2 adds Reed-Solomon parity — RS(12,8) over GF(256), 4 parity symbols over the
64 data bits — and a reader applies it **before** reading the version field, so
a flipped version bit is recovered like any other.

Why it exists, measured rather than assumed: over 220 production codes read
through the shipped 3 s window, 10.0% came back wrong, and **every one of those
was exactly one flipped bit, never an absent mark**. Without parity, that is a
valid-looking code resolving to no manifest — found, but tied to nothing.

On extraction, more than 10% ones in a zero region means "not our mark" (noise
from an unmarked video). The test applies to each zero region on its own rather
than to the tail as a whole: a short noisy stretch averaged against a long
guaranteed-zero one clears a bar it should fail.

Soft binding value: `0*<256-bit string>` (schema `0` — VideoSeal has no BCH
schema concept; `0` keeps the value shape identical across modalities).

### Video where VideoSeal cannot mark: `com.adobe.trustmark.P` per frame

Since 2026-10-18. VideoSeal 1.0.1 takes no readable mark on near-uniform
frames (flat motion graphics: a title, a box or slides on a solid or gradient
background). Where the delivery's public read recovers no VideoSeal code, and
on frozen video, every frame is marked with TrustMark P instead, at the image
operating point, with the image card (`[8-bit version][53-bit code]`, 61 bits).
The manifest then declares `com.adobe.trustmark.P` with the image value
`1*<61-bit string>`.

A reader decodes frames with TrustMark P. Ours reads 9 frames spread over the
first 30 s and accepts a code only with at least 5 of 9 votes and at least
twice the runner-up, because a single frame can decode as another valid code;
`POST /detect` and `matches/byContent` run this read only after VideoSeal
recovered nothing, so every video delivered before then reads as it did.

The routing code shares the image namespace, so one `wm_code` lookup answers
both stills and video frames from the same labeled asset.

### Audio — `eu.trion.audioseal.1`

AudioSeal 0.2.0. Each frame is a **16-bit** card. Two layouts:

**v1 (legacy and short clips):**

```
[8-bit version = 1][2-bit generation level][6 zero bits]
```

Generation level encoding: `01` = full, `10` = partial. No routing code.

**v3 (manifest pointer, clips ≥ one cycle):**

Sixteen tiled frames across 16 × 1.0 s = 16 s carry one shared 53-bit routing
code (same namespace as image and video) plus level:

```
[2-bit version = 3][2-bit generation level][4-bit chunk index][8-bit payload]
```

The eight payload bytes are **not** eight slices of the code. They are the data
symbols of a Reed–Solomon RS(16, 8) codeword over GF(2⁸) whose message is the
53-bit code plus a CRC-11/FLEXRAY (polynomial `0x385`, init `0x01A`). The parity
corrects up to 4 wrong symbols, or 8 erasures, or any mix satisfying
2·e + f ≤ 8, so a chunk the detector never sees is an erasure the parity
absorbs rather than a dead end.

Superseded v2 — recorded because a v2 label may still exist in the wild — tiled
**eight** frames across 8 × 2.0 s as
`[4-bit version = 2][2-bit level][3-bit chunk index][7-bit payload]`, with no
error correction of any kind.

Each chunk carries one Reed–Solomon data symbol; any 8 of the 16 chunks
reconstruct the codeword, and the code is emitted only once RS **and** the CRC
accept it. Beyond the correction budget nothing is claimed: the asset reads as
unmarked or v1 only.

Soft binding value (pointer layouts only): `0*<61-bit string>` — the same
`[8-bit version][53-bit code]` payload shape as image
(`imperceptible.pack(code)`), not the raw 16-bit card. New short clips and
honest v1 delivery omit the `c2pa.soft-binding` assertion entirely (no value
on the wire). Legacy labels that still carry a 16-bit card as `0*<16 bits>`
resolve to no match via `parse()` — that shape never packed a routing code.

The routing code shares the image and video namespace.

---

## 5. Short clips, legacy v1, and partial recovery

AudioSeal's native card is 16 bits per frame. **v1** carries an 8-bit layout
version and a 2-bit generation level only — no manifest routing code. **v3**
tiles sixteen frames across a 16 s cycle to carry the shared 53-bit code, with
Reed–Solomon parity and a CRC over the whole word. The superseded **v2** tiled
eight frames across the same cycle with no error correction.

Therefore:

- Clips shorter than one v3 cycle, and legacy v1 labels, carry presence and
  level only. New labels in that case omit `c2pa.soft-binding` (no routing
  code claimed). Older manifests may still expose a legacy 16-bit soft-binding
  value; `GET /matches/byBinding` correctly returns `{"matches": []}` for it.
- **A code is emitted only after Reed–Solomon and the CRC have both accepted
  it.** A partial chunk set is not a dead end — missing chunks are erasures and
  the parity reconstructs the code while the budget holds — but past that budget
  no pointer is invented.

  This paragraph previously claimed the same property for **v2**, and that claim
  was false. Measured 2026-08-20: v2 protected only 3 of its 56 payload
  positions (a spare-bit check), so a **single** frame bit error — occurring at
  8% per frame even at detector presence 1.0 — produced a well-formed but
  *wrong* 53-bit code that the API served as fact. It did not fail closed; it
  failed silently and confidently. v3 exists because of that measurement, and
  the property is now real rather than assumed. See
  `docs/superpowers/decisions/2026-07-31-watermark-libraries.md`, "Audio — v3
  chunked pointer".
- When the v3 cycle is recovered, `byBinding`, `byContent` and `/detect`
  resolve through the same `wm_code` namespace as image and video.
- `POST /matches/byContent` on audio runs AudioSeal and resolves any recovered
  routing code.

The `c2pa.soft-binding` assertion on labeled audio is emitted only when a
recovered and verified v3 routing code is claimed. Short clips and legacy v1 get no
soft-binding assertion on new labels; the watermark layer may still survive
as presence and level.

Do not interpret an empty `matches` array on audio as "detection failed" when
the clip was too short for one v3 cycle, or only v1 is present, or the recovered
word did not pass its checks. An empty array is this endpoint declining to
guess, which is the behaviour a detection solution should have.

---

## 6. Free of charge and unmetered access

Sub-measure 2.1.1 requires the detection solution to be **free of charge**.
The query surface at `/api/v1/softbinding` and `POST /api/v1/detect` are both
part of that solution.

The following parties receive **unmetered** access via an API key carrying the
`detect` scope (Sub-measure 2.1.1):

- market surveillance authorities
- other regulators
- law enforcement
- media
- fact-checkers
- trusted flaggers
- independent researchers
- educational and research institutions
- civil society

All other callers share a per-IP rate limit (`detect_rate_per_hour`, default 60
requests per hour). Pass the key in the `X-Api-Key` header.

To request a `detect`-scoped key, contact
[compliance@trionsecurity.com](mailto:compliance@trionsecurity.com) with your
organisation, the party category above that applies, and the intended use.

Keys with the `detect` scope are issued by the operator (`python -m src.manage create-api-key --scopes detect --tenant-id …`) and **expire after 90 days** unless `--no-expiry` is set for a named party that must keep a long-lived key. Customer plan keys do not carry `detect`. Public anonymous `/detect` remains available and rate-limited per IP (F27: residual abuse surface; not a product bug).

---

## 7. Privacy and data protection (Sub-measure 2.1.3)

Sub-measure 2.1.3 sets eight requirements. This table answers each honestly for
this deployment.

| Point | What the Code asks | What this deployment does |
|---|---|---|
| **(a)** | Privacy-sensitive processing per EU law | Content is read, hashed and analysed in request scope only. No personal data from `/detect` or `/matches/byContent` reaches storage. |
| **(b)** | Confidentiality and integrity in transit and at rest | **At rest:** vacuous — nothing is at rest. **In transit:** depends on the operator's TLS terminator (not in this repository; guide 04 Phase 3). Signer link is mTLS-only and never publicly exposed. |
| **(c)** | Data minimisation | Caller IP held **in process memory only** in `_hits`, one-hour window, swept at 10,000 keys — never disk or database (`api/src/label/detect.py:390-422`). Legal basis: security and abuse prevention per point (c). |
| **(d)** | Sole purpose — detection only | No analytics, no training on submitted content, no request-body logging middleware (`api/src/main.py`). |
| **(e)** | Zero retention | Bytes read for the duration of the request; never written to MediaStore, ManifestStore or database. Video uses a request-scoped temp file deleted in `finally` (`api/src/label/detect.py:1-7`). Binding queries carry no asset bytes. |
| **(f)** | Security safeguards | Upload caps (25 MB / 50 MP images; modality-specific caps for video/audio), rate limiter, misuse-safeguard table (§8), mTLS to signer. Operator's: patching, network policy, backups. |
| **(g)** | Third-country transfers | Self-hosted reference deployment makes **no** outbound call with submitted content. Where the operator hosts is their DPIA subject. |
| **(h)** | Cybersecurity protecting access | API keys stored as SHA-256 digests with optional expiry (`api/src/auth/apikey.py:19-27`); text detection expert-gated behind `detect_text` scope because uncontrolled access to a low-reliability mechanism is the risk this point names. |

**Declined encouragement — derivative upload only:** uploading a perceptual hash
instead of the asset is **not implemented**. TrustMark, VideoSeal and AudioSeal
extract from pixels and samples; a derivative without semantics carries no
watermark and would defeat detection. The C2PA layer already offers a derivative
path: `GET /verify/by-hash/{sha256}` answers from a hash alone (§2).

---

## 8. Safeguards against misuse

Measure 3.4(c) requires safeguards against misuse risks. This deployment
implements the query subset only:

| Route | Status | Reason |
|---|---|---|
| `POST /manifests`, `POST/PUT/DELETE /bindings` | **absent** | Would let a stranger register bindings pointing at manifests we never signed — poisoning the lookup table. We resolve only bindings we minted. |
| `POST /matches/byReference` | **absent** | Would fetch a caller-supplied HTTPS URL from a public unauthenticated endpoint — server-side request forgery. |

Additional safeguards:

- **Shared rate limiter** with `/detect`: two independent budgets would let
  one caller spend twice against the same IP.
- **`maxResults` clamped** to 100 regardless of the caller's request.
- **Five caller-supplied fields become public** through
  `GET /api/v1/softbinding/manifests/{id}`, which returns the signed manifest
  bytes to anyone: `model_id` and `model_version` reach `softwareAgent`,
  `generated_at` reaches `c2pa.created.when`, and `ai_provider` and
  `operations` reach the `eu.art50.provenance` assertion (Section 1, Measure
  1.3). Do not put secrets in any of them. `operations` is a **closed
  vocabulary** rather than free text precisely because it is published:
  Sub-measure 1.1.1 requires richer metadata to be added "without including
  privacy-sensitive or business-sensitive information", and a free-text field
  on a public manifest is where a prompt ends up. The full list is in
  [`docs/guides/03-api-integration.md`](../guides/03-api-integration.md).

---

## 9. Not detrimental to robustness

The interoperability solution embeds **nothing** in the content. It is a lookup
over routing codes already issued at labeling time. The figures in
[`marking-detection-test-report.md`](marking-detection-test-report.md) are the
figures **before and after P10.2** — the Measure's condition is satisfied by
construction, not by re-measurement.

---

## 10. Unnecessary traffic

Measure 3.4(c)(i) requires that signatories "shall not create unnecessary
traffic to other services" where a known interoperability solution can be read
directly. This obligation binds us as a **client** of other providers'
resolution endpoints. We broadcast no queries to anyone; we only answer queries
sent to us.

---

## 11. What is not implemented, and why

| Not doing | Why |
|---|---|
| A second, signpost watermark (limb ii) | Costs robustness the Measure forbids spending; circular for images. |
| A consortium detection solution (limb iii) | Not a thing one signatory can implement. If one forms, joining is a config change. |
| The C2PA **decentralized lookup** (`smartContract` / `eventLog`) | The only standardised endpoint-discovery mechanism in the C2PA spec, and it is a distributed-ledger dependency. Rejected on the "no new services" constraint. |
| `POST /matches/byReference` | Fetching an attacker-supplied HTTPS URL from a public unauthenticated endpoint is server-side request forgery. |
| The storage routes (`POST /manifests`, `POST/PUT/DELETE /bindings`) | We resolve only bindings we minted. Accepting third-party bindings is the misuse risk 3.4(c) asks us to guard against. |
| ~~Loading the official C2PA trust list~~ — **done** | It was routed to P13.2 on the grounds that an empty trust store made `ValidationState::Trusted` unreachable. It is no longer empty: `signer/src/trust.rs` embeds the Program's Trust List and TSA Trust List and builds two contexts, so a third-party manifest signed by Adobe or Google now reaches `Trusted`, and our own reaches it only in the second context, which never presents itself as third-party endorsement. |
| Reading *other* providers' markings in `/detect` | This plan makes us *answerable*, not omniscient. |
| Text (`com.writerslogic.*`, `com.cognitive-proof.vsrmark.1`) | P9.3 ships no text watermark. Nothing to bind. See [`text-watermarking-gap.md`](text-watermarking-gap.md). |

---

## 12. Registry submission procedure

The video and audio entries are prepared in
`docs/compliance/softbinding-algorithm-entries.json`.

To submit:

1. Open a pull request against
   [`github.com/c2pa-org/softbinding-algorithm-list`](https://github.com/c2pa-org/softbinding-algorithm-list)
   adding both objects to `softbinding-algorithm-list.json`.
2. The C2PA Technical Working Group assigns an `identifier` during review.
3. When the PR merges, update the status table in this document, copy it to
   `marketing/public/docs/compliance/detection-solution.md` (a test enforces
   that the two stay byte-identical), and update the status strings in
   `marketing/src/app/detection-solution/page.tsx`.

There is deliberately **no deployment setting** to flip. Registration status
is a fact about the C2PA registry, not about this endpoint, and the three
files above are the only places it is recorded — a flag that no code reads
would let a maintainer believe they had updated the published status when
they had only changed an environment variable.

The endpoint answers for `eu.trion.videoseal.1` and `eu.trion.audioseal.1`
either way; registration makes them discoverable from the normative registry
without reading this document.

---

## 13. Contact and cooperation

Measure 3.4(d) asks signatories to cooperate in good faith with the Commission
and relevant stakeholders towards improved interoperability solutions and
standards.

- **Integration questions and detect-key requests:**
  [compliance@trionsecurity.com](mailto:compliance@trionsecurity.com)
- **Measure 3.5** (advancing the state of the art through public benchmarks,
  reference datasets and cooperation): we have **not** joined the optional
  cooperation activities or task forces that Measure 3.5 describes. This
  document and the API it describes are our contribution under Measures
  3.4(b) and 3.4(c)(i).

---

## 14. Retirement and backward compatibility (Sub-measure 2.1.4)

A detection solution may be retired **only** if replaced by one that yields the
same or better detection capabilities and is **backward compatible**, enabling
detection of previously marked content.

Backward compatibility here requires the replacement to still resolve:

- `com.adobe.trustmark.P` and `com.adobe.trustmark.Q` image codes
- `eu.trion.videoseal.1` and `eu.trion.audioseal.1` payloads
- C2PA manifests signed by tenant certificate chains
- the `Manifest.wm_code` routing table

**Without replacement:** if the solution stops being provided, it is made
available to competent **market surveillance** authorities under confidentiality
and security safeguards. Artefacts handed over:

- extraction code paths (`api/src/label/imperceptible.py`, `video_watermark.py`,
  `audio_watermark.py`, `detect.py`)
- model checkpoints: trustmark 0.9.1 (Q), videoseal 1.0.1, audioseal 0.2.0
- `wm_code` → manifest routing table
- tenant signing-certificate chains and the private **CA root** for legacy validation

All three checkpoints are third-party artefacts under **MIT** (P8.1 licence
clearance), so they may be redistributed. A redistribution-restricted checkpoint
would have made this commitment impossible to keep.

**Operational note:** the signer CA volume is the single point of failure; guide 04
Phase 3 asks the operator to document custody and back it up.

---

## 15. What this document is not

This document records what the platform does and how to integrate with it. It
does not certify legal sufficiency for any customer's use of the platform.
