Skip to content

# API Reference

Changemaker Lite exposes two REST APIs sharing a single PostgreSQL database.

Server | Framework | Port | Purpose
--- | --- | --- | ---
Main API | Express.js | `4000` | Auth, campaigns, map, shifts, canvassing, pages, email, settings
Media API | Fastify | `4100` | Video library, analytics, playlists, reactions, comments

Both APIs use JWT Bearer authentication and return JSON. All request/response bodies are `application/json` unless noted otherwise.

## Authentication

### Token Flow

    sequenceDiagram
        participant Client
        participant API
        participant DB

        Client->>API: POST /api/auth/login {email, password}
        API->>DB: Verify credentials
        DB-->>API: User record
        API-->>Client: {accessToken, refreshToken}
        Note over Client: Store tokens

        Client->>API: GET /api/campaigns (Authorization: Bearer <accessToken>)
        API-->>Client: 200 OK

        Note over Client: Access token expires (15 min)

        Client->>API: POST /api/auth/refresh {refreshToken}
        API->>DB: Rotate token (atomic transaction)
        DB-->>API: New token pair
        API-->>Client: {accessToken, refreshToken}

### Headers

All authenticated requests require:

    Authorization: Bearer <accessToken>

The Media API also accepts tokens via query parameter for SSE streams:

    GET /api/public/:id/chat-stream?token=<accessToken>

### Roles

Role | Access Level
--- | ---
`SUPER_ADMIN` | Full platform access
`INFLUENCE_ADMIN` | Campaign and advocacy management
`MAP_ADMIN` | Map, locations, shifts, canvassing
`USER` | Volunteer portal, public features
`TEMP` | Limited access (auto-created on public shift signup)

### Middleware Reference

Middleware | Effect
--- | ---
`authenticate` | Requires valid JWT. Sets `req.user` with `id`, `email`, `role`. Returns `401` if missing or invalid.
`optionalAuth` | Same as `authenticate` but continues without user if token is absent.
`requireRole(...roles)` | Checks user role against allowed list. Returns `403` if not authorized.
`requireNonTemp` | Blocks `TEMP` users. Returns `403`.
`validate(schema, source)` | Validates request body/query/params against a Zod schema. Returns `400` on failure.

## Error Responses

All errors follow a consistent format:

    {
      "error": {
        "message": "Human-readable error description",
        "code": "ERROR_CODE",
        "statusCode": 400
      }
    }

Status | Code | Meaning
--- | --- | ---
`400` | `VALIDATION_ERROR` | Request body/query failed schema validation
`401` | `UNAUTHORIZED` | Missing or invalid access token
`403` | `FORBIDDEN` | Valid token but insufficient role
`404` | `NOT_FOUND` | Resource does not exist
`429` | `RATE_LIMITED` | Too many requests (see Rate Limits)
`500` | `INTERNAL_ERROR` | Unexpected server error

Enumeration Prevention

Auth endpoints (`/login`, `/register`, `/forgot-password`) return generic success messages to prevent user enumeration. A `401` from `/api/auth/me` does not reveal whether the user exists.

## Rate Limits

Rate limits are Redis-backed and keyed by IP address.

Endpoint Group | Window | Max Requests | Redis Prefix
--- | --- | --- | ---
Auth (login, register, refresh) | 15 min | 10 | `rl:auth:`
Email sending | 1 hour | 30 | `rl:email:`
Response submission | 1 hour | 10 | `rl:response:`
Shift signup | 1 hour | 10 | `rl:shift-signup:`
Canvass visits | 1 min | 30 | `rl:canvass-visit:`
Canvass bulk visits | 1 min | 5 | `rl:canvass-visit-bulk:`
GPS tracking | 1 min | 6 | `rl:gps-tracking:`
Canvass geocode | 1 min | 10 | `rl:canvass-geocode:`
Observability | 1 min | 20 | `rl:observability:`
Health/metrics | 1 min | 30 | `rl:health-metrics:`
Global (all other) | Configurable | Configurable | `rl:global:`

When rate-limited, the API returns:

    {
      "error": {
        "message": "Too many requests, please try again later",
        "code": "RATE_LIMITED",
        "statusCode": 429
      }
    }

## Main API (Express — Port 4000)

### Health & Metrics

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/health` |  | Health check — PostgreSQL + Redis ping
GET | `/api/metrics` |  | Prometheus metrics (text/plain)

Health response

    {
      "status": "healthy",
      "checks": {
        "database": "ok",
        "redis": "ok"
      }
    }

### Auth

Prefix: `/api/auth`

Method | Path | Auth | Rate Limited | Description
--- | --- | --- | --- | ---
POST | `/api/auth/login` |  |  | Email + password login
POST | `/api/auth/register` |  |  | Create account (always `USER` role)
POST | `/api/auth/verify-email` |  |  | Verify email with token
POST | `/api/auth/resend-verification` |  |  | Resend verification email
POST | `/api/auth/forgot-password` |  |  | Send password reset email
POST | `/api/auth/reset-password` |  |  | Set new password with reset token
POST | `/api/auth/refresh` |  |  | Rotate refresh token → new token pair
POST | `/api/auth/logout` |  |  | Invalidate refresh token
GET | `/api/auth/me` |  |  | Current user profile

Login request & response

Request:

    {
      "email": "admin@example.com",
      "password": "SecurePass123!"
    }

Response:

    {
      "accessToken": "eyJhbG...",
      "refreshToken": "eyJhbG...",
      "user": {
        "id": "uuid",
        "email": "admin@example.com",
        "name": "Admin",
        "role": "SUPER_ADMIN"
      }
    }

Password Policy

Passwords must be at least 12 characters with at least one uppercase letter, one lowercase letter, and one digit.

### Users

Prefix: `/api/users` · Auth: All routes require authentication

Method | Path | Role | Description
--- | --- | --- | ---
GET | `/api/users` | Admin | Paginated user list with search, role, and status filters
GET | `/api/users/:id` | Admin or self | Single user profile
POST | `/api/users` | Admin | Create user
PUT | `/api/users/:id` | Admin or self | Update user (non-admins cannot change role/status)
POST | `/api/users/:id/approve` | Admin | Approve pending user; sends approval email
POST | `/api/users/:id/reject` | Admin | Reject pending user
DELETE | `/api/users/:id` | Admin | Delete user

Query parameters for `GET /api/users`:

Param | Type | Description
--- | --- | ---
`page` | number | Page number (default 1)
`limit` | number | Items per page (default 20)
`search` | string | Search by name or email
`role` | string | Filter by role
`status` | string | Filter by status

### Dashboard

Prefix: `/api/dashboard` · Auth: Admin roles required

Method | Path | Role | Description
--- | --- | --- | ---
GET | `/api/dashboard/summary` | Any admin | Platform-wide counts (users, campaigns, locations, shifts)
GET | `/api/dashboard/system` | `SUPER_ADMIN` | Hardware + OS info (CPU, memory, disk)
GET | `/api/dashboard/containers` | `SUPER_ADMIN` | Docker container statuses
GET | `/api/dashboard/weather` | Any admin | Current weather at map center coordinates
GET | `/api/dashboard/api-metrics` | `SUPER_ADMIN` | Prometheus API performance metrics
GET | `/api/dashboard/time-series` | `SUPER_ADMIN` | Prometheus time-series data
GET | `/api/dashboard/container-resources` | `SUPER_ADMIN` | cAdvisor CPU/memory/network per container

Query parameters for `GET /api/dashboard/time-series`:

Param | Type | Description
--- | --- | ---
`metrics` | string | Comma-separated metric keys (whitelist-validated)
`range` | string | Time range (e.g., `1h`, `24h`, `7d`)
`step` | string | Sample interval (e.g., `5m`, `1h`)

### Campaigns

#### Admin CRUD

Prefix: `/api/campaigns` · Auth: Admin roles

Method | Path | Description
--- | --- | ---
GET | `/api/campaigns` | Paginated campaign list
GET | `/api/campaigns/:id` | Single campaign detail
POST | `/api/campaigns` | Create campaign
PUT | `/api/campaigns/:id` | Update campaign
DELETE | `/api/campaigns/:id` | Delete campaign

#### Public

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/campaigns/public` |  | List all active campaigns
GET | `/api/campaigns/:slug/details` |  | Campaign detail by slug (ACTIVE only)

#### User Submissions

Auth: Authenticated, non-TEMP users

Method | Path | Description
--- | --- | ---
POST | `/api/campaigns/user/submit` | Submit campaign for moderation (5/hour limit)
GET | `/api/campaigns/user/my-campaigns` | List own submitted campaigns
PUT | `/api/campaigns/user/:id` | Edit own pending campaign

#### Moderation

Auth: Admin roles

Method | Path | Description
--- | --- | ---
GET | `/api/campaigns/moderation/queue` | Campaigns pending moderation
GET | `/api/campaigns/moderation/stats` | Moderation queue statistics
PATCH | `/api/campaigns/moderation/:id` | Approve or reject campaign

#### Campaign Emails

Method | Path | Auth | Description
--- | --- | --- | ---
POST | `/api/campaigns/:slug/send-email` |  | Send advocacy email to representatives (rate limited: 30/hour)
POST | `/api/campaigns/:slug/track-mailto` |  | Track mailto link click
GET | `/api/campaigns/:id/emails` | Admin | Paginated emails for campaign
GET | `/api/campaigns/:id/email-stats` | Admin | Email statistics

### Responses

Prefix: `/api/campaigns` (public) and `/api/responses` (admin + actions)

#### Public

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/campaigns/:slug/responses` |  | List approved public responses
GET | `/api/campaigns/:slug/response-stats` |  | Response statistics
POST | `/api/campaigns/:slug/responses` |  | Submit response (rate limited: 10/hour)
POST | `/api/responses/:id/upvote` | Optional | Upvote a response
DELETE | `/api/responses/:id/upvote` | Optional | Remove upvote
GET | `/api/responses/:id/verify/:token` |  | Verify response via email link

#### Admin

Auth: Admin roles

Method | Path | Description
--- | --- | ---
GET | `/api/responses` | All responses with filters
PATCH | `/api/responses/:id/status` | Approve or reject response
POST | `/api/responses/:id/resend-verification` | Resend verification email
DELETE | `/api/responses/:id` | Delete response

### Representatives

Prefix: `/api/representatives`

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/representatives/by-postal/:postalCode` |  | Lookup representatives by postal code (cache-first)
GET | `/api/representatives/test-connection` |  | Represent API health check
GET | `/api/representatives/cache-stats` | Admin | Cache statistics
GET | `/api/representatives` | Admin | Paginated cached representatives
GET | `/api/representatives/:id` | Admin | Single cached representative
DELETE | `/api/representatives/by-postal/:postalCode` | Admin | Clear cache for postal code
DELETE | `/api/representatives/:id` | Admin | Delete cached representative

Query parameters for postal code lookup:

Param | Type | Description
--- | --- | ---
`refresh` | boolean | Force API call, bypass cache

### Email Queue

Prefix: `/api/email-queue` · Auth: Admin roles

Method | Path | Description
--- | --- | ---
GET | `/api/email-queue/stats` | BullMQ queue statistics (waiting, active, completed, failed)
POST | `/api/email-queue/pause` | Pause email processing
POST | `/api/email-queue/resume` | Resume email processing
POST | `/api/email-queue/clean` | Clean completed jobs

### Locations

Prefix: `/api/map/locations`

#### Public

Method | Path | Description
--- | --- | ---
GET | `/api/map/locations/public` | All geocoded locations for map (no PII); optional `?bounds=`

#### Admin

Auth: `SUPER_ADMIN` or `MAP_ADMIN`

Method | Path | Description
--- | --- | ---
GET | `/api/map/locations` | Paginated locations with filters
GET | `/api/map/locations/stats` | Location statistics
GET | `/api/map/locations/all` | All geocoded locations for admin map
GET | `/api/map/locations/export-csv` | CSV export
GET | `/api/map/locations/:id` | Single location
GET | `/api/map/locations/:id/history` | Edit history
POST | `/api/map/locations` | Create location
PUT | `/api/map/locations/:id` | Update location
DELETE | `/api/map/locations/:id` | Delete location
POST | `/api/map/locations/bulk-delete` | Bulk delete
POST | `/api/map/locations/geocode` | Geocode single address
POST | `/api/map/locations/geocode-missing` | Batch geocode all ungeocoded
POST | `/api/map/locations/reverse-geocode` | Reverse geocode lat/lng to address
POST | `/api/map/locations/import-csv` | Import from CSV (10 MB limit)
POST | `/api/map/locations/import-bulk` | Bulk NAR or standard CSV import (100 MB limit)

#### Bulk Geocode

Prefix: `/api/map/locations/bulk-geocode` · Auth: Map admins

Method | Path | Description
--- | --- | ---
POST | `/api/map/locations/bulk-geocode` | Start BullMQ bulk geocoding job
GET | `/api/map/locations/bulk-geocode/:jobId` | Poll job status
GET | `/api/map/locations/bulk-geocode/stats` | Queue statistics

#### NAR Import

Prefix: `/api/map/nar-import` · Auth: Map admins

Method | Path | Description
--- | --- | ---
GET | `/api/map/nar-import/datasets` | Available NAR datasets by province
POST | `/api/map/nar-import` | Start province import (fire-and-forget)
GET | `/api/map/nar-import/status/:importId` | Poll import progress

NAR Import body

    {
      "provinceCode": "24",
      "filterType": "city",
      "filterCity": "Edmonton",
      "residentialOnly": true,
      "deduplicateRadius": 10,
      "batchSize": 500
    }

#### Area Import

Prefix: `/api/map/area-import` · Auth: Map admins

Method | Path | Description
--- | --- | ---
POST | `/api/map/area-import/preview` | Preview bounds + estimated record counts
POST | `/api/map/area-import` | Start area import (fire-and-forget)
GET | `/api/map/area-import/status/:importId` | Poll import progress

### Cuts (Polygons)

Prefix: `/api/map/cuts`

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/map/cuts/public` |  | All public cuts as GeoJSON
GET | `/api/map/cuts` | Map admin | Paginated cuts list
GET | `/api/map/cuts/:id` | Map admin | Single cut
POST | `/api/map/cuts` | Map admin | Create cut (polygon GeoJSON)
PUT | `/api/map/cuts/:id` | Map admin | Update cut
DELETE | `/api/map/cuts/:id` | Map admin | Delete cut
GET | `/api/map/cuts/:id/locations` | Map admin | All locations within cut polygon
GET | `/api/map/cuts/:id/statistics` | Map admin | Support level breakdown
GET | `/api/map/cuts/export-geojson` | Map admin | All cuts as GeoJSON FeatureCollection
GET | `/api/map/cuts/:id/export-geojson` | Map admin | Single cut as GeoJSON Feature
POST | `/api/map/cuts/import-geojson` | Map admin | Import cuts from GeoJSON file

### Shifts

Prefix: `/api/map/shifts`

#### Public

Method | Path | Description
--- | --- | ---
GET | `/api/map/shifts/public` | List upcoming public shifts
POST | `/api/map/shifts/public/:id/signup` | Public signup (creates TEMP user if needed; rate limited: 10/hour)

#### Volunteer

Auth: Any authenticated user

Method | Path | Description
--- | --- | ---
GET | `/api/map/shifts/volunteer/upcoming` | Upcoming shifts with signup status
GET | `/api/map/shifts/volunteer/my-signups` | Own confirmed signups
POST | `/api/map/shifts/volunteer/:id/signup` | Sign up for shift
DELETE | `/api/map/shifts/volunteer/:id/signup` | Cancel signup

#### Admin

Auth: Map admins

Method | Path | Description
--- | --- | ---
GET | `/api/map/shifts` | Paginated shifts with filters
GET | `/api/map/shifts/stats` | Statistics
GET | `/api/map/shifts/calendar` | Calendar data (`?startDate=&endDate=`)
GET | `/api/map/shifts/:id` | Single shift with signups
POST | `/api/map/shifts` | Create shift
PUT | `/api/map/shifts/:id` | Update shift
DELETE | `/api/map/shifts/:id` | Delete shift
POST | `/api/map/shifts/:id/signups` | Admin-add volunteer
DELETE | `/api/map/shifts/:id/signups/:signupId` | Remove volunteer
POST | `/api/map/shifts/:id/email-details` | Email details to all volunteers

#### Shift Series

Auth: Map admins

Method | Path | Description
--- | --- | ---
POST | `/api/map/shifts/series` | Create recurring shift series
GET | `/api/map/shifts/series/:id` | Get series
PUT | `/api/map/shifts/series/:id` | Update series
DELETE | `/api/map/shifts/series/:id` | Delete series

### Canvassing

Prefix: `/api/map/canvass`

#### Volunteer

Auth: Any authenticated user

Method | Path | Description
--- | --- | ---
GET | `/api/map/canvass/my/assignments` | Shift assignments
GET | `/api/map/canvass/my/stats` | Personal canvass statistics
GET | `/api/map/canvass/my/visits` | Visit history
GET | `/api/map/canvass/my/session` | Active canvass session
POST | `/api/map/canvass/sessions` | Start canvass session
POST | `/api/map/canvass/sessions/:id/end` | End session
GET | `/api/map/canvass/cuts/:cutId/locations` | Locations in cut with visit annotations
GET | `/api/map/canvass/cuts/:cutId/route` | Walking route algorithm for cut
GET | `/api/map/canvass/locations` | All locations with visit annotations
PUT | `/api/map/canvass/locations/:id` | Edit address (role-gated fields)
POST | `/api/map/canvass/locations` | Create location
POST | `/api/map/canvass/reverse-geocode` | Reverse geocode lat/lng
POST | `/api/map/canvass/geocode-search` | Geocode address for map (rate limited: 10/min)
POST | `/api/map/canvass/visits` | Record door knock (rate limited: 30/min)
POST | `/api/map/canvass/visits/bulk` | Record visit for all unvisited units (rate limited: 5/min)

#### Admin

Auth: `SUPER_ADMIN` or `MAP_ADMIN`

Method | Path | Description
--- | --- | ---
GET | `/api/map/canvass/stats` | Platform-wide canvass statistics
GET | `/api/map/canvass/stats/cuts/:cutId` | Statistics for specific cut
GET | `/api/map/canvass/activity` | Recent activity feed
GET | `/api/map/canvass/volunteers` | All volunteers with canvass activity
GET | `/api/map/canvass/volunteers/:userId` | Individual volunteer statistics
GET | `/api/map/canvass/visits` | All visits with filters

### GPS Tracking

Prefix: `/api/map/tracking`

#### Volunteer

Auth: Any authenticated user

Method | Path | Description
--- | --- | ---
POST | `/api/map/tracking/sessions` | Start GPS tracking session
POST | `/api/map/tracking/sessions/:id/end` | End tracking session
POST | `/api/map/tracking/sessions/:id/points` | Submit GPS point batch (rate limited: 6/min)
POST | `/api/map/tracking/sessions/:id/link-canvass` | Link to canvass session
GET | `/api/map/tracking/my/session` | Active tracking session
GET | `/api/map/tracking/my/sessions` | Own historical sessions
GET | `/api/map/tracking/my/sessions/:id/route` | Full route for own session

#### Admin

Auth: Map admins

Method | Path | Description
--- | --- | ---
GET | `/api/map/tracking/live` | Live volunteer positions + trails
GET | `/api/map/tracking/sessions` | All historical tracking sessions
GET | `/api/map/tracking/sessions/:id/route` | Full route for any session

### Map Settings

Prefix: `/api/map/settings`

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/map/settings` |  | Public map settings (center, zoom, walk sheet config)
PUT | `/api/map/settings` | Map admin | Update map settings

### Geocoding

Prefix: `/api/map/geocoding` · Auth: Map admins

Method | Path | Description
--- | --- | ---
GET | `/api/map/geocoding/search` | Geocode address search (`?q=&limit=1-10`)

### Landing Pages

Prefix: `/api/pages` and `/api/page-blocks`

#### Public

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/pages/:slug/view` |  | Get published page by slug

#### Admin

Auth: Admin roles

Method | Path | Description
--- | --- | ---
GET | `/api/pages` | Paginated landing pages
GET | `/api/pages/:id` | Single page
POST | `/api/pages` | Create page
PUT | `/api/pages/:id` | Update page
DELETE | `/api/pages/:id` | Delete page
POST | `/api/pages/sync` | Sync MkDocs overrides from filesystem
POST | `/api/pages/validate` | Validate and repair MkDocs exports

#### Block Library

Auth: Admin roles

Method | Path | Description
--- | --- | ---
GET | `/api/page-blocks` | List blocks
GET | `/api/page-blocks/:id` | Single block
POST | `/api/page-blocks` | Create block
PUT | `/api/page-blocks/:id` | Update block
DELETE | `/api/page-blocks/:id` | Delete block

### Email Templates

Prefix: `/api/email-templates` · Auth: Admin roles (seed/cache require `SUPER_ADMIN`)

Method | Path | Description
--- | --- | ---
GET | `/api/email-templates` | List templates
GET | `/api/email-templates/:id` | Single template
POST | `/api/email-templates` | Create template
PUT | `/api/email-templates/:id` | Update template
DELETE | `/api/email-templates/:id` | Delete template
GET | `/api/email-templates/:id/versions` | Version history
GET | `/api/email-templates/:id/versions/:versionNumber` | Specific version
POST | `/api/email-templates/:id/rollback` | Rollback to prior version
POST | `/api/email-templates/validate` | Validate Handlebars syntax
POST | `/api/email-templates/:id/test` | Send test email (rate limited: 10/15min)
GET | `/api/email-templates/:id/test-logs` | Test send logs
POST | `/api/email-templates/seed` | Seed templates from filesystem
POST | `/api/email-templates/clear-cache` | Clear template cache

### QR Codes

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/qr` |  | Generate QR code PNG (`?text=&size=50-500`)

Cached for 1 hour. Returns `image/png`.

### Site Settings

Prefix: `/api/settings`

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/settings` |  | Public site settings (SMTP credentials stripped)
GET | `/api/settings/admin` | `SUPER_ADMIN` | Full settings including SMTP credentials
PUT | `/api/settings` | `SUPER_ADMIN` | Update settings
POST | `/api/settings/email/test-connection` | `SUPER_ADMIN` | Test SMTP connection
POST | `/api/settings/email/test-send` | `SUPER_ADMIN` | Send test email

### Listmonk (Newsletter Sync)

Prefix: `/api/listmonk` · Auth: `SUPER_ADMIN`

Method | Path | Description
--- | --- | ---
GET | `/api/listmonk` | Sync status + connection check
GET | `/api/listmonk/stats` | Subscriber counts from Listmonk
POST | `/api/listmonk/test-connection` | Health check
POST | `/api/listmonk/sync/participants` | Sync campaign participants
POST | `/api/listmonk/sync/locations` | Sync locations
POST | `/api/listmonk/sync/users` | Sync users
POST | `/api/listmonk/sync/all` | Run all sync operations
POST | `/api/listmonk/reinitialize` | Reinitialize Listmonk lists
GET | `/api/listmonk/proxy-url` | Proxy port + JWT for iframe

### Documentation Management

Prefix: `/api/docs` · Auth: Authenticated, non-TEMP (write operations require `SUPER_ADMIN`)

Method | Path | Description
--- | --- | ---
GET | `/api/docs/status` | MkDocs + Code Server availability
GET | `/api/docs/config` | Port numbers for iframe URLs
GET | `/api/docs/mkdocs-config` | Read raw `mkdocs.yml`
PUT | `/api/docs/mkdocs-config` | Write `mkdocs.yml`
POST | `/api/docs/build` | Trigger MkDocs build
POST | `/api/docs/upload` | Upload asset (20 MB, whitelisted extensions)
GET | `/api/docs/files` | File tree (`?force=true` bypasses cache)
POST | `/api/docs/files/rename` | Rename or move file
GET | `/api/docs/files/*` | Read file content
PUT | `/api/docs/files/*` | Write file content
POST | `/api/docs/files/*` | Create file or folder
DELETE | `/api/docs/files/*` | Delete file or empty folder

### Services

Prefix: `/api/services` · Auth: `SUPER_ADMIN`

Method | Path | Description
--- | --- | ---
GET | `/api/services/status` | Health check all managed services (NocoDB, n8n, Gitea, MailHog, Mini QR, Excalidraw, Homepage)
GET | `/api/services/config` | Port numbers + subdomain info

### Pangolin (Tunnel Management)

Prefix: `/api/pangolin` · Auth: `SUPER_ADMIN`

Method | Path | Description
--- | --- | ---
GET | `/api/pangolin/status` | Tunnel health + connection info
GET | `/api/pangolin/config` | Current env configuration
GET | `/api/pangolin/newt-status` | Newt container status
POST | `/api/pangolin/newt-restart` | Restart Newt container
GET | `/api/pangolin/sites` | List Pangolin sites
GET | `/api/pangolin/exit-nodes` | Available exit nodes
GET | `/api/pangolin/resource-definitions` | Resource definitions from YAML
GET | `/api/pangolin/resources` | List resources
POST | `/api/pangolin/setup` | Create site + all resources (rate limited: ⅗min)
POST | `/api/pangolin/sync` | Sync resources (create missing, update changed)
PUT | `/api/pangolin/resource/:id` | Update resource
DELETE | `/api/pangolin/resource/:id` | Delete resource
GET | `/api/pangolin/resource/:id/clients` | Connected clients
GET | `/api/pangolin/certificate/:domainId/:domain` | Certificate info
POST | `/api/pangolin/certificate/:certId` | Update certificate

### Observability

Prefix: `/api/observability` · Auth: `SUPER_ADMIN` · Rate limited: 20/min

Method | Path | Description
--- | --- | ---
GET | `/api/observability/status` | Check 7 monitoring services
GET | `/api/observability/metrics-summary` | Key metrics from Prometheus
GET | `/api/observability/alerts` | Active alerts from Alertmanager

### Payments

Prefix: `/api/payments`

#### Public

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/payments/config` |  | Stripe publishable key + donation settings
GET | `/api/payments/plans` |  | Active subscription plans
GET | `/api/payments/products` |  | Active products (`?type=`)
POST | `/api/payments/subscribe` |  | Create subscription checkout
POST | `/api/payments/purchase` | Optional | Product checkout (guest or logged-in)
POST | `/api/payments/donate` |  | Donation checkout
GET | `/api/payments/my-subscription` |  | Current subscription
POST | `/api/payments/my-subscription/cancel` |  | Cancel subscription
POST | `/api/payments/webhook` |  | Stripe webhook (raw body)

#### Admin

Auth: `SUPER_ADMIN`

Method | Path | Description
--- | --- | ---
GET | `/api/payments/admin/settings` | Payment settings (secrets masked)
PUT | `/api/payments/admin/settings` | Update payment settings
POST | `/api/payments/admin/settings/test-connection` | Test Stripe connection
GET | `/api/payments/admin/dashboard` | Subscription + donation statistics
GET | `/api/payments/admin/plans` | All subscription plans
POST | `/api/payments/admin/plans` | Create plan
PUT | `/api/payments/admin/plans/:id` | Update plan
DELETE | `/api/payments/admin/plans/:id` | Delete plan
POST | `/api/payments/admin/plans/:id/sync-stripe` | Sync plan to Stripe
GET | `/api/payments/admin/subscriptions` | All subscriptions with filters
POST | `/api/payments/admin/subscriptions/:id/cancel` | Cancel subscription
GET | `/api/payments/admin/products` | All products
POST | `/api/payments/admin/products` | Create product
PUT | `/api/payments/admin/products/:id` | Update product
DELETE | `/api/payments/admin/products/:id` | Delete product
POST | `/api/payments/admin/products/:id/sync-stripe` | Sync product to Stripe
GET | `/api/payments/admin/orders` | List orders
POST | `/api/payments/admin/orders/:id/refund` | Refund order
GET | `/api/payments/admin/donations` | List donations
GET | `/api/payments/admin/export` | CSV export of completed orders

## Media API (Fastify — Port 4100)

The Media API is a separate Fastify server sharing the same PostgreSQL database. It handles all video-related functionality.

### Health

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/health` |  | Media API health check

### Videos (Admin)

Prefix: `/api/videos` · Auth: Admin roles

#### CRUD & Publishing

Method | Path | Description
--- | --- | ---
GET | `/api/videos` | List videos (`?limit=&offset=&search=&orientation=&producers=&isShort=`)
GET | `/api/videos/producers` | Distinct producer list
GET | `/api/videos/health` | Video count health check
GET | `/api/videos/:id` | Single video detail
PATCH | `/api/videos/:id` | Update metadata (title, producer, tags, quality, etc.)
POST | `/api/videos/:id/publish` | Publish to category
POST | `/api/videos/:id/unpublish` | Unpublish
POST | `/api/videos/bulk-publish` | Bulk publish
POST | `/api/videos/bulk-unpublish` | Bulk unpublish
POST | `/api/videos/:id/lock` | Lock published video
POST | `/api/videos/:id/unlock` | Unlock video
POST | `/api/videos/:id/generate-thumbnail` | Generate thumbnail via FFmpeg
POST | `/api/videos/bulk-generate-thumbnails` | Bulk thumbnail generation

#### Upload

Method | Path | Description
--- | --- | ---
POST | `/api/videos/upload` | Single video upload (multipart, 10 GB limit, streams to disk)
POST | `/api/videos/upload/batch` | Batch upload (returns 207 multi-status)

#### Actions

Method | Path | Description
--- | --- | ---
POST | `/api/videos/:id/duplicate` | Duplicate video record
POST | `/api/videos/:id/replace` | Replace video file, keep metadata
GET | `/api/videos/:id/analytics` | Detailed analytics (`?startDate=&endDate=`)
POST | `/api/videos/:id/reset-analytics` | Reset all analytics
GET | `/api/videos/:id/preview-link` | Generate 24-hour JWT preview link
GET | `/api/videos/analytics/top` | Top videos (`?metric=views|watchTime&limit=`)
GET | `/api/videos/analytics/overview` | Global analytics overview

#### Scheduling

Method | Path | Description
--- | --- | ---
POST | `/api/videos/:id/schedule-publish` | Schedule future publish (`{publishAt, timezone?}`)
POST | `/api/videos/:id/schedule-unpublish` | Schedule future unpublish
DELETE | `/api/videos/:id/schedule/:action` | Cancel scheduled operation
GET | `/api/videos/schedules/upcoming` | Upcoming scheduled operations
GET | `/api/videos/:id/schedule-history` | Schedule history for video
GET | `/api/videos/schedules/stats` | Schedule queue statistics
POST | `/api/videos/schedules/pause` | Pause schedule queue
POST | `/api/videos/schedules/resume` | Resume schedule queue
POST | `/api/videos/schedules/cleanup` | Clean old completed jobs

#### Video Fetch

Method | Path | Description
--- | --- | ---
POST | `/api/videos/fetch` | Submit fetch job (`{urls: string[]}`, 1–20 URLs)
GET | `/api/videos/fetch/jobs` | List recent fetch jobs
GET | `/api/videos/fetch/jobs/:jobId` | Job detail + log
GET | `/api/videos/fetch/jobs/:jobId/log` | SSE log stream (Redis pub/sub)
DELETE | `/api/videos/fetch/jobs/:jobId` | Cancel fetch job

### Streaming (Public)

Prefix: `/api/videos`

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/videos/stream/health` |  | Streaming health check
GET | `/api/videos/:id/stream` | Optional | HTTP range-supporting video stream
GET | `/api/videos/:id/thumbnail` | Optional | Serve thumbnail image
GET | `/api/videos/:id/metadata` |  | Public video metadata for embedding

Note

Admins can stream unpublished videos by providing a valid JWT.

### Public Gallery

Prefix: `/api/public`

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/public` | Optional | Published videos (`?limit=&offset=&search=&sort=recent|popular|oldest&category=`)
GET | `/api/public/categories` | Optional | Categories with video counts
GET | `/api/public/producers` | Optional | Published producers
GET | `/api/public/:id` | Optional | Single published video
GET | `/api/public/:id/thumbnail` | Optional | Published thumbnail
GET | `/api/public/:id/stream` | Optional | Published video stream

### Tracking

Prefix: `/api/track` · Auth: None required

Method | Path | Description
--- | --- | ---
GET | `/api/track/health` | Tracking health check
POST | `/api/track/view` | Record video view (returns `{viewId}`)
POST | `/api/track/event` | Record play/pause/seek/complete event
POST | `/api/track/heartbeat` | Update watch time (10s interval, `sendBeacon`)
POST | `/api/track/batch` | Batch up to 50 tracking events

Tracking is GDPR-compliant

IP addresses are hashed with a daily-rotating salt. Raw IPs are never stored. Tracking data is retained for 90 days.

### Reactions

Prefix: `/api/reactions`

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/reactions/config` |  | Available reaction types + emoji mappings
GET | `/api/reactions` |  | List reactions (`?mediaId=&userId=&limit=`)
GET | `/api/reactions/:mediaId/chat` |  | Reactions in chat timeline format
POST | `/api/reactions` |  | Add reaction (30s cooldown per type)

Available types: `like`, `love`, `laugh`, `wow`, `sad`, `angry`

### Comments & Chat

#### Public Comments

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/public/:id/comments` |  | List comments (`?limit=&offset=`)
POST | `/api/public/:id/comments` | Optional | Create comment (word-filtered; rate limited: 5/min)
GET | `/api/public/:id/chat-stream` |  | SSE stream for real-time chat (30s keepalive)

#### Comment Admin

Prefix: `/api/media/admin/comments` · Auth: Admin roles

Method | Path | Description
--- | --- | ---
GET | `/api/media/admin/comments/stats` | Counts by status
GET | `/api/media/admin/comments` | All comments with filters
PATCH | `/api/media/admin/comments/:id/approve` | Approve comment
PATCH | `/api/media/admin/comments/:id/hide` | Hide comment
PATCH | `/api/media/admin/comments/:id/unhide` | Unhide comment
PUT | `/api/media/admin/comments/:id/notes` | Update moderation notes
DELETE | `/api/media/admin/comments/:id` | Delete comment

#### Word Filters

Prefix: `/api/media/admin/word-filters` · Auth: Admin roles

Method | Path | Description
--- | --- | ---
GET | `/api/media/admin/word-filters` | List filter entries grouped by level
POST | `/api/media/admin/word-filters` | Add word (`{word, level: low|medium|high|custom}`)
DELETE | `/api/media/admin/word-filters/:id` | Remove word

#### Chat Threads & Notifications

Auth: Authenticated

Method | Path | Description
--- | --- | ---
GET | `/api/media/chat/threads` | Videos with user's comments + unread counts
POST | `/api/media/chat/threads/:mediaId/read` | Mark thread as read
GET | `/api/media/notifications/stream` | Per-user SSE notification stream (`?token=`)

### Shorts

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/shorts` | Optional | Shorts feed (`?sort=recent|popular|random`)
POST | `/api/shorts/scan` | Admin | Auto-classify short videos by duration

### Upvotes

Method | Path | Auth | Description
--- | --- | --- | ---
POST | `/api/public/:id/upvote` |  | Toggle upvote (session-based via `X-Session-ID` header)
GET | `/api/public/:id/upvote-status` |  | Check upvote status for current session

### Playlists

#### Public

Prefix: `/api/playlists`

Method | Path | Auth | Description
--- | --- | --- | ---
GET | `/api/playlists/featured` | Optional | Featured playlists
GET | `/api/playlists/popular` | Optional | Popular public playlists (`?search=`)
GET | `/api/playlists/share/:token` | Optional | Playlist by share token
GET | `/api/playlists/:id` | Optional | Playlist detail (public, owner, or share token)
POST | `/api/playlists/:id/view` | Optional | Record playlist view

#### User Playlists

Auth: Authenticated

Method | Path | Description
--- | --- | ---
GET | `/api/playlists/my` | Own playlists
POST | `/api/playlists` | Create playlist
PUT | `/api/playlists/:id` | Update playlist (ownership check)
DELETE | `/api/playlists/:id` | Delete playlist
POST | `/api/playlists/:id/videos` | Add video (`{mediaId}`)
DELETE | `/api/playlists/:id/videos/:mediaId` | Remove video
PUT | `/api/playlists/:id/videos/reorder` | Reorder videos
POST | `/api/playlists/:id/share` | Generate share token
DELETE | `/api/playlists/:id/share` | Revoke share token

#### Playlist Admin

Prefix: `/api/media/playlists` · Auth: Admin roles

Method | Path | Description
--- | --- | ---
GET | `/api/media/playlists` | All playlists
GET | `/api/media/playlists/featured` | Featured playlists with admin info
POST | `/api/media/playlists/:id/feature` | Feature a playlist
DELETE | `/api/media/playlists/:id/feature` | Unfeature a playlist
PUT | `/api/media/playlists/featured/reorder` | Reorder featured playlists
PUT | `/api/media/playlists/:id` | Admin update any playlist
POST | `/api/media/playlists/:id/duplicate` | Duplicate playlist
DELETE | `/api/media/playlists/:id` | Admin delete any playlist

### User Profile

Prefix: `/api/media/me` · Auth: Authenticated

Method | Path | Description
--- | --- | ---
GET | `/api/media/me/stats` | User stats + 30-day activity + achievements
GET | `/api/media/me/watch-history` | Paginated watch history
POST | `/api/media/me/stats/recalculate` | Recompute stats from raw data
GET | `/api/media/me/settings` | Privacy settings
PUT | `/api/media/me/settings` | Update privacy settings
PUT | `/api/media/me/profile` | Update display name
PUT | `/api/media/me/password` | Change password

## Route Summary

API | Module | Endpoint Count
--- | --- | ---
Express | Auth | 9
| Users | 7
| Dashboard | 7
| Campaigns (CRUD + public + user + moderation + emails) | 16
| Responses | 10
| Email Queue | 4
| Representatives | 7
| Locations (CRUD + geocode + import) | 21
| Cuts | 11
| Shifts (CRUD + series) | 19
| Canvassing | 20
| GPS Tracking | 10
| Map Settings + Geocoding | 3
| Pages + Blocks | 12
| Email Templates | 13
| QR Codes | 1
| Site Settings | 5
| Listmonk | 9
| Docs Management | 11
| Services | 2
| Pangolin | 16
| Observability | 3
| Payments (public + admin) | 29
| Health + Metrics | 3
Express Total |  | ~248
Fastify | Videos (CRUD + upload + actions + schedule + fetch) | 39
| Streaming | 4
| Public Gallery | 6
| Tracking | 5
| Reactions | 4
| Comments + Chat | 13
| Shorts + Upvotes | 4
| Playlists (public + user + admin) | 18
| User Profile | 7
| Health | 1
Fastify Total |  | ~101
Grand Total |  | ~349