File metadata stripping REST API & CLI

Strip identifying metadata.
Verify before you share.

Anonymizer removes author fields, XMP packets, annotations, embedded cover art, chapters, and EXIF tags from PDFs, audio, and images—and verifies the output bytes before marking a job completed.

No original filenames echoed. No unverified tool exits. Replayable for 24 hours with a UUID v4 idempotency key.

OpenAPI 3.1 RFC 7807 errors 50 MiB upload ceiling
Create, verify, and download REST · /v1/anonymizations
# Strip metadata with a retryable UUID v4 key
$ curl -sS https://anonymize-api.zysys.org/v1/anonymizations \
  -H "Authorization: Bearer $ANONYMIZER_LIVE_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -F "file=@q3-board-notes.pdf;type=application/pdf"
# Download verified bytes with attachment + nosniff headers
$ curl -sS -X POST \
  https://anonymize-api.zysys.org/v1/anonymizations/anon_4fjK2nQ7mX1a/content \
  -H "Authorization: Bearer $ANONYMIZER_LIVE_KEY" \
  -o anon_4fjK2nQ7mX1a.pdf

A file is not anonymized because a media tool exited zero. It is completed when re-parsing the output confirms the identifying fields are gone.

Why a dedicated boundary

Tools exit zero even when metadata survives.

Calling a PDF or EXIF helper directly can leave Info dictionaries, producer stamps, embedded cover streams, or original filenames intact unless every format enforces a post-condition.

01 · Routing

Magic bytes beat extensions.

%PDF-, PNG, JPEG, WAV, FLAC, and ID3/MP3 signatures select the sanitizer before a filename extension or application/octet-stream header can misroute a file.

Mislabeled PDFs still run the full PDF rewrite.
02 · Verification

Post-conditions gate completion.

PDF outputs are re-parsed and byte-scanned to confirm Author, Title, Subject, Creator, Keywords, Producer, XMP, and /Annots are absent before returning 200.

Every resource includes sha256 and removed classes.
03 · Isolation

Principal-scoped retention.

Accepted outputs and replay keys are scoped by credential digest and mode for 24 hours, with neutral <id>.<ext> filenames that never echo the uploader's original name.

128 MiB and 1,024 retained results per container.

Format-specific execution

Four stages. Checked on the way out.

Each upload is inspected by magic bytes, routed to a deterministic sanitizer, checked for residual identifiers, and served with non-rendering download headers.

01PDF

Page-only linearize & scrub

Clears Info dictionaries, XMP catalog streams, page annotations, JavaScript name trees, and embedded files, then runs qpdf --empty --linearize --object-streams=disable and re-parses the output.

info · xmp · annotations
02Audio

320 kbps stereo re-encode

Maps only the first audio stream (-map 0:a:0), drops all container and stream tags, strips chapters and attached cover art, and encodes bit-exact 44.1 kHz stereo MP3, WAV, or FLAC.

audio_tags · chapters · streams
03Image

In-place EXIF & comment clear

Strips EXIF, IPTC, XMP, GPS coordinates, and macOS Finder comments with exiftool -all= -MDItemFinderComment=, rejecting unwritable containers with 422.

exif · gps · finder_comment
04Delivery

Attachment-only download

Serves raw anonymized bytes from /v1/anonymizations/{id}/content with Content-Disposition: attachment and X-Content-Type-Options: nosniff.

attachment · nosniff

When a format cannot be stripped safely, the API returns 422 unsupported_type with advice_code: do_not_retry instead of storing untouched bytes.

%PDF- · audio/* · image/* → verified

The HTTP contract

Five-field envelope. RFC 7807 errors.

Every JSON success response uses the same five root keys. Every error returns application/problem+json with a machine-readable code and advice_code.

200 OK · AnonymizationEnvelope5 root keys

Neutral filename and output digest

The response echoes your Idempotency-Key, reports the matched key mode, and returns a neutral filename plus the output SHA-256 and stripped classes.

{
  "object": "anonymization",
  "request_id": "req_01HZK2N5GQR9T8X4B6FJW3Y1AS",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
  "livemode": true,
  "data": {
    "id": "anon_4fjK2nQ7mX1a",
    "status": "completed",
    "filename": "anon_4fjK2nQ7mX1a.pdf",
    "file_type": "application/pdf",
    "bytes": 48291,
    "created": "2026-10-31T14:32:01Z",
    "sha256": "d8926a01cd9800ada836058490125cdb...",
    "removed": ["info", "xmp", "annotations"]
  }
}
Retained for 24 h · scoped to the creating credential
4xx / 5xx · ProblemDetailsRFC 7807

Actionable failure semantics

Deterministic input errors tell callers not to retry. Capacity and concurrency limits return Retry-After: 5 without evicting accepted results or poisoning the key.

{
  "type": "about:blank",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "The uploaded file type or format is not supported for anonymization.",
  "code": "unsupported_type",
  "advice_code": "do_not_retry",
  "request_id": "req_01HZK2N5GQR9T8X4B6FJW3Y1AS"
}
Stable code and advice_code enums

One create call, safely retryable for 24 hours.

Replaying the same UUID v4 key and body returns the identical resource without running a second encode. Reusing a key with different bytes returns 409 idempotency_conflict.

curl -sS https://anonymize-api.zysys.org/v1/anonymizations \
  -H "Authorization: Bearer $ANONYMIZER_LIVE_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -F "file=@q3-board-notes.pdf;type=application/pdf"

curl -sS -X POST https://anonymize-api.zysys.org/v1/anonymizations/anon_4fjK2nQ7mX1a/content \
  -H "Authorization: Bearer $ANONYMIZER_LIVE_KEY" \
  -o anon_4fjK2nQ7mX1a.pdf
Runtime limits50 MiB · upload ceiling2 jobs · concurrent encodes128 MiB / 1,024 · retained results24 h · replay windowOpenAPI 3.1 spec ↗

Idempotent followers waiting on an in-flight encode do not consume an encode slot. Lookups and downloads bypass the encode concurrency limiter.

Operations & error codes

Four routes. Explicit status codes.

All mutating and lookup routes use POST with bearer authentication. Health checks verify both API key configuration and native tool availability.

GET /healthzPublic readiness probe

Checks that API keys are configured and qpdf, exiftool, and ffmpeg are executable on PATH.

200 {"status":"ok"} or 503 dependency_unavailable.

POST /v1/anonymizationsMultipart create + replay

Accepts file (up to 50 MiB) and optional type with a required UUID v4 Idempotency-Key header.

200 envelope · 400 · 401 · 409 · 413 · 422 · 429 · 503.

POST /v1/anonymizations/{id}Resource metadata lookup

Returns the five-field JSON envelope for a retained job created by the same credential and mode.

200 envelope · 401 unauthorized · 404 not_found.

POST /v1/anonymizations/{id}/contentRaw binary download

Streams the anonymized file bytes with the detected Content-Type, Content-Disposition: attachment, and nosniff.

200 binary stream · 401 unauthorized · 404 not_found.

Get started

Call the hosted API or run the CLI locally.

The same Go engine powers anonymize-api.zysys.org and the standalone anonymizer CLI. Generate a typed client from openapi.yaml or call the endpoints with standard HTTP.

One engine for HTTP and local pipelines.

Live keys use the anon_live_ prefix; test keys use anon_test_. Both modes run the full stripping and verification pipeline while keeping retained jobs isolated.

REST API + Go CLIOpenAPI 3.1RFC 7807 errors
Read the repository

Local CLI usage requires ffmpeg, exiftool, and qpdf on your PATH. The hosted API bundles all three inside the Cloudflare container.

CLI & health check
# Install the standalone Go CLI
go install github.com/zachbornheimer/anonymizer/cmd/anonymizer@main

# Strip a file in place on your machine
anonymizer q3-board-notes.pdf

# Verify the hosted API readiness probe
curl -sS https://anonymize-api.zysys.org/healthz
Hosted on Cloudflare Workers & Containers · anonymize-api.zysys.org