# API Contract: Upload Thumbnails

**Branch**: `003-upload-thumbnails` | **Date**: 2026-05-03

---

## New endpoint

### `GET /api/v1/images/{image_id}/thumbnail`

Returns the thumbnail content for the given image. If no thumbnail was generated
(image pre-dates the feature or generation failed), falls back to the full-size
original.

**Path parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `image_id` | UUID | Unique identifier of the image |

**Responses**

#### `200 OK` — Thumbnail (or original fallback) content

| Header | Value | Notes |
|--------|-------|-------|
| `Content-Type` | `image/webp` | Always WebP when a thumbnail exists; original `mime_type` when falling back to the original |
| `ETag` | `"{sha256-hex}"` | Same hash as the original image — content is immutable |
| `Cache-Control` | `public, max-age=31536000, immutable` | Safe: thumbnail content never changes |

Body: raw image bytes (WebP thumbnail, or original bytes as fallback).

#### `404 Not Found` — Image not found

```json
{ "detail": "Image not found", "code": "image_not_found" }
```

#### `500 Internal Server Error` — Storage retrieval failure

```json
{ "detail": "Failed to retrieve image content", "code": "storage_error" }
```

---

## Changed endpoint: `POST /api/v1/images`

The upload response body gains one new field:

| Field | Type | Notes |
|-------|------|-------|
| `thumbnail_key` | `string \| null` | S3 key of the generated thumbnail. `null` if generation failed. |

All existing fields are unchanged.

**Example response** (new field only shown):
```json
{
  "id": "...",
  "thumbnail_key": "abc123…-thumb",
  ...
}
```

---

## Changed endpoint: `GET /api/v1/images` and `GET /api/v1/images/{id}`

Both metadata responses gain the same `thumbnail_key` field (`string | null`).

---

## UI contract

The Angular `ImageService` gains one new method:

```
getThumbnailUrl(id: string): string
  → '/api/v1/images/{id}/thumbnail'
```

The `ImageRecord` interface gains:

```
thumbnail_key: string | null;
```

The library grid component uses `getThumbnailUrl(image.id)` as the `src` for
every grid cell. The detail component continues using `getFileUrl(image.id)`.
