Your API Is Stripping C2PA Metadata: How to Catch It With a Test

Claude, Gemini and OpenAI now attach signed C2PA manifests to generated files, so real provenance is arriving at your upload endpoint and your pipeline is probably deleting it. A two-minute curl proves it, and a three-layer test keeps it fixed.

Ashley Innocent

Ashley Innocent

12 August 2026

Your API Is Stripping C2PA Metadata: How to Catch It With a Test

Apidog for Enterprise

On-Premises Deploy

SSO & RBAC

SOC 2 Compliant

Explore Apidog Enterprise

Claude now attaches signed C2PA provenance metadata to the files it generates. So do OpenAI’s image models, and so does Gemini. Which means a real provenance signal is arriving at your upload endpoint for the first time, and there’s a good chance your pipeline is deleting it before anyone sees it.

Not maliciously. By default. sharp().resize() produces a clean file with no metadata unless you ask otherwise. So does ImageMagick. So does Pillow. So do most image CDNs. The manifest goes in, a smaller JPEG comes out, and nothing in your logs mentions it.

This is a testable failure, and the test is not complicated. Here’s how metadata dies in a normal pipeline, how to prove it’s happening, and how to wire a round-trip check into CI so it can’t come back. Apidog handles the orchestration; c2patool handles the byte-level verification.

button

What actually gets destroyed

A C2PA manifest is a cryptographically signed block embedded in the file container. It records who signed the asset and what was claimed about it, and because it’s signed, altering the bytes without re-signing breaks the signature in a way any reader can detect.

Container-level is the operative phrase. Rewrite the container and the manifest is gone.

Operation Manifest survives by default?
Byte-for-byte copy or move Yes
sharp().resize().toBuffer() No
ImageMagick convert / magick No
Pillow Image.save() No
PNG to WebP, JPEG to AVIF No
Image CDN auto-optimization Usually no
Screenshot No
Re-save from an image editor No
S3 upload with no transformation Yes

Everything in the “no” column is something a normal web app does to every image it accepts. Thumbnails, responsive variants, format negotiation, EXIF scrubbing for privacy. Each one is reasonable in isolation, and each one silently ends the provenance chain.

Worth noting: the privacy-motivated -strip habit is often deliberate, because EXIF carries GPS coordinates and camera serials. Stripping all metadata to remove location data also removes the provenance manifest. Those two goals now conflict, and resolving it means being selective rather than nuking the whole block.

Prove it in two minutes

Before building anything, confirm you have the problem. You need one file with a valid manifest. Any image Claude generates works, or grab a signed sample from the Content Authenticity Initiative.

Install the reference CLI:

cargo install c2patool

Check the fixture is actually signed:

c2patool fixtures/signed-sample.png

You should get a JSON report naming the claim generator and the signature status. Now push it through your own stack and check the other end:

# Upload through your real endpoint
curl -sS -X POST https://api.example.com/v1/assets \
  -H "Authorization: Bearer $API_TOKEN" \
  -F "file=@fixtures/signed-sample.png" \
  -o /tmp/upload.json

# Fetch it back through the URL your frontend would use
ASSET_URL=$(jq -r '.url' /tmp/upload.json)
curl -sS "$ASSET_URL" -o /tmp/roundtrip.png

# Did the manifest survive?
c2patool /tmp/roundtrip.png

Three possible outcomes, and they mean different things:

That third outcome is the one to hunt for. It usually means a transformation library preserved the metadata block while rewriting the pixels.

Find the step that does it

If the round trip fails, bisect the pipeline. Check the manifest immediately after each stage rather than guessing.

Typical suspects in order of likelihood:

1. The resize or thumbnail step. Most likely culprit. In sharp, metadata is dropped unless you keep it explicitly:

// Drops the C2PA manifest
await sharp(input).resize(1200).toFile(output);

// Preserves the metadata block
await sharp(input).resize(1200).keepMetadata().toFile(output);

Preserving the block is necessary but not sufficient. The pixels changed, so the original signature no longer validates against the new bytes. To keep a working provenance chain you re-sign the output and record the transformation as an action assertion, typically c2pa.resized. The c2pa libraries for Rust, Python, JavaScript, and C all support this.

2. Format conversion. Serving AVIF or WebP means a new container. Same rule: preserve and re-sign, or accept the chain ends there and say so.

3. The CDN. Many image CDNs rewrite on delivery. Some now preserve and re-sign Content Credentials natively; most historically stripped them. Test through the delivery URL your users actually hit, not through origin, or you’ll get a green result that means nothing.

4. Upload normalization. Services that re-encode on ingest to standardize formats are easy to forget, because the code lives in an infrastructure repo nobody reads.

Make it a permanent test

A one-off curl proves the state today. It doesn’t stop someone adding a resize step next sprint. The check needs to live in CI.

Split it into two layers, because two different tools are good at two different things.

Layer one: the round trip, in Apidog

The orchestration is a normal chained API test: upload a fixture, capture the returned URL, fetch the asset back through the real delivery path, assert on what comes back.

In Apidog this is a test scenario with two steps.

Step 1: POST /v1/assets

const body = pm.response.json();
pm.environment.set("ASSET_URL", body.url);
pm.test("upload returns a delivery URL", function () {
    pm.expect(body.url).to.be.a("string").and.to.include("https://");
});

Step 2: GET {{ASSET_URL}}

const uploadedBytes = Number(pm.environment.get("FIXTURE_BYTES"));
const returnedBytes = pm.response.responseSize;
pm.test("asset was not silently re-encoded", function () {
    pm.expect(returnedBytes).to.be.above(uploadedBytes * 0.9);
});

Size is a heuristic, not proof. It catches the loud failures cheaply and it runs in the same suite as everything else. Standard assertion patterns are covered in API assertions.

Layer two: the byte-level check, in CI

Verifying a signature means parsing the container, which is c2patool’s job, not an HTTP client’s. Run it as a pipeline step against the file the round trip fetched:

# .github/workflows/provenance.yml
name: provenance
on: [pull_request]

jobs:
  c2pa-round-trip:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install c2patool
        run: cargo install c2patool

      - name: Install Apidog CLI
        run: npm install -g apidog-cli

      - name: Run the round-trip scenario
        run: |
          apidog run --access-token "$APIDOG_ACCESS_TOKEN" \
            -t "$SCENARIO_ID" -e "$ENV_ID" -r cli,html --out-dir ./apidog-reports
        env:
          APIDOG_ACCESS_TOKEN: ${{ secrets.APIDOG_ACCESS_TOKEN }}
          SCENARIO_ID: ${{ vars.PROVENANCE_SCENARIO_ID }}
          ENV_ID: ${{ vars.APIDOG_ENV_ID }}

      - name: Verify the manifest survived
        run: |
          set -euo pipefail
          curl -sS "$ASSET_URL" -o /tmp/roundtrip.png
          c2patool /tmp/roundtrip.png > /tmp/report.json
          jq -e '.validation_status == null or (.validation_status | length) == 0' /tmp/report.json

set -euo pipefail matters. Without it, c2patool failing on a stripped file produces a warning and a green build, which is the exact failure you were trying to prevent. If you’re new to running Apidog scenarios in a pipeline, automating API tests in GitHub Actions covers the setup.

Layer three, optional: a verification endpoint

If provenance is a product feature rather than an internal check, the cleanest design is a small endpoint in your own service that runs the c2pa library and returns a structured result. Then the whole thing is testable as ordinary JSON, and your frontend gets a real answer instead of a guess.

{
  "asset_id": "img_9f2c41",
  "provenance": {
    "status": "verified",
    "standard": "c2pa",
    "signer": "Anthropic",
    "signature_valid": true,
    "checked_at": "2026-08-11T09:14:22Z",
    "tool": "c2patool/0.9"
  }
}

Keep three states, not two. verified, absent, and invalid mean genuinely different things, and collapsing absent and invalid into one boolean throws away the most interesting signal you have. Add unchecked if your verifier can be unavailable, so an outage doesn’t masquerade as a clean result.

Document the shape in your OpenAPI definition and validate against it, so the fields can’t disappear in a refactor. How to validate OpenAPI specs covers that side.

The four fixtures worth keeping

A provenance suite needs deliberately broken inputs, not just a happy path.

  1. Valid signed file. Expect verified. Catches over-eager stripping.
  2. Stripped file. Same image, manifest removed with exiftool -all=. Expect absent, not an error and definitely not verified.
  3. Tampered file. Signed file with a byte altered after signing. Expect invalid. This is the one that proves you’re checking the signature rather than just checking a block exists.
  4. Unsupported format. Something with no manifest support at all. Expect a clean absent rather than a 500.

Commit all four to the repo next to the test scenario. They’re small, they never change, and they’re the difference between a test that passes and a test that means something.

Why bother

Three reasons, in ascending order of how much they’ll cost you.

Your product claim. If your UI shows a provenance badge, and your pipeline strips manifests, the badge is wrong for every asset that went through a resize. That’s a trust problem you’ll find out about from a user.

Your compliance story. If you’re relying on C2PA for anything Article 50 related, a stripped manifest is a control that isn’t working. The provider and deployer split in EU AI Act Article 50 for API developers explains which duties actually sit with you.

The signal itself. Provenance only works if the chain holds end to end. Every pipeline that quietly drops manifests makes the whole ecosystem less useful, including for you when you’re the one trying to verify something.

Download Apidog to build the round-trip scenario against your own endpoints, then wire the c2patool step in behind it.

FAQ

Does resizing an image remove C2PA metadata? Yes, by default in every common library. Preserving the metadata block requires an explicit flag, and keeping a valid signature requires re-signing the transformed output.

How do I check if a file has C2PA metadata? Run c2patool <file> from the command line, or drop the file into the Content Credentials verification page.

Can I keep C2PA metadata through a resize? Yes, but not by preserving alone. Preserve the block, then re-sign the output with an action assertion such as c2pa.resized using one of the c2pa libraries. Otherwise the old signature no longer matches the new bytes.

Do CDNs strip Content Credentials? Many do when they auto-optimize. Some now preserve and re-sign natively. Test through the delivery URL your users hit, not through origin.

What’s the difference between a stripped manifest and an invalid one? Stripped means no manifest was found, which tells you nothing about the file’s origin. Invalid means a manifest exists but its signature doesn’t match the bytes, which means the file changed after signing. Keep them as separate states.

Can Apidog verify a C2PA signature directly? It orchestrates the round trip and asserts on HTTP responses, including a verification endpoint’s JSON. The signature parsing itself is c2patool’s job, run as a CI step or inside your own service. Use both together.

Should I strip EXIF for privacy but keep C2PA? That’s the right goal and it needs a selective approach. A blanket -strip removes both. Remove the EXIF blocks you care about specifically and leave the C2PA manifest intact.

The takeaway

Provenance metadata arrives at your API intact and usually leaves in pieces, and nothing in your monitoring will tell you. The fix is a fixture, a round trip through the real delivery path, and a c2patool check that fails the build.

Twenty minutes of setup, and it turns a claim you’re making in your UI into a guarantee your pipeline actually enforces.

button

Explore more

Is Claude Opus 5.5 Free? (And the Cheapest Paid Path)

Is Claude Opus 5.5 Free? (And the Cheapest Paid Path)

No: Claude Free gets Sonnet and Haiku. Opus 5.5 needs Pro from $17/month. The free routes that do exist, plus the cheapest paid path via caching and batch.

23 September 2026

Is GPT-6 Sol Free? (And the Cheapest Way to Run It)

Is GPT-6 Sol Free? (And the Cheapest Way to Run It)

GPT-6 Sol is not free: it needs Plus, Pro, Business, Enterprise or Edu in ChatGPT Work and Codex. GPT-6 Luna is free in the desktop app. Plus the cheapest paid path at $2/$10.

23 September 2026

How to Use GPT-6 Luna for Free ?

How to Use GPT-6 Luna for Free ?

GPT-6 Luna is genuinely free: Free and Go users get it in the ChatGPT desktop app, not in Chat, and not GPT-6 Sol. Here is the exact boundary, what the free tier leaves out, and the cheapest paid path at $0.10/$0.50 per 1M with 90% off cached reads.

23 September 2026

Practice API Design-first in Apidog

Discover an easier way to build and use APIs

Your API Is Stripping C2PA Metadata: How to Catch It With a Test