Backoffice API
Base URL: http://localhost:3001 (dev) · port 8001 (Docker)
JWT audience: backoffice — all routes require a valid Bearer token unless noted.
Roles: operator, photographer
Auth
POST /v1/auth/register
Register a new operator account.
Public — no token required.
Body
{ "name": "string", "email": "string", "password": "string", "timezone": "string?" }Response 201
{ "token": "string" }POST /v1/auth/login
Log in as an operator or photographer.
Public — no token required.
Body
{ "email": "string", "password": "string" }Response 200
{ "token": "string" }POST /v1/auth/register-photographer
Register a new photographer under the authenticated operator.
Role operator
Body
{ "name": "string", "email": "string", "password": "string" }Response 201
{ "id": "uuid", "name": "string", "email": "string" }Projects
GET /v1/projects
List all projects belonging to the authenticated operator.
Role operator
Response 200 — array of project summaries.
POST /v1/projects
Create a new project.
Role operator
Body
{
"name": "string",
"type": "private | public",
"status": "draft | live",
"locationName": "string?",
"startAt": "ISO 8601?",
"endAt": "ISO 8601?",
"projectLink": "url?",
"termsAndConditions": "string?",
"timezone": "string?"
}type— defaults to"private". Private projects require email-bound invite links; public projects are open to anyone with the link or QR code.status— defaults to"draft". Use"live"to publish immediately.
Response 201 — project detail with empty photographers array.
GET /v1/projects/:id
Get project detail including assigned photographers.
Role operator, photographer
PATCH /v1/projects/:id
Update project fields (name, type, location, dates, timezone).
Role operator
PATCH /v1/projects/:id/status
Transition a project to a new status.
Role operator
Body
{ "status": "draft | live | closed | archived" }Status transitions:
draft→live— publishes the project; participants can join and upload selfies.live→draft— hides the project from participants; existing joins are preserved.live→closed— prevents new joins; existing members retain access.closed→archived— hides the project from all views.
Response 200 — updated project detail.
POST /v1/projects/:id/photographers
Assign a photographer to a project.
Role operator
Body { "photographerId": "uuid" }
Response 204
DELETE /v1/projects/:id/photographers/:photographerId
Remove a photographer from a project.
Role operator · Response 204
GET /v1/projects/:id/discounts
List discounts linked to a project.
Role operator
Response 200 — array of discount objects.
POST /v1/projects/:id/discounts
Link a discount to a project.
Role operator
Body { "discountId": "uuid" }
Response 204
DELETE /v1/projects/:id/discounts/:discountId
Unlink a discount from a project.
Role operator · Response 204
GET /v1/projects/:id/invites
List all invites for a project, with each invitee's registration status.
Role operator
Response 200 — array of invite items, newest first.
[
{
"id": "uuid",
"email": "alice@example.com",
"registrationStatus": "pending",
"createdAt": "ISO 8601",
"expiresAt": "ISO 8601"
}
]registrationStatus values:
pending— invite sent, user has not registered yetregistered— user has created an account but not joined the projectjoined— user has joined the projectexpired— invite expired without the user registering
POST /v1/projects/:id/invites/bulk
Send email invitations to a list of users who have opted in to receive photos from this project.
Role operator
Body
{ "emails": ["alice@example.com", "bob@example.com"] }emails— 1–500 valid email addresses.
Response 202
{ "queued": 2, "skipped": 0 }queued— number of invite emails enqueued.skipped— emails that were deduplicated: already project members or already have a live pending invite.
Skipping is silent (not an error). Re-invite after expiry is supported — a new invite is created once the previous one expires.
DELETE /v1/projects/:id
Permanently delete a project and all its photos (R2 objects and DB rows). This action cannot be undone.
Role operator · Response 204
Photographers
GET /v1/photographers/me/projects
List projects the authenticated photographer is assigned to.
Role photographer
GET /v1/photographers/me/projects/:id
Get a single project the photographer is assigned to.
Role photographer
GET /v1/photographers
List all photographers belonging to the authenticated operator, including the projects each photographer is assigned to.
Role operator
Response 200 — array of photographers, each with a projects: [{ id, name }] array.
PATCH /v1/photographers/:id
Update a photographer's name or email.
Role operator
DELETE /v1/photographers/:id
Delete a photographer. Fails with 409 if the photographer is assigned to any project.
Role operator
Pricing
GET /v1/pricing/rules
List all pricing rules for the authenticated operator (operator default + all project overrides).
Role operator
Response 200 — array of pricing rule objects.
POST /v1/pricing/rules
Create a pricing rule. If projectId is omitted, the rule becomes the operator default.
Role operator
Body
{
"projectId": "uuid?",
"pricePerPhotoCents": 499,
"compareAtPriceCents": 799,
"validFrom": "ISO 8601?",
"validUntil": "ISO 8601?"
}Response 201 — pricing rule object.
PATCH /v1/pricing/rules/:id
Update a pricing rule (price, compare-at price, dates, isActive).
Role operator · Response 200
DELETE /v1/pricing/rules/:id
Delete a pricing rule.
Role operator · Response 204
Discounts
GET /v1/discounts
List all discounts for the authenticated operator.
Role operator
Response 200 — array of discount objects.
POST /v1/discounts
Create a discount.
Role operator
Body
{
"name": "string",
"type": "percentage | fixed | bundle",
"value": 20,
"bundleMinPhotos": 5,
"validFrom": "ISO 8601?",
"validUntil": "ISO 8601?"
}type=percentage—valueis percentage points (1–100).bundleMinPhotosignored.type=fixed—valueis cents.bundleMinPhotosignored.type=bundle—valueis percentage points;bundleMinPhotosis required (≥2).
Response 201 — discount object.
PATCH /v1/discounts/:id
Update a discount (name, dates, isActive).
Role operator · Response 200
DELETE /v1/discounts/:id
Delete a discount. Returns 409 if the discount is still linked to any project.
Role operator · Response 204
Promo Codes
GET /v1/promo-codes
List all promo codes for the authenticated operator.
Role operator
Response 200 — array of promo code objects.
POST /v1/promo-codes
Create a promo code linked to a discount (the operator must own the discount).
Role operator
Body
{
"code": "SUMMER20",
"discountId": "uuid",
"maxUses": 100,
"maxUsesPerUser": 1,
"validFrom": "ISO 8601?",
"validUntil": "ISO 8601?"
}Response 201 — promo code object.
PATCH /v1/promo-codes/:id
Update a promo code (isActive, limits, dates).
Role operator · Response 200
DELETE /v1/promo-codes/:id
Delete a promo code.
Role operator · Response 204
Operators
GET /v1/operators/me
Get the authenticated operator's profile.
Role operator
PATCH /v1/operators/me
Update profile fields (name, address, billing address, currency, locale, timezone, profile picture URL).
Role operator
Analytics
GET /v1/analytics
Returns KPIs, photos by photographer, revenue by project, and conversion funnel for the operator's account.
Role operator
Query params (all optional, comma-separated UUIDs):
projectIds— scope to specific projectsphotographerIds— scope to specific photographers
Response
{
"kpis": {
"membersEnrolled": 0,
"revenueInCents": 0,
"photosUploaded": 0,
"usersTagged": 0,
"purchasesMade": 0
},
"photosByPhotographer": [{ "name": "string", "photoCount": 0 }],
"revenueByProject": [{ "projectName": "string", "purchasesMade": 0, "revenueInCents": 0 }],
"conversionFunnel": { "invited": 0, "enrolled": 0, "tagged": 0, "purchased": 0 }
}Orders
GET /v1/purchases
List all purchases across the operator's projects, newest first.
Role operator
Query
projectId— filter to a single project (UUID)status— filter by status:pending|paid|failed|refundedpage— page number, 1-indexed (default1)limit— items per page, max 100 (default50)
Response 200
{
"items": [
{
"id": "uuid",
"status": "paid",
"photoCount": 5,
"subtotalCents": 2500,
"discountAppliedCents": 500,
"totalCents": 2000,
"appliedPromoCode": "SUMMER10",
"stripePaymentIntentId": "pi_xxx",
"createdAt": "2026-04-08T12:00:00.000Z",
"project": { "id": "uuid", "name": "string" },
"user": { "id": "uuid", "email": "string", "name": "string" }
}
],
"total": 42,
"page": 1,
"limit": 50
}GET /v1/purchases/:id
Get full detail for a single purchase, including the list of purchased photo IDs.
Role operator
Returns 403 if the purchase belongs to a project not owned by the authenticated operator.
Response 200
{
"id": "uuid",
"status": "paid",
"photoCount": 5,
"subtotalCents": 2500,
"discountAppliedCents": 500,
"totalCents": 2000,
"appliedPromoCode": "SUMMER10",
"stripePaymentIntentId": "pi_xxx",
"createdAt": "2026-04-08T12:00:00.000Z",
"project": { "id": "uuid", "name": "string" },
"user": { "id": "uuid", "email": "string", "name": "string" },
"downloads": [{ "photoId": "uuid" }]
}