Skip to content

Media API

Base URL: http://localhost:3003 (dev) · port 8003 (Docker)

Two JWT audiences are accepted depending on the caller:

  • media — end users (selfie upload)
  • backoffice — operators and photographers (profile pictures, photo uploads)

All file uploads use presigned PUT URLs — the server never proxies file bytes. The client receives a URL and PUTs the file directly to Cloudflare R2.


Selfie Upload

POST /v1/selfie/upload-url

Request a presigned PUT URL to upload a selfie.

Audience media · Role user

Requires consent

This endpoint is guarded by requireConsent('biometric_processing'). The request is rejected with 403 if the user has no active consent row.

Response 201

json
{
  "selfieId": "uuid",
  "uploadUrl": "string",
  "key": "string",
  "expiresInSeconds": 600
}

After uploading to uploadUrl, the process_selfie job is enqueued automatically. It calls the Retina clustering service to generate a 512-dim face embedding and, if approved, enqueues cluster_faces for each project the user has joined.


Photo Upload

POST /v1/photos/upload-url

Request a presigned PUT URL to upload a project photo.

Audience media · Role photographer

Body { "projectId": "uuid" }

Response 201

json
{
  "photoId": "uuid",
  "uploadUrl": "string",
  "key": "string",
  "expiresInSeconds": 600
}

On upload the following jobs are enqueued:

  • generate_thumbnail — Sharp resize to 400px and 800px; after completion enqueues cluster_faces for each project member with an approved selfie
  • generate_watermark — Sharp composite with text overlay, EXIF stripped

POST /v1/photographers/me/photos/upload-url

Same as above but accepts a backoffice-audience token. Used by the backoffice-web photographer dashboard.

Audience backoffice · Role photographer


Photographer Photos

GET /v1/photographers/me/projects/:projectId/photos

List photos for a project the authenticated photographer is assigned to. Photos are ordered by takenAt descending (undated last).

Audience backoffice · Role photographer

Response 200 — array of photo objects including takenAt (nullable ISO 8601), status, thumbnailUrl, fullUrl.


DELETE /v1/photographers/me/projects/:projectId/photos/:photoId

Delete a photo the authenticated photographer uploaded. Removes the DB row and all R2 objects (original, thumbnails, watermarked).

Audience backoffice · Role photographer · Response 204


Operator Media

GET /v1/operators/me/photos

List all photos across the operator's projects, with optional multi-select filtering.

Audience backoffice · Role operator

Query params

  • projectIds — comma-separated list of project UUIDs (optional, defaults to all)
  • photographerIds — comma-separated list of photographer UUIDs (optional, defaults to all)

Response 200 — array of photo objects including projectId, projectName, photographerName, takenAt (nullable ISO 8601).


GET /v1/operators/me/projects/:projectId/photos

List photos for a single project belonging to the authenticated operator.

Audience backoffice · Role operator

Response 200 — array of photo objects including photographerName and takenAt.


DELETE /v1/operators/me/projects/:projectId/photos/:photoId

Delete any photo within the operator's project. Removes the DB row and all R2 objects.

Audience backoffice · Role operator · Response 204


POST /v1/operators/me/profile-picture/upload-url

Request a presigned PUT URL for an operator profile picture.

Audience backoffice · Role operator

Response 200

json
{ "uploadUrl": "string", "fileUrl": "string" }

fileUrl is the permanent R2 path to store on the operator profile after upload.


Storage Key Conventions

selfies/{userId}/{timestamp}.jpg
photos/{projectId}/{photoId}/original.jpg
photos/{projectId}/{photoId}/thumb-400.jpg
photos/{projectId}/{photoId}/thumb-800.jpg
photos/{projectId}/{photoId}/watermarked.jpg
operators/{operatorId}/profile.jpg

Never return public R2 URLs

All download URLs served to clients must be presigned GET URLs (15-minute expiry). The R2_PUBLIC_URL is used for internal key construction only.

Wairo — Internal Documentation