DEV Community

Cover image for Your API Is Stripping C2PA Metadata: How to Catch It With a Test
Hassann
Hassann

Posted on Originally published at apidog.com

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

Claude now attaches signed C2PA provenance metadata to the files it generates. OpenAI’s image models do too, and so does Gemini. That means your upload endpoint may receive a real provenance signal for the first time—and your image pipeline may be deleting it before anyone can verify it.

Try Apidog today

This usually is not intentional. By default, sharp().resize() writes a clean file without metadata unless you explicitly retain it. ImageMagick, Pillow, and many image CDNs behave similarly. A signed manifest goes in, an optimized JPEG comes out, and your logs remain silent.

You can test this failure mode. This guide shows how metadata is lost in a typical image pipeline, how to identify the exact stage responsible, and how to add a CI round-trip check. Use Apidog to orchestrate the API flow and c2patool to verify the file bytes.

What actually gets destroyed

A C2PA manifest is a cryptographically signed block embedded in an asset’s file container. It records information about the signer and the asset’s claimed provenance. Because it is signed, changing the bytes without re-signing causes validation to fail.

The key detail is that C2PA data is container-level metadata. Rewrite the container, and the manifest is typically 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

The “no” column covers standard web-image processing: thumbnails, responsive variants, format negotiation, and EXIF scrubbing. Each operation is reasonable, but each can silently end the provenance chain.

There is also a privacy trade-off. A blanket -strip operation can remove EXIF data such as GPS coordinates and camera serial numbers, but it can also remove the C2PA manifest. If you need privacy controls and provenance, remove only the metadata blocks you do not need instead of stripping everything.

Prove it in two minutes

First, verify that your pipeline has the problem.

Use a file with a valid C2PA manifest. For example, use an image generated by Claude or a signed sample from the Content Authenticity Initiative.

Install the reference CLI:

cargo install c2patool
Enter fullscreen mode Exit fullscreen mode

Verify the fixture before uploading it:

c2patool fixtures/signed-sample.png
Enter fullscreen mode Exit fullscreen mode

You should receive a JSON report with the claim generator and signature status.

Next, send that same file through your real upload and delivery path:

# 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 through the URL used by your frontend
ASSET_URL=$(jq -r '.url' /tmp/upload.json)
curl -sS "$ASSET_URL" -o /tmp/roundtrip.png

# Verify the returned file
c2patool /tmp/roundtrip.png
Enter fullscreen mode Exit fullscreen mode

Interpret the result carefully:

  • Valid report: the manifest survived.
  • No manifest found: a pipeline stage stripped the manifest.
  • Validation error: the manifest is still present, but its signature no longer matches the file bytes.

The last outcome deserves immediate investigation. It commonly means a transformation library retained a metadata block while changing the pixels. Downstream verifiers will treat that as tampering.

Find the step that removes metadata

If the round trip fails, do not guess. Check the manifest after every transformation stage:

  1. Original upload
  2. Ingest normalization
  3. Thumbnail generation
  4. Format conversion
  5. Object storage write
  6. CDN delivery URL

The most common failure points are below.

1. Resize and thumbnail generation

In sharp, metadata is dropped unless you retain it explicitly:

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

// Retains the metadata block
await sharp(input).resize(1200).keepMetadata().toFile(output);
Enter fullscreen mode Exit fullscreen mode

Keeping metadata is necessary, but it is not enough to preserve a valid provenance chain. The pixels changed, so the original signature no longer validates against the transformed output.

To preserve verifiable provenance after a resize:

  1. Retain the relevant metadata.
  2. Re-sign the generated file.
  3. Add an action assertion describing the transformation, typically c2pa.resized.

The c2pa libraries for Rust, Python, JavaScript, and C support this workflow.

2. Format conversion

Converting PNG to WebP or JPEG to AVIF creates a new container. Apply the same rule:

  • Preserve metadata and re-sign the transformed file, or
  • Treat the conversion as the end of the provenance chain and document that behavior.

3. CDN optimization

Many image CDNs transform assets during delivery. Some preserve and re-sign Content Credentials natively, while many historically stripped them.

Always test the public delivery URL your users access. Testing only the origin file can give you a passing result that does not reflect production behavior.

4. Upload normalization

Ingest pipelines often re-encode uploads to standardize dimensions, formats, or quality. These transformations are easy to miss because they may live in a separate infrastructure service.

Add a c2patool check immediately after normalization, not only after delivery.

Make the check permanent

A manual curl test proves the current state. It does not prevent a future resize step from breaking provenance.

Add the check to CI in two layers:

  1. An API round trip that uploads and fetches the file.
  2. A byte-level signature check against the downloaded output.

Layer one: run the round trip in Apidog

The API scenario is straightforward:

  1. Upload a signed fixture.
  2. Capture the returned delivery URL.
  3. Fetch the asset through the real delivery path.
  4. Check status, content type, and size.

Step 1: POST /v1/assets

Configure a multipart/form-data request with the signed fixture.

The setup is the same as testing file upload APIs.

Add assertions for:

  • HTTP status 201
  • Expected response schema
  • A usable delivery URL

Use a post-response script to save the URL for the next request:

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://");
});
Enter fullscreen mode Exit fullscreen mode

Step 2: GET {{ASSET_URL}}

Assert that:

  • The response status is 200.
  • Content-Type matches the expected output format.
  • The returned file size is close to the uploaded file size.
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);
});
Enter fullscreen mode Exit fullscreen mode

Size is only a heuristic, not cryptographic verification. It catches obvious re-encoding failures cheaply and keeps the signal in the same suite as your other API checks.

For more assertion patterns, see API assertions.

Layer two: verify bytes in CI

An HTTP test client should orchestrate requests, but signature verification requires parsing the asset container. That is c2patool’s job.

Run it in CI against the file fetched during the round trip:

# .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
Enter fullscreen mode Exit fullscreen mode

set -euo pipefail is important. Without it, a failed c2patool command can become a warning while the workflow still passes—the exact outcome this test is meant to prevent.

For Apidog pipeline setup, see automating API tests in GitHub Actions.

Layer three: expose verification as an endpoint

If provenance verification is a product feature rather than only an internal control, add a small verification endpoint to your service.

The endpoint can run a c2pa library and return structured JSON:

{
  "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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Keep at least three distinct result states:

Status Meaning
verified A manifest exists and its signature validates.
absent No manifest was found.
invalid A manifest exists, but validation failed.

Do not collapse absent and invalid into one boolean. They represent different operational and security signals.

If verification can fail because the verifier is unavailable, add an unchecked status so an outage is not reported as a clean result.

Document this response shape in your OpenAPI definition and validate it in CI. See How to validate OpenAPI specs.

Keep these four fixtures

A useful provenance test suite needs broken inputs, not only a happy path.

  1. Valid signed file

    Expect verified. This catches accidental stripping.

  2. Stripped file

    Use the same image but remove metadata with exiftool -all=. Expect absent, not an error and never verified.

  3. Tampered file

    Modify a byte after signing. Expect invalid. This proves you validate the signature instead of merely checking for a metadata block.

  4. Unsupported format

    Use a format with no manifest support. Expect a clean absent result rather than a 500.

Commit these fixtures alongside the API scenario. They are small, stable, and ensure your tests validate real behavior.

Why this matters

Your product claim

If your UI displays a provenance badge but your resize pipeline strips manifests, that badge is wrong for transformed assets. Users will eventually discover the mismatch.

Your compliance controls

If you rely on C2PA for Article 50-related workflows, stripping manifests means a control is not functioning as intended. The provider and deployer responsibilities are covered in EU AI Act Article 50 for API developers.

The provenance signal

Provenance works only when the chain remains intact from generation through delivery. Pipelines that quietly remove manifests reduce the usefulness of the ecosystem for everyone, including teams that need to verify assets later.

Download Apidog to build the API round-trip scenario against your endpoints, then add c2patool verification behind it in CI.

FAQ

Does resizing an image remove C2PA metadata?

Yes, by default in common image libraries. Retaining the metadata block requires an explicit option, and maintaining a valid signature requires re-signing the transformed output.

How do I check whether a file has C2PA metadata?

Run:

c2patool <file>
Enter fullscreen mode Exit fullscreen mode

You can also upload the file to the Content Credentials verification page.

Can I preserve C2PA metadata through a resize?

Yes, but metadata preservation alone is insufficient. Preserve the block, then re-sign the transformed output with an action assertion such as c2pa.resized using a c2pa library. Otherwise, the original signature does not match the new bytes.

Do CDNs strip Content Credentials?

Many do when auto-optimization is enabled. Some now preserve and re-sign Content Credentials natively. Test the delivery URL your users receive, not only the origin file.

What is the difference between a stripped and invalid manifest?

A stripped manifest means no manifest was found, so there is no provenance signal to verify. An invalid manifest means a manifest exists but its signature does not match the asset bytes, indicating that the asset changed after signing.

Keep those states separate.

Can Apidog verify a C2PA signature directly?

Apidog can orchestrate the upload/download round trip and assert HTTP responses, including JSON from your own verification endpoint. Use c2patool or a c2pa library for container parsing and signature verification.

Should I strip EXIF for privacy but retain C2PA?

Yes. Use selective metadata removal. A blanket -strip operation can remove both EXIF and C2PA data, so remove the specific EXIF blocks you need to exclude while retaining the provenance manifest.

The takeaway

C2PA provenance metadata can arrive at your API intact and leave your delivery pipeline missing or invalid, with no monitoring alert to tell you.

The practical fix is:

  1. Keep a signed fixture.
  2. Upload it through the real endpoint.
  3. Download it through the real delivery URL.
  4. Run c2patool against the returned file.
  5. Fail CI when the manifest is absent or invalid.

A short setup turns a provenance claim in your UI into a guarantee your pipeline actively enforces.

Top comments (0)