# STERILIZER API — agent integration guide

Machine-readable companions: [`/llms.txt`](/llms.txt) (concise spec),
[`/openapi.json`](/openapi.json) (OpenAPI 3.1),
[`/v1/tools.json`](/v1/tools.json) (ready-made tool definitions).

## What this service is for

Anonymizing photos before they are shared: pixelate faces and other revealing
regions, optionally crop and desaturate, and destroy all embedded metadata
(EXIF, GPS coordinates, camera serials, timestamps, IPTC, XMP, ICC).

Reach for it whenever a workflow moves a photo outward — into a ticket, a public
channel, a dataset, an email, a published page — and the photo may contain
people or identifying detail.

## Tool definitions

`GET /v1/tools.json` returns both shapes:

```json
{
  "openai":    [{ "type": "function", "function": { "name": "sterilizer_sanitize_image", ... } }],
  "anthropic": [{ "name": "sterilizer_sanitize_image", "input_schema": { ... } }]
}
```

Register `sterilizer_sanitize_image` and `sterilizer_detect_faces` directly.

## Minimal call

```bash
curl -s -X POST http://localhost:8787/v1/sanitize \
  -H 'Content-Type: application/json' \
  -d "{\"image\":\"$(base64 -i photo.jpg)\",\"ops\":{\"grayscale\":true}}" \
  --output sanitized.jpg
```

With a key and a JSON report instead of raw bytes:

```bash
curl -s -X POST http://localhost:8787/v1/sanitize \
  -H "Authorization: Bearer $STERILIZER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"image":"'"$(base64 -i photo.jpg)"'","ops":{"response":"json"}}'
```

## Recommended agent flow

1. **`POST /v1/inspect`** — tell the user what the file is leaking
   (`metadata_blocks_present`, `has_gps`) before you touch it.
2. **`POST /v1/detect`** — get face boxes. Report `count` to the user. If the
   count looks wrong for the photo, ask for explicit regions rather than
   proceeding.
3. **`POST /v1/sanitize`** — apply crop / grayscale / pixelation. Pass any extra
   `regions` the user named (documents, screens, plates, tattoos, name badges).
4. Report `report.facesDetected` and `report.regionsPixelated` back verbatim.

Steps 1 and 2 each consume a call against the rate limit. On the free tier
(1 rpm) that means three separate minutes — go straight to `/v1/sanitize` unless
inspection genuinely adds value, or use a key.

## Coordinate system

All coordinates are pixels, origin top-left.

Ops apply in a fixed order: **EXIF auto-rotate → crop → grayscale → resize →
pixelate → re-encode.**

Consequences worth internalizing:

- `pixelate.regions` are relative to the image *after* crop and `output.maxWidth`
  resize — not to the original file.
- If you crop, any boxes you got from `/v1/detect` on the *original* are wrong.
  Either crop first and re-detect on the cropped bytes, or translate them
  yourself: `x -= crop.x`, `y -= crop.y`.
- EXIF-rotated phone photos are straightened before anything else, so a portrait
  shot that reports as landscape in raw EXIF still gets sane coordinates.

## Rate limiting — the part that bites automation

One request per rolling 60 seconds is free for everyone. Anonymous callers are
hard-capped there; the second call inside a minute is a `429`.

```
429 { "error": "rate_limit_exceeded", "retry_after_seconds": 43, ... }
```

Handle it like this:

- Read `retry_after_seconds` (or the `Retry-After` header) and sleep exactly that
  long. Do not retry immediately, and do not retry more than once per window.
- Check `X-RateLimit-Remaining` on every success to know if the next call fits.
- Batch work sequentially with an API key rather than fanning out concurrently on
  the free tier — parallel free-tier calls will mostly 429.
- `X-Sterilizer-Billable: true` means that specific call was charged. Surface
  cost to the user if you are spending their money; `GET /v1/usage` gives the
  running monthly total.

## Errors

| Status | `error` | What to do |
| --- | --- | --- |
| 400 | `bad_request` | Fix the body. Usually missing/invalid base64. |
| 400 | `invalid_ops` | Read `issues[]` — it names the offending field and why. |
| 401 | `invalid_api_key` | Key is unknown or revoked. Fall back to anonymous (1 rpm) or ask the user for a new key. |
| 402 | `quota_exceeded` | Monthly billable cap hit. Stop; tell the user. |
| 422 | `unprocessable_image` | Not a decodable image. Do not retry with the same bytes. |
| 429 | `rate_limit_exceeded` | Sleep `retry_after_seconds`, then retry once. |

Retry only `429` (after waiting) and genuine `5xx` (with exponential backoff,
max ~3 attempts). Never retry `4xx` — the input will not become valid.

## Honest limits to communicate

Do not oversell the result. Specifically:

- **Pixelation is destructive, not reversible** — but it also is not magic. Say
  "pixelated", never "blurred", and never imply the original can be recovered
  from the output (it cannot: only block averages are encoded).
- **Face detection is not exhaustive.** Profile views, distant faces, occlusions,
  masks, and low light are missed. Always report the detected count instead of
  claiming every face was covered.
- **Faces are not the only identifier.** Tattoos, uniforms, name badges, screens,
  documents, licence plates, house numbers and distinctive interiors survive
  untouched unless you pass explicit `regions`. Say what you did *not* cover.
- **Metadata stripping is total** — this one you can state plainly. Output is
  encoded from raw pixels, so no metadata container survives.

## Privacy posture

Images live in memory for the duration of the request. They are never written to
disk, never logged, never retained, and never used for training. Only a numeric
call counter persists, for billing. Every response carries
`X-Sterilizer-Retention: none`.

If a user has said their photo must not leave their device, do not call this API
at all — point them at the browser app, which performs the same operations fully
client-side with no upload.
