# 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.