Main API
Base URL: http://localhost:3002 (dev) · port 8002 (Docker)
JWT audience: main — authenticated routes require a Bearer token unless noted.
Auth
POST /v1/auth/register
Register a new end user account.
Public
Body
{ "name": "string", "email": "string", "password": "string" }Response 201
{ "token": "string" }POST /v1/auth/login
Log in as an end user.
Public
Body { "email": "string", "password": "string" }
Response 200 { "token": "string" }
Consent
POST /v1/consent/biometric
Give biometric processing consent. Required before a selfie can be uploaded.
Body
{ "consentVersion": "string" }Response 201
GET /v1/consent/biometric
List all consent records for the authenticated user (including revoked).
Response 200 — array of consent rows.
DELETE /v1/consent/biometric
Revoke biometric consent. Triggers a cascade:
- Rekognition
DeleteFacesfor the user's face - Selfie row and R2 object deleted
- All auto-tagged
photoTagssoft-deleted - Consent row marked
revokedAt
All steps are wrapped in a DB transaction; R2 and Rekognition side effects are enqueued after commit.
Response 204
QR Join
POST /v1/qr/join
Join a project using a QR token. Creates a projectMembers row.
Body { "token": "string" }
Response 200
{ "projectId": "uuid", "projectName": "string" }Photos
GET /v1/photos
List all photos the authenticated user is tagged in (reviewStatus = 'confirmed').
Pending tags are never included.
Response 200 — array of photo summaries with presigned thumbnail URLs.
GET /v1/photos/:id/download
Get a presigned GET URL (15-minute expiry) for a watermarked photo.
WARNING
Only available after the user has a paid purchase for the photo's project.
Response 200
{ "downloadUrl": "string", "expiresInSeconds": 900 }Projects (user-facing)
GET /v1/projects/:id/packages
List available pricing packages for a project.
Used on the purchase flow to show the user what they can buy.
Response 200 — array of packages with name, price (cents), and photo count.
Purchases
POST /v1/purchases
Create a Stripe PaymentIntent for a project package.
Body
{ "projectId": "uuid", "packageId": "uuid" }Response 201
{ "clientSecret": "string", "purchaseId": "uuid" }The clientSecret is passed to Stripe.js on the frontend to complete the payment.
Stripe Webhook
POST /v1/stripe-webhook
Receives Stripe webhook events. Requires raw request body (no JSON parsing) for HMAC verification.
Handles:
payment_intent.succeeded→ setspurchase.status = 'paid'payment_intent.payment_failed→ setspurchase.status = 'failed'