# Contract: Image Metadata Response

**Version**: 2.0 (adds `file_url`, `thumbnail_url`)
**Endpoints affected**: `GET /api/v1/images`, `GET /api/v1/images/{id}`, `POST /api/v1/images`, `PATCH /api/v1/images/{id}/tags`

## Response Schema

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "filename": "reaction.gif",
  "mime_type": "image/gif",
  "size_bytes": 204800,
  "width": 480,
  "height": 270,
  "storage_key": "e3b0c44298fc1c149afbf4c8996fb924",
  "thumbnail_key": "e3b0c44298fc1c149afbf4c8996fb924.thumb",
  "file_url": "https://cdn.reactbin.juggalol.com/e3b0c44298fc1c149afbf4c8996fb924",
  "thumbnail_url": "https://cdn.reactbin.juggalol.com/e3b0c44298fc1c149afbf4c8996fb924.thumb",
  "created_at": "2026-05-08T12:00:00.000000",
  "tags": ["funny", "reaction"]
}
```

## Field Descriptions

| Field | Type | Nullable | Notes |
|-------|------|----------|-------|
| `id` | string (UUID) | No | Stable image identifier |
| `hash` | string (hex) | No | SHA-256 of file content; deduplication key |
| `filename` | string | No | Original upload filename |
| `mime_type` | string | No | One of: `image/jpeg`, `image/png`, `image/gif`, `image/webp` |
| `size_bytes` | integer | No | File size in bytes |
| `width` | integer | No | Image width in pixels |
| `height` | integer | No | Image height in pixels |
| `storage_key` | string | No | Object storage key (retained for backward compat) |
| `thumbnail_key` | string | Yes | Thumbnail object storage key; null if generation failed |
| `file_url` | string | No | Full URL to fetch the image file — CDN URL in production, API proxy path in local dev |
| `thumbnail_url` | string | Yes | Full URL to fetch the thumbnail — CDN URL in production, API proxy path in local dev; null if no thumbnail |
| `created_at` | string (ISO 8601) | No | Upload timestamp |
| `tags` | string[] | No | Lowercase normalised tag list |
| `duplicate` | boolean | Yes | Present only on upload responses; true if hash matched an existing image |

## URL Behaviour

| Configuration | `file_url` example | `thumbnail_url` example |
|---------------|--------------------|------------------------|
| `S3_PUBLIC_BASE_URL` set | `https://cdn.reactbin.juggalol.com/{storage_key}` | `https://cdn.reactbin.juggalol.com/{thumbnail_key}` |
| `S3_PUBLIC_BASE_URL` not set | `/api/v1/images/{id}/file` | `/api/v1/images/{id}/thumbnail` |

## UI Contract

The UI MUST use `file_url` and `thumbnail_url` from the response to render images. The UI MUST NOT construct image URLs from `id`, `storage_key`, or `thumbnail_key` directly. The UI MUST treat `thumbnail_url: null` as "no thumbnail available" and fall back to `file_url` for display.
