# Bot Mugshots Public API (v1)

Machine-readable contract: [`openapi.yaml`](./openapi.yaml) (served at `/api/openapi.yaml`).  
Entry point: `GET /api` (JSON catalog) and `GET /api/health`.

**Privacy:** Names are **not logged or persisted**. Render responses may sit in an **in-memory cache for up to one hour** (also cleared on process restart).

## Versioning

| Item | Value |
|------|--------|
| Generator / API contract | **`v=1` (frozen)** — behavior for v1 will not change; future breaking changes use `v=2`, etc. |
| Response header | `X-Mugshot-Version: 1` on every response |
| Default | If `v` is omitted, the server uses v1 but cache TTL is shorter (5 minutes). **Plugins should always send `v=1`.** |

Unsupported `v` → **400** `{"error":"unsupported_version","supported":[1]}`

## Endpoints

| Method | Path | Response |
|--------|------|----------|
| `GET` / `HEAD` | `/api` | JSON catalog (links to docs); HEAD has no body |
| `GET` / `HEAD` | `/api/health` | JSON liveness |
| `GET` / `HEAD` | `/api/openapi.yaml` | OpenAPI 3.1 YAML |
| `GET` / `HEAD` | `/api/API.md` | This document (text/markdown) |
| `GET` / `HEAD` | `/api/portrait` | `image/svg+xml` — one portrait |
| `GET` / `HEAD` | `/api/lineup` | `application/json` — 100 subjects + rap sheets |
| `GET` / `HEAD` | `/api/lineup.svg` | `image/svg+xml` — contact sheet |

Unknown paths → **404** `{"error":"not_found"}`  
Generator failures → **500** `{"error":"generator_error","message":"..."}`

## Query parameters

All endpoints that generate art accept the roll/render options below unless noted.

| Param | Type | Default | Allowed / notes |
|-------|------|---------|------------------|
| `v` | integer | `1` (implicit) | **`1` only** (frozen) |
| `pack` | string | `bots` | `bots` (default, frozen bot v1) or `animals` |
| `name` | string | `BOTANO` | Roll name; also accepts alias `seed`. Uppercase `A–Z` / `0–9`, max **12** (same as the site) |
| `i` | integer | `0` | Portrait only: subject index **0–99** |
| `size` | integer | `512` | Portrait only: **64–2048** px |
| `ink` | string | `chalk` | `ink`, `chalk`, `blueprint`, `sodium`, `paper` (`paper` → chalk) |
| `fill` | string | `sticker` | `sticker`, `outline` |
| `pal` | string | `candy` | `candy`, `ocean`, `retro`, `pastel` |
| `line` | number | `1.25` (sticker) / `4` (outline) | Stroke weight |
| `bg` | string | `none` | `none` / `transparent` → transparent SVG; `paper`/`chalk`/`light`; or palette keys for solid paper |

### Examples

```http
GET /api/portrait?name=sarah&i=7&size=512&ink=chalk&fill=sticker&pal=candy&line=1.25&bg=none&v=1
GET /api/portrait?pack=animals&name=sarah&i=7&size=512&v=1
GET /api/lineup?name=sarah&v=1
GET /api/lineup?pack=animals&name=sarah&v=1
GET /api/lineup.svg?name=sarah&v=1
```

### Lineup JSON (`/api/lineup`)

Every pack returns an **`items`** array (100 entries: booking, nickname, traits, `rapSheet`, etc.). This is the stable field to use in new clients.

| Pack | `pack` field | Legacy v1 key (same array as `items`) |
|------|----------------|----------------------------------------|
| Bots (default) | `bots` | `bots` |
| Animals | `animals` | `animals` |

### Contact sheet (`/api/lineup.svg`)

The animals pack contact sheet embeds 100 portraits and is **about 1.2 MB** for a typical roll. Prefer **`/api/lineup` JSON plus `/api/portrait`** when you only need a few headshots or want a smaller payload.

## CORS

- `Access-Control-Allow-Origin: *`
- Methods: `GET`, `HEAD`, `OPTIONS`
- Clients may send optional header **`X-Client-Id`** (see rate limits).

## Rate limits

- **60 requests per minute** per **client IP**
- **60 requests per minute** per **client bucket** (so one noisy plugin does not starve others on the same IP)
- Bucket key: sanitized `X-Client-Id` if present (max 64 chars, `[A-Za-z0-9._-]`), otherwise a hash of `User-Agent`

**429** `{"error":"rate_limit","retryAfter":<seconds>}` with `Retry-After` header.

## Caching

| Request | `Cache-Control` | `ETag` |
|---------|-----------------|--------|
| With `v=1` (recommended) | `public, max-age=31536000, immutable` | SHA-256 of body |
| Without `v` | `public, max-age=300` | SHA-256 of body |

Send `If-None-Match: "<etag>"` for **304** responses.

### Optional Caddy edge cache

See [DEPLOY.md](./DEPLOY.md#caddy-edge-cache-optional) for an **optional** edge-cache snippet (requires Caddy’s separate **`cache` module**, which is not installed on the current production host).

## Static SVG policy

Responses are static SVG only: no `<script>`, event handlers, external URLs, or `foreignObject`.  
Headers: `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'`, `X-Content-Type-Options: nosniff`.

## Terms

Usage terms (effective **October 11, 2026**): [TERMS.md](./TERMS.md) · [terms.html](https://mugshots.bot/terms.html) (`/terms.html` on the site).
