# Contract: Image API (Short ID Update)

## ImageRecord Response Schema

All image endpoints return this shape. `short_id` is a new field; all other fields are unchanged.

```json
{
  "id": "7343d164-80bb-473b-b239-717f2842ae4e",
  "short_id": "xK7mN2pQ",
  "hash": "163dec08460650439f1e7439721e8e566aff7d8aaad60cf451e7d3518a334a23",
  "filename": "image.gif",
  "mime_type": "image/gif",
  "size_bytes": 1957149,
  "width": 265,
  "height": 199,
  "storage_key": "xK7mN2pQ",
  "thumbnail_key": "xK7mN2pQ-thumb",
  "file_url": "https://cdn.reactbin.juggalol.com/xK7mN2pQ",
  "thumbnail_url": "https://cdn.reactbin.juggalol.com/xK7mN2pQ-thumb",
  "created_at": "2026-05-09T02:46:29.520296+00:00",
  "tags": ["kfc"]
}
```

**Constraints**:
- `short_id`: exactly 8 alphanumeric characters `[a-zA-Z0-9]{8}`
- `storage_key`: equals `short_id` (post-migration)
- `thumbnail_key`: equals `{short_id}-thumb` or `null` if no thumbnail exists
- `file_url`: `{cdn_base}/{short_id}` when CDN is configured; `/api/v1/images/{short_id}/file` otherwise
- `thumbnail_url`: `{cdn_base}/{short_id}-thumb` or `null`

---

## Route Changes

All routes that previously accepted `{image_id}` as a UUID now accept `{short_id}` as an 8-character alphanumeric string.

### GET /api/v1/images/{short_id}

Fetch a single image by short ID.

- **Path param**: `short_id` — 8-char alphanumeric string
- **Response 200**: ImageRecord
- **Response 404**: `{"detail": "Image not found", "code": "image_not_found"}`
- **Response 422**: `{"detail": "Invalid image ID", "code": "invalid_short_id"}` if param is not 8 alphanumeric chars

### PATCH /api/v1/images/{short_id}/tags

Update tags on an image. Auth required.

- **Path param**: `short_id` — 8-char alphanumeric string
- **Body**: `{"tags": ["tag1", "tag2"]}`
- **Response 200**: ImageRecord (updated)
- **Response 404/422**: same shape as above

### DELETE /api/v1/images/{short_id}

Delete an image and its storage objects. Auth required.

- **Path param**: `short_id` — 8-char alphanumeric string
- **Response 204**: no body
- **Response 404**: error envelope

### GET /api/v1/images/{short_id}/file

Serve the raw image file (proxy mode, when CDN is not configured).

- **Path param**: `short_id`
- **Response 200**: raw image bytes with correct `Content-Type`

### GET /api/v1/images/{short_id}/thumbnail

Serve the thumbnail (proxy mode).

- **Path param**: `short_id`
- **Response 200**: WebP bytes or original image if no thumbnail

### POST /api/v1/images (upload — unchanged route, updated response)

- **Response**: ImageRecord with `short_id` populated

---

## Frontend Route Change

| Old route       | New route    |
|-----------------|--------------|
| `/images/:id`   | `/i/:id`     |

The `:id` segment now contains the `short_id` value (8 alphanumeric chars) rather than a UUID.

---

## ImageRecord TypeScript Interface (updated)

```typescript
export interface ImageRecord {
  id: string;           // UUID — retained, not used for routing
  short_id: string;     // NEW — 8-char base62, used for all routing and API calls
  hash: string;
  filename: string;
  mime_type: string;
  size_bytes: number;
  width: number;
  height: number;
  storage_key: string;
  thumbnail_key: string | null;
  file_url: string;
  thumbnail_url: string | null;
  created_at: string;
  tags: string[];
  duplicate?: boolean;
}
```
