Media & Photos API
Community photo submissions, CDN-served place gallery images, and curator management for official place photos.
Status
Live — POST /api/v1/places/:id/photos, POST /api/v1/admin/places/:placeId/photos, PATCH /api/v1/admin/photos/:photoId, and DELETE /api/v1/admin/photos/:photoId are implemented. Tracked under KUB-146, KUB-149, KUB-150, KUB-151.
Photo Source Types
Each photo has a source_type field indicating its origin:
| Value | Description |
|---|---|
admin_upload | Uploaded by a curator via the admin console (KUB-150) |
scraped | Imported from an upstream source and retained for provenance |
community | Submitted by an authenticated user via the app (KUB-146) |
Community photos start with status: pending and must be approved before appearing in gallery responses. Related: KUB-151.
Submit Community Photo
POST /api/v1/places/:id/photos
Authorization: Bearer <token>
Content-Type: multipart/form-dataAuthenticated users may submit photos of a place. Photos are held for moderation (status: pending) and do not appear publicly until approved.
Request
| Field | Type | Required | Description |
|---|---|---|---|
photo | file | Yes | JPEG, PNG, or WebP, max 10 MB |
attribution | string | No | Optional photographer credit |
Response
HTTP/1.1 201 Created{
"data": {
"id": "42",
"status": "pending",
"moderation_eta": null
}
}Related: KUB-146.
Error Responses
400 invalid_bodywhen the multipart body cannot be parsed.401 unauthorizedwhen the caller is not authenticated.403 forbiddenwhen the caller is a guest session.404 not_foundwhen the place does not exist or is unpublished.413 file_too_largewhen the uploaded image exceeds 10 MB.415 unsupported_media_typewhen the file is not JPEG, PNG, or WebP.422 validation_errorwhen thephotofield is missing or not a file upload.
CDN Media URLs
Gallery responses return url and thumbnail_url fields that are already CDN-backed. Clients should treat both fields as opaque URLs rather than constructing media paths locally. Related: KUB-149.
Curator: Upload Official Photo
POST /api/v1/admin/places/:placeId/photos
Authorization: Bearer <admin-token>
Content-Type: multipart/form-dataAdmin/curator endpoint to attach an official photo to a place. Photos are immediately published (status: published, source_type: admin_upload).
Request
| Field | Type | Required | Description |
|---|---|---|---|
photo | file | Yes | JPEG, PNG, or WebP, max 10 MB |
attribution | string | No | Optional photographer credit |
alt_text | string | No | Optional accessibility text |
Response
HTTP/1.1 201 Created{
"data": {
"id": "99",
"source_type": "admin_upload",
"status": "published"
}
}Related: KUB-150.
Error Responses
400 invalid_bodywhen the multipart body cannot be parsed.401 unauthorizedwhen the caller is not authenticated.403 forbiddenwhen the caller lacks curator/admin access.404 not_foundwhen the place does not exist.413 file_too_largewhen the uploaded image exceeds 10 MB.415 unsupported_media_typewhen the file is not JPEG, PNG, or WebP.422 validation_errorwhen thephotofield is missing or not a file upload.
Curator: Update Photo Metadata
PATCH /api/v1/admin/photos/:photoId
Authorization: Bearer <admin-token>
Content-Type: application/jsonCurators and admins can update attribution, alt text, or the moderation status for an existing place photo.
Request
{
"attribution": "Roseau Photo Collective",
"alt_text": "View toward Morne Bruce",
"status": "approved"
}status accepts only approved or rejected. Invalid field types and unsupported status values return 422 validation_error.
Malformed JSON returns 400 invalid_body before field validation runs.
Response
{
"data": {
"id": "99",
"updated": true
}
}Curator: Delete Photo
DELETE /api/v1/admin/photos/:photoId
Authorization: Bearer <admin-token>Permanently removes a photo. Works for admin_upload, scraped, and community photos.
Response
{
"data": {
"id": "99",
"deleted": true
}
}Related: KUB-150.