4.9 KiB
API Contract: Reaction Image Board v1
Base URL: /api/v1
Format: JSON (application/json) for all request/response bodies except
file uploads (multipart/form-data)
Auth: None in v1 (NoOpAuthProvider)
Error envelope (all 4xx/5xx responses):
{ "detail": "<human-readable message>", "code": "<machine-readable code>" }
Image Resource
Image object shape
{
"id": "uuid",
"hash": "sha256-hex-string-64-chars",
"filename": "original.jpg",
"mime_type": "image/jpeg",
"size_bytes": 102400,
"width": 800,
"height": 600,
"storage_key": "sha256-hex-string-64-chars",
"created_at": "2026-05-01T12:00:00Z",
"tags": ["cat", "funny"],
"duplicate": false
}
duplicate is only present on POST responses; omit or set to false on GET.
POST /api/v1/images — Upload Image
Request: Content-Type: multipart/form-data
| Field | Required | Type | Notes |
|---|---|---|---|
file |
yes | file | The image binary |
tags |
no | string | Comma-separated, e.g. "cat,funny,reaction" |
Processing:
- Validate MIME type → 422
invalid_mime_typeif not accepted - Validate file size ≤ MAX_UPLOAD_BYTES → 422
file_too_largeif exceeded - Compute SHA-256 hash of raw bytes
- Query DB for existing record with same hash
- If duplicate: return existing record,
duplicate: true, HTTP 200 - If new: write to storage, insert record, upsert tags, HTTP 201
Responses:
| Status | Condition | Body |
|---|---|---|
| 201 | New image stored | Image object with duplicate: false |
| 200 | Duplicate detected | Image object with duplicate: true |
| 422 | Invalid MIME type | {"detail":"...", "code":"invalid_mime_type"} |
| 422 | File too large | {"detail":"...", "code":"file_too_large"} |
| 422 | Invalid tag | {"detail":"...", "code":"invalid_tag"} |
GET /api/v1/images — List / Search Images
Query parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
tags |
string | — | Comma-separated. AND logic — all tags must be present. |
limit |
integer | 50 | Max 100 |
offset |
integer | 0 | Pagination offset |
Response 200:
{
"items": [ { ...image object... } ],
"total": 142,
"limit": 50,
"offset": 0
}
Each item includes the tags array. duplicate field omitted.
Results ordered by created_at descending.
GET /api/v1/images/{id} — Get Single Image
Path: {id} is a UUID.
Response 200: Single image object (tags included, duplicate omitted).
| Status | Code | Condition |
|---|---|---|
| 404 | image_not_found |
No image with that UUID |
GET /api/v1/images/{id}/file — Serve Image File
Path: {id} is a UUID.
Response 302: Redirect to pre-signed S3 URL (1-hour expiry).
Location header contains the URL.
| Status | Code | Condition |
|---|---|---|
| 404 | image_not_found |
No image with that UUID |
PATCH /api/v1/images/{id}/tags — Update Tags
Request: Content-Type: application/json
{ "tags": ["cat", "funny", "new-tag"] }
Replaces the full tag set. Empty array removes all tags from the image. Tag records themselves are never deleted.
Response 200: Full image object with updated tags.
| Status | Code | Condition |
|---|---|---|
| 404 | image_not_found |
No image with that UUID |
| 422 | invalid_tag |
A tag fails validation |
DELETE /api/v1/images/{id} — Delete Image
Path: {id} is a UUID.
Processing:
- Look up image record
- Delete all
image_tagsrows for this image - Delete the image record
- Delete the S3 object at
storage_key
Response 204: No body.
| Status | Code | Condition |
|---|---|---|
| 404 | image_not_found |
No image with that UUID |
Tag Resource
Tag object shape
{
"id": "uuid",
"name": "cat",
"image_count": 42
}
GET /api/v1/tags — List Tags
Query parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
q |
string | — | Prefix filter on tag name |
limit |
integer | 100 | Max 200 |
offset |
integer | 0 | Pagination offset |
Response 200:
{
"items": [ { "id": "uuid", "name": "cat", "image_count": 42 } ],
"total": 7,
"limit": 100,
"offset": 0
}
Results ordered alphabetically by name.
Health
GET /api/v1/health
Response 200:
{ "status": "ok" }
No auth or error cases.
Constraints Summary
| Constraint | Value |
|---|---|
| Accepted MIME types | image/jpeg, image/png, image/gif, image/webp |
| Max upload size | 52 428 800 bytes (50 MiB); configurable via MAX_UPLOAD_BYTES env var |
| Tag name pattern | ^[a-z0-9_-]{1,64}$ (after normalisation) |
| Pre-signed URL expiry | 3 600 seconds (1 hour) |
Max limit for images list |
100 |
Max limit for tags list |
200 |
Default limit for images list |
50 |
Default limit for tags list |
100 |