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.
File metadata stripping REST API & CLI
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.
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
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.
%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.
PDF outputs are re-parsed and byte-scanned to confirm Author, Title, Subject, Creator, Keywords, Producer,
XMP, and /Annots are absent before returning 200.
sha256 and removed classes.
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.
Format-specific execution
Each upload is inspected by magic bytes, routed to a deterministic sanitizer, checked for residual identifiers, and served with non-rendering download headers.
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
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
Strips EXIF, IPTC, XMP, GPS coordinates, and macOS Finder
comments with exiftool -all= -MDItemFinderComment=,
rejecting unwritable containers with 422.
exif · gps · finder_comment
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.
The HTTP contract
Every JSON success response uses the same five root keys. Every
error returns application/problem+json with a
machine-readable code and advice_code.
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"]
}
}
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"
}
code and advice_code enums
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
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
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
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.
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.
Local CLI usage requires ffmpeg, exiftool, and qpdf on your PATH.
The hosted API bundles all three inside the Cloudflare container.
# 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
anonymize-api.zysys.org