Youth events need a name and date of birth for every attendee, and a parent/guardian's details for under-18s. Parents get an email and WhatsApp link to a consent page that records the signed name, time, IP, device and exact consent text; medical and emergency details are stored encrypted. Tickets are issued but check-in is blocked until consent is given (supervisor override audited). A daily job sends reminders and purges medical details after the retention period. Adds a supervisor consent tracker, consent status and medical list reports, and clears consent data when an account is anonymised. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
616 lines
28 KiB
Markdown
616 lines
28 KiB
Markdown
# Hope Events — API Documentation
|
||
|
||
Full interactive docs (with request/response examples) are available at `GET /docs?token=<admin-jwt>` on the running backend.
|
||
|
||
All authenticated requests require:
|
||
```
|
||
Authorization: Bearer <jwt>
|
||
```
|
||
|
||
Tokens are issued by `POST /api/users/login`.
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
- [Users](#users)
|
||
- [Events](#events)
|
||
- [Event Options & Variants](#event-options--variants)
|
||
- [Event Attachments](#event-attachments)
|
||
- [Registrations](#registrations)
|
||
- [Coupons](#coupons)
|
||
- [Parental consent](#parental-consent)
|
||
- [Payments](#payments)
|
||
- [Tickets](#tickets)
|
||
- [Broadcasts (Email)](#broadcasts-email)
|
||
- [WhatsApp Broadcasts](#whatsapp-broadcasts)
|
||
- [Scheduled Messages](#scheduled-messages)
|
||
- [Automations](#automations)
|
||
- [Reports](#reports)
|
||
- [Uploads](#uploads)
|
||
- [Sections](#sections)
|
||
- [Banner](#banner)
|
||
- [Forms](#forms)
|
||
- [Yoco Transactions](#yoco-transactions)
|
||
- [WhatsApp Admin](#whatsapp-admin)
|
||
- [Webhooks](#webhooks)
|
||
- [Settings](#settings)
|
||
- [Setup](#setup)
|
||
- [Static Files](#static-files)
|
||
|
||
---
|
||
|
||
## Users
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/users` | public | Register new account |
|
||
| POST | `/api/users/login` | public | Login, returns JWT — or a two-factor challenge when the account has 2FA on |
|
||
| POST | `/api/users/login/2fa` | public | Exchange a two-factor code for the JWT |
|
||
| POST | `/api/users/login/2fa/resend` | public | Send a new code for a pending login challenge (60s cooldown) |
|
||
| POST | `/api/users/forgot` | public | Request password reset email |
|
||
| POST | `/api/users/reset` | public | Reset password with token |
|
||
| POST | `/api/users/activate` | public | Activate account from email link |
|
||
| GET | `/api/users/profile` | user+ | Own profile |
|
||
| PUT | `/api/users/profile` | user+ | Update own profile |
|
||
| POST | `/api/users/revoke-sessions` | user+ | Revoke own sessions |
|
||
| POST | `/api/users/close-account` | user+ | Deactivate own account |
|
||
| POST | `/api/users/2fa/enable` | user+ | Start turning on 2FA — sends a code to the chosen channel |
|
||
| POST | `/api/users/2fa/confirm` | user+ | Finish turning on 2FA with that code |
|
||
| POST | `/api/users/2fa/disable` | user+ | Turn off 2FA (requires password) |
|
||
| POST | `/api/users/verify-password` | user+ | Re-confirm own password without starting a new session |
|
||
| GET | `/api/users` | supervisor+ | List users — supports `?search=`, `?role=`, `?isActive=true\|false`, `?page=`, `?limit=` |
|
||
| GET | `/api/users/:id` | admin | Get user by ID |
|
||
| PUT | `/api/users/:id` | admin | Update user (name, email, role, isActive, phoneNumber). `twoFactorEnabled: false` resets a user's 2FA; it cannot be switched on this way |
|
||
| DELETE | `/api/users/:id` | admin | Deactivate user (`isActive: false`) |
|
||
| POST | `/api/users/:id/revoke-sessions` | admin | Revoke all sessions for a user |
|
||
| POST | `/api/users/:id/anonymize` | admin | Erase personal data — sets name to "Deleted User", clears email/phone, deactivates account |
|
||
|
||
### POST /api/users
|
||
```json
|
||
{
|
||
"name": "Jane Smith",
|
||
"email": "jane@example.com",
|
||
"password": "securepass123",
|
||
"phoneNumber": "+27821234567"
|
||
}
|
||
```
|
||
Returns: `{ _id, name, email, role, token }`
|
||
|
||
### POST /api/users/login
|
||
```json
|
||
{ "email": "jane@example.com", "password": "securepass123", "deviceToken": "optional-trusted-device-token" }
|
||
```
|
||
Returns: `{ _id, name, email, role, token }`
|
||
|
||
If the account has two-factor authentication on and `deviceToken` is missing, expired or not recognised, no token is issued. A 6-digit code is sent by email or WhatsApp and the response is instead:
|
||
```json
|
||
{ "twoFactorRequired": true, "challengeId": "uuid", "method": "email", "destination": "j•••@example.com" }
|
||
```
|
||
|
||
**Required for admins and supervisors.** An admin or supervisor without 2FA gets the same challenge with `"enrolling": true` (trusted devices don't skip it), sent to their email when SMTP is configured, otherwise WhatsApp. Verifying it turns 2FA on with that channel. If neither channel can deliver a code, the requirement is skipped so the account isn't locked out. `TWO_FACTOR_REQUIRED_ROLES=false` turns the requirement off.
|
||
|
||
### POST /api/users/login/2fa
|
||
```json
|
||
{ "challengeId": "uuid", "code": "123456", "rememberDevice": true }
|
||
```
|
||
Returns: `{ _id, name, email, role, token }`, plus `deviceToken` when `rememberDevice` is `true`. Send that `deviceToken` with later logins to skip the code for 30 days. Codes expire after 10 minutes and allow 5 attempts. Trusted devices are cleared when the password changes or sessions are revoked.
|
||
|
||
### Two-factor setup (`/api/users/2fa/*`)
|
||
- `POST /api/users/2fa/enable` — `{ "method": "email" | "whatsapp", "password": "..." }` sends a code to that channel and returns `{ challengeId, method, destination }`. WhatsApp needs a valid phone number on the account and a connected WhatsApp session.
|
||
- `POST /api/users/2fa/confirm` — `{ "challengeId": "uuid", "code": "123456" }` turns 2FA on.
|
||
- `POST /api/users/2fa/disable` — `{ "password": "..." }` turns it off. Returns 403 for admins and supervisors, who must keep it on (they can switch channel with `enable`/`confirm`).
|
||
|
||
An admin or supervisor session without 2FA (from before the requirement, or after a promotion or 2FA reset) is rejected by every protected route with `401 { "code": "TWO_FACTOR_REQUIRED" }`; logging in again enrols them. Promoting a user to supervisor/admin ends their sessions when they don't have 2FA yet.
|
||
|
||
At login, if a WhatsApp code can't be sent, the code falls back to the account's email address.
|
||
|
||
### PUT /api/users/profile
|
||
```json
|
||
{ "name": "Jane Doe", "phoneNumber": "+27829999999", "notificationPreference": "email" }
|
||
```
|
||
`notificationPreference`: `"email"` | `"whatsapp"` | `"both"`
|
||
|
||
---
|
||
|
||
## Events
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/events` | public | Active, public, upcoming events. Includes `isSoldOut: boolean` per event. |
|
||
| GET | `/api/events/all` | staff+ | All events including hidden/past (`?includePast=true`, `?includeInactive=true`) |
|
||
| GET | `/api/events/admin/all` | admin | Admin view of all events |
|
||
| GET | `/api/events/:id` | public | Full event detail — options, variants, earlyBirdTiers, attachments, form, availableCount per option/variant |
|
||
| GET | `/api/events/by-alias/:redirectUrl` | public | Get event by URL alias |
|
||
| POST | `/api/events` | supervisor+ | Create event |
|
||
| PUT | `/api/events/:id` | supervisor+ | Update event |
|
||
| DELETE | `/api/events/:id` | admin | Delete event |
|
||
| POST | `/api/events/:id/email-attendees` | supervisor+ | Bulk email attendees (`dryRun: true` to preview) |
|
||
| POST | `/api/events/:id/email-attendees/schedule` | supervisor+ | Schedule bulk email |
|
||
| POST | `/api/events/:id/whatsapp-attendees` | supervisor+ | Bulk WhatsApp to attendees (`dryRun: true` to preview) |
|
||
| POST | `/api/events/:id/whatsapp-attendees/schedule` | supervisor+ | Schedule bulk WhatsApp |
|
||
| GET | `/api/events/attachments/status` | admin | Attachment manifest sync status |
|
||
| POST | `/api/events/attachments/sync` | admin | Re-sync attachment manifests to DB |
|
||
|
||
### isSoldOut logic (`GET /api/events`)
|
||
`isSoldOut` is `true` when every option on the event that has `stockLimit > 0` has sold out its full allocation (computed from non-cancelled `RegistrationOption` quantities). Events with no limited options are never marked sold out.
|
||
|
||
### POST /api/events
|
||
```json
|
||
{
|
||
"title": "Family Camp 2025",
|
||
"description": "Annual family camp at the farm.",
|
||
"startDate": "2025-07-10T08:00:00.000Z",
|
||
"endDate": "2025-07-14T17:00:00.000Z",
|
||
"registrationDeadline": "2025-07-01T00:00:00.000Z",
|
||
"goLiveAt": "2025-05-01T00:00:00.000Z",
|
||
"price": 450,
|
||
"picture": "/uploads/events/camp.jpg",
|
||
"redirectUrl": "camp2025",
|
||
"isHidden": false,
|
||
"requiresAuth": true,
|
||
"form": {
|
||
"isRequired": true,
|
||
"fields": [
|
||
{ "type": "text", "label": "Dietary requirements", "isRequired": false, "order": 0 }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Event Options & Variants
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/events/:id/options` | supervisor+ | Add option to event |
|
||
| PUT | `/api/events/options/:id` | supervisor+ | Update option (name, price, stockLimit, variants, earlyBirdTiers) |
|
||
| DELETE | `/api/events/options/:id` | admin | Delete option (fails if registrations exist) |
|
||
| POST | `/api/events/options/:id/variants` | supervisor+ | Add variant to option |
|
||
| PUT | `/api/events/variants/:id` | supervisor+ | Update variant |
|
||
| DELETE | `/api/events/variants/:id` | admin | Delete variant |
|
||
|
||
### POST /api/events/:id/options
|
||
```json
|
||
{
|
||
"name": "Adult",
|
||
"price": 450,
|
||
"isMainTicket": false,
|
||
"stockLimit": 100
|
||
}
|
||
```
|
||
|
||
### PUT /api/events/options/:id — with variants and early-bird tiers
|
||
```json
|
||
{
|
||
"name": "Adult",
|
||
"price": 450,
|
||
"stockLimit": 100,
|
||
"variants": [
|
||
{ "name": "Standard", "price": null, "stockLimit": 60, "order": 0 },
|
||
{ "name": "VIP", "price": 600, "stockLimit": 40, "order": 1 }
|
||
],
|
||
"earlyBirdTiers": [
|
||
{ "deadline": "2025-06-01T00:00:00.000Z", "price": 350, "stockLimit": 30, "order": 0, "variantId": null }
|
||
]
|
||
}
|
||
```
|
||
|
||
`stockLimit: 0` means unlimited. Early-bird tiers can be option-level (`variantId: null`) or variant-level (`variantId: "<uuid>"`).
|
||
|
||
---
|
||
|
||
## Event Attachments
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/events/:id/attachments` | public | List attachments for event |
|
||
| POST | `/api/events/:id/attachments` | supervisor+ | Upload attachment (`multipart/form-data`, field `file`) |
|
||
| DELETE | `/api/events/:eventId/attachments/:id` | supervisor+ | Delete attachment |
|
||
|
||
Accepted file types: `.pdf`, `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.txt`, `.csv`, `.zip`. Max 10 MB.
|
||
|
||
---
|
||
|
||
## Registrations
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/registrations` | optional | Create registration |
|
||
| GET | `/api/registrations/myregistrations` | user+ | Own registrations |
|
||
| GET | `/api/registrations/:id` | optional | Get registration by ID |
|
||
| PUT | `/api/registrations/:id/options` | user+ | Update selected options |
|
||
| DELETE | `/api/registrations/:id` | user+ | Cancel registration (blocked if positive payments exist unless admin) |
|
||
| GET | `/api/registrations/:id/forms/draft` | optional | Get form draft |
|
||
| PUT | `/api/registrations/:id/forms/draft` | optional | Save form draft |
|
||
| POST | `/api/registrations/:id/forms/responses` | optional | Submit form responses |
|
||
| PUT | `/api/registrations/:id/forms/responses` | supervisor+ | Replace form responses |
|
||
| GET | `/api/registrations` | staff+ | All registrations |
|
||
| PUT | `/api/registrations/:id` | staff+ | Update registration status |
|
||
| GET | `/api/registrations/event/:eventId` | staff+ | Registrations for an event |
|
||
| POST | `/api/registrations/manual` | supervisor+ | Manual registration (accepts `couponCode`) |
|
||
| PUT | `/api/registrations/:id/attendees` | user+ | Replace the named attendees (`{ attendees: [{ id?, eventOptionId, variantId?, name, email?, phone? }], ticketDelivery? }`); owner or staff |
|
||
| POST | `/api/registrations/:id/attendees/:attendeeId/send-form-link` | user+ | Email/WhatsApp an attendee the link to their own form |
|
||
| POST | `/api/registrations/:id/coupon` | user+ | Apply a code to an existing registration (owner, or staff on someone's behalf) |
|
||
| DELETE | `/api/registrations/:id/coupon` | staff+ | Remove the code (refused once a ticket on a paid registration has been scanned) |
|
||
|
||
### POST /api/registrations
|
||
```json
|
||
{
|
||
"eventId": "ev-uuid-...",
|
||
"options": [
|
||
{ "eventOptionId": "opt-uuid-...", "quantity": 2 },
|
||
{ "eventOptionId": "opt-uuid-...", "quantity": 1, "variantId": "variant-uuid-..." }
|
||
]
|
||
}
|
||
```
|
||
Add `"couponCode": "STAFF50"` to apply a discount code. The code is checked before anything is saved (400 with the reason if it can't be used). If the last use is taken by someone else in the moment between checking and saving, the registration is still created at full price and the response includes `couponError`.
|
||
|
||
**Group tickets:** on events with `allowNamedAttendees`, `POST /api/registrations` and `/manual` accept `attendees: [{ eventOptionId, variantId?, name, email?, phone? }]` (main-ticket places only, no more than the places bought) and `ticketDelivery: "payer" | "each"`.
|
||
- Each named attendee gets their own quantity-1 ticket; unnamed places share one quantity ticket.
|
||
- An attendee whose email/phone matches one existing account is linked to it, so the ticket also appears in that person's `GET /api/tickets/mytickets`.
|
||
- With `"each"`, tickets are emailed/WhatsApped to attendees directly, falling back to the payer.
|
||
- Form responses can carry `attendeeId`.
|
||
|
||
Registration responses include `discountTotal` (the coupon discount), `subtotalComputed` (items before the discount), `totalDueComputed` (after it) and `couponRedemptions` (the active code).
|
||
|
||
For unauthenticated (guest) registration on events with `requiresAuth: false`:
|
||
```json
|
||
{
|
||
"eventId": "ev-uuid-...",
|
||
"options": [...],
|
||
"guestName": "Jane Smith",
|
||
"guestEmail": "jane@example.com",
|
||
"guestPhone": "+27821234567"
|
||
}
|
||
```
|
||
|
||
### Cancellation rules
|
||
|
||
| Actor | Condition | Result |
|
||
|-------|-----------|--------|
|
||
| Owner | No positive payments | 200 — status set to `cancelled` |
|
||
| Owner | Any positive payment exists | 403 — must contact organisation |
|
||
| Admin | Any state | 200 — always allowed |
|
||
| Anyone | Already cancelled | 400 |
|
||
|
||
---
|
||
|
||
## Parental consent
|
||
|
||
Youth events (`requiresParentalConsent`, plus `consentText`, `consentCollectMedical`, `consentCollectEmergency` and `consentCollectPhoto` on the event) need every main place named with a `dateOfBirth`. Attendees under 18 on the start date also need `parentName`, `parentEmail` and `parentPhone`. Changing `consentText` bumps `consentTextVersion`.
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/consent/:token` | public | What the parent sees: attendee, event, consent text, fields to fill in |
|
||
| POST | `/api/consent/:token` | public | `{ decision: "give"\|"decline", signedName, isGuardian: true, medical?, emergency?, photoConsent? }` |
|
||
| GET | `/api/consent/events/:eventId` | staff+ | Consent status per attendee, with counts |
|
||
| POST | `/api/consent/requests/:id/resend` | user+ | Re-send one request with a new link (registrant or staff) |
|
||
| POST | `/api/consent/events/:eventId/resend-pending` | supervisor+ | Re-send every pending request |
|
||
| POST | `/api/consent/events/:eventId/rerequest` | supervisor+ | New requests for consent given on older text, and minors with none |
|
||
| GET | `/api/consent/events/:eventId/medical` | supervisor+ | Decrypted medical & emergency list (audited as `medical_list_viewed`) |
|
||
|
||
How requests work:
|
||
- Requests go to the parent by both email and WhatsApp, whatever anyone's notification preference.
|
||
- Links are random tokens; only an HMAC is stored. They expire when the event ends.
|
||
- The answer stores the signed name, time, IP, device, and the exact text and version shown.
|
||
- Medical and emergency details are AES-256-GCM encrypted. A daily job (09:00, `CONSENT_JOBS_ENABLED=false` to disable) sends reminders (day 2, then every 3 days) and deletes them `consent_medical_retention_days` (default 30) after the event.
|
||
|
||
Check-in rules:
|
||
- `GET /api/tickets/scan-preview/:qrCode` returns `consentStatus` (`not_required`, `missing`, `pending`, `given` or `declined`).
|
||
- `POST /api/tickets/scan/:qrCode` returns `403 { code: "CONSENT_REQUIRED" }` unless consent is given.
|
||
- Supervisors can pass `overrideConsent: true`, which is audited as `consent_override`.
|
||
|
||
## Coupons
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/coupons/validate` | optional | Check a code against a basket (`{ code, eventId, options, forUserId? }`) |
|
||
| GET | `/api/coupons` | supervisor+ | List codes with use counts and total discount |
|
||
| POST | `/api/coupons` | supervisor+ | Create a code |
|
||
| GET | `/api/coupons/:id` | supervisor+ | One code with its uses |
|
||
| PUT | `/api/coupons/:id` | supervisor+ | Edit or turn off a code |
|
||
| GET | `/api/coupons/redemptions` | supervisor+ | Uses for the Discounts report (`eventIds`, `from`, `to`) |
|
||
|
||
- **Types:** `percent` (value 1–100), `fixed` (rand amount), `comp` (free).
|
||
- **Scope:** `eventIds` (none = every event) and `options: [{ eventOptionId, variantId? }]` (none = the whole registration).
|
||
- **Restrictions:** `userIds`, `restrictRoles` and `restrictDomain`. A match on any one is enough, and restricted codes need a known account.
|
||
- **Limits:** `validFrom`, `validUntil`, `maxUses`, `maxUsesPerUser` (default 1) and `minQuantity`.
|
||
- **Early-bird:** `stackEarlyBird: false` means the better price wins; `true` discounts the early-bird price.
|
||
- **Locked amount:** the discount is locked when applied. Editing items recalculates percentage and comp codes, but fixed codes keep their amount. Cancelling releases the use.
|
||
|
||
Options and variants also accept an optional `staffPrice`, charged automatically to Staff and above when it's lower than the normal price.
|
||
|
||
## Payments
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/payments` | supervisor+ | Record manual payment (cash/EFT) |
|
||
| POST | `/api/payments/yoco-checkout` | user+ | Initiate Yoco checkout; re-evaluates early-bird pricing first — returns `{ priceUpdated: true, newTotal }` if prices changed |
|
||
| GET | `/api/payments/mypayments` | user+ | Own payments |
|
||
| GET | `/api/payments/:id` | user+ | Payment by ID |
|
||
| GET | `/api/payments/registration/:registrationId` | user+ | Payments for a registration |
|
||
| GET | `/api/payments` | supervisor+ | All payments |
|
||
| GET | `/api/payments/event/:eventId` | staff+ | Payments for an event |
|
||
| PUT | `/api/payments/assign-donation` | supervisor+ | Assign a donation payment to a registration |
|
||
| POST | `/api/payments/refund` | supervisor+ | Create refund record |
|
||
| GET | `/api/payments/admin/stats` | admin | Payment statistics |
|
||
|
||
### POST /api/payments (manual)
|
||
```json
|
||
{
|
||
"registrationId": "reg-uuid-...",
|
||
"amount": 450,
|
||
"method": "cash",
|
||
"note": "Paid at the door"
|
||
}
|
||
```
|
||
|
||
### POST /api/payments/yoco-checkout
|
||
```json
|
||
{
|
||
"registrationId": "reg-uuid-...",
|
||
"successUrl": "https://events.yourchurch.org/payment/success",
|
||
"cancelUrl": "https://events.yourchurch.org/payment/cancel",
|
||
"failureUrl": "https://events.yourchurch.org/payment/failure"
|
||
}
|
||
```
|
||
Returns: `{ redirectUrl }` or `{ priceUpdated: true, newTotal, message }` if early-bird prices changed.
|
||
|
||
---
|
||
|
||
## Tickets
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/tickets/mytickets` | user+ | Own tickets (excludes cancelled registrations) |
|
||
| GET | `/api/tickets/:id` | user+ | Ticket by ID |
|
||
| POST | `/api/tickets/email` | user+ | Email own tickets as PDF |
|
||
| POST | `/api/tickets/send-to` | staff+ | Send tickets to a specific user |
|
||
| GET | `/api/tickets/scan-preview/:qrCode` | staff+ | Preview scan (no mark) |
|
||
| GET | `/api/tickets/qr/:qrCode` | staff+ | Ticket by QR code |
|
||
| POST | `/api/tickets/scan/:qrCode` | staff+ | Scan/redeem ticket |
|
||
| GET | `/api/tickets/event/:eventId` | staff+ | All tickets for an event |
|
||
| GET | `/api/tickets/scans/recent` | staff+ | Recent scan activity |
|
||
| GET | `/api/tickets/scans/stats` | staff+ | Scan statistics |
|
||
| POST | `/api/tickets/generate` | supervisor+ | Generate tickets for a registration |
|
||
| GET | `/api/tickets` | supervisor+ | All tickets |
|
||
| PUT | `/api/tickets/email-sent` | admin | Bulk mark tickets as emailed |
|
||
|
||
---
|
||
|
||
## Broadcasts (Email)
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/broadcasts/preview` | supervisor+ | Dry-run — returns recipient count and sample |
|
||
| POST | `/api/broadcasts/send` | supervisor+ | Send email broadcast immediately |
|
||
| POST | `/api/broadcasts/schedule` | supervisor+ | Schedule email broadcast |
|
||
|
||
### POST /api/broadcasts/send
|
||
```json
|
||
{
|
||
"subject": "Camp 2025 — Final reminder",
|
||
"body": "Hi {{name}}, just a reminder that camp starts tomorrow at 08:00.",
|
||
"userIds": ["user-uuid-1", "user-uuid-2"],
|
||
"extraEmails": ["extra@example.com"]
|
||
}
|
||
```
|
||
|
||
Dynamic placeholders: `{{name}}`, `{{event.title}}`, `{{event.start}}`, `{{event.link}}`, `{{balance}}`, `{{promo.title}}`, `{{promo.link}}`
|
||
|
||
---
|
||
|
||
## WhatsApp Broadcasts
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/whatsapp-broadcasts/preview` | supervisor+ | Dry-run |
|
||
| POST | `/api/whatsapp-broadcasts/send` | supervisor+ | Send WhatsApp broadcast immediately |
|
||
| POST | `/api/whatsapp-broadcasts/schedule` | supervisor+ | Schedule WhatsApp broadcast |
|
||
|
||
---
|
||
|
||
## Scheduled Messages
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/scheduled-emails` | supervisor+ | List all queued/scheduled jobs |
|
||
| PATCH | `/api/scheduled-emails/:id` | supervisor+ | Edit scheduled job (reschedule, update content) |
|
||
| DELETE | `/api/scheduled-emails/:id` | supervisor+ | Cancel a queued job |
|
||
|
||
---
|
||
|
||
## Automations
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/automations/schedule` | supervisor+ | Schedule event lifecycle automations (pre-event reminder, thank-you, promo, etc.) |
|
||
|
||
---
|
||
|
||
## Reports
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/reports/pdf` | user+ | Generate and download PDF attendance/payment report |
|
||
| POST | `/api/reports/email` | user+ | Email PDF report to current user |
|
||
|
||
---
|
||
|
||
## Uploads
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/uploads/event-image` | supervisor+ | Upload event cover image (`multipart/form-data`, field `image`). Returns `{ url }`. Max 5 MB. Formats: jpg, jpeg, png, webp, gif. |
|
||
| POST | `/api/uploads/logo` | admin (or unauthenticated during setup) | Upload site logo (`multipart/form-data`, field `image`). Returns `{ url }`. Max 2 MB. Formats: jpg, jpeg, png, webp, svg. |
|
||
|
||
---
|
||
|
||
## Sections
|
||
|
||
Groups of event options shown as labelled sections on the registration page (e.g. "Tickets", "Add-ons", "Accommodation").
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/sections` | staff+ | List sections |
|
||
| POST | `/api/sections` | supervisor+ | Create section |
|
||
| PUT | `/api/sections/:id` | supervisor+ | Update section |
|
||
| DELETE | `/api/sections/:id` | admin | Delete section |
|
||
|
||
### POST /api/sections
|
||
```json
|
||
{
|
||
"eventId": "ev-uuid-...",
|
||
"name": "Accommodation",
|
||
"allowedOptionIds": ["opt-uuid-1", "opt-uuid-2"]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Banner
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/banner` | public | Get current site-wide banner (text, colour, active flag) |
|
||
| POST | `/api/banner` | supervisor+ | Set site banner |
|
||
|
||
### POST /api/banner
|
||
```json
|
||
{
|
||
"text": "Camp registrations close Friday!",
|
||
"color": "#f59e0b",
|
||
"isActive": true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Forms
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/forms/responses` | staff+ | List all submitted form responses across all events; supports filtering |
|
||
|
||
---
|
||
|
||
## Yoco Transactions
|
||
|
||
Raw webhook events from Yoco. Used for reconciling payments that weren't matched automatically.
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/yoco-transactions` | supervisor+ | All Yoco webhook transactions |
|
||
| GET | `/api/yoco-transactions/unreconciled` | supervisor+ | Transactions not yet matched to a payment |
|
||
| POST | `/api/yoco-transactions/:id/reconcile` | supervisor+ | Manually link transaction to a registration |
|
||
| POST | `/api/yoco-transactions/:id/ignore` | supervisor+ | Mark transaction as intentionally ignored |
|
||
|
||
---
|
||
|
||
## WhatsApp Admin
|
||
|
||
Manages the WAWP WhatsApp API instance. Credentials are stored in `AppSetting` (preferred) or `.env` (fallback).
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/whatsapp/config` | admin | Get current WAWP credentials |
|
||
| POST | `/api/whatsapp/config` | admin | Save WAWP credentials to DB |
|
||
| GET | `/api/whatsapp/status` | admin | WAWP session status |
|
||
| GET | `/api/whatsapp/qr` | admin | QR code for WhatsApp device linking |
|
||
| POST | `/api/whatsapp/create-instance` | admin | Create a new WAWP instance |
|
||
| POST | `/api/whatsapp/delete-instance` | admin | Delete WAWP instance |
|
||
| POST | `/api/whatsapp/request-code` | admin | Request pairing code (alternative to QR) |
|
||
| POST | `/api/whatsapp/logout` | admin | Logout current WAWP session |
|
||
| POST | `/api/whatsapp/start` | admin | Start WAWP instance |
|
||
| POST | `/api/whatsapp/restart` | admin | Restart WAWP instance |
|
||
|
||
---
|
||
|
||
## Webhooks
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/webhooks/yoco` | HMAC-verified | Yoco payment webhook — verifies `X-Yoco-Signature`, creates `Payment` + `YocoTransaction`, updates registration status, generates tickets |
|
||
| POST | `/api/webhooks/whatsapp` | — | WAWP inbound event webhook |
|
||
|
||
---
|
||
|
||
## Settings
|
||
|
||
Runtime configuration stored in the `AppSetting` table. Sensitive values (SMTP credentials, WAWP token) are stored AES-256-GCM encrypted.
|
||
|
||
For what each individual setting key defaults to when it's never been saved, see [`SETTINGS.md`](../SETTINGS.md) at the repo root.
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| GET | `/api/settings` | public | Public settings (org name, tagline, colour, logo, legal keys) |
|
||
| GET | `/api/settings/all` | admin | All settings — `smtp_pass` masked as `••••••••`, `smtp_user` decrypted |
|
||
| GET | `/api/settings/needs-setup` | public | `{ needsSetup: bool }` — true until setup wizard completes |
|
||
| PUT | `/api/settings` | admin | Upsert settings; `smtp_user`/`smtp_pass` encrypted before storage; sending `••••••••` for `smtp_pass` keeps existing value |
|
||
| POST | `/api/settings/test-smtp` | admin | Test SMTP with provided credentials; sends real test email to admin on success. The admin JWT returned by `POST /api/setup/register` also qualifies during the setup wizard. |
|
||
|
||
### POST /api/settings/test-smtp
|
||
```json
|
||
{
|
||
"host": "smtp.office365.com",
|
||
"port": 587,
|
||
"secure": false,
|
||
"user": "noreply@yourchurch.org",
|
||
"pass": "app-password-here",
|
||
"from": "noreply@yourchurch.org"
|
||
}
|
||
```
|
||
|
||
**Success (200):**
|
||
```json
|
||
{ "message": "SMTP connection verified — a test email has been sent to admin@yourchurch.org" }
|
||
```
|
||
|
||
**Failure (400):**
|
||
```json
|
||
{
|
||
"message": "Authentication failed — check your SMTP username and password.",
|
||
"raw": "530 5.7.57 Client not authenticated to send mail. [JN3P275CA0121...]"
|
||
}
|
||
```
|
||
|
||
The `message` field is always a human-readable sentence. The `raw` field is included on errors only and contains the original SMTP error for debugging. Error code 530 (Microsoft "Client not authenticated") maps to the authentication-failure message.
|
||
|
||
---
|
||
|
||
## Setup
|
||
|
||
One-time first-run wizard. Both endpoints are blocked once any user exists and setup is complete.
|
||
|
||
| Method | Path | Auth | Description |
|
||
|--------|------|------|-------------|
|
||
| POST | `/api/setup/register` | public (one-time) | Create the first admin account; returns a short-lived setup JWT |
|
||
| POST | `/api/setup` | setup token or admin | Save initial site settings (org name, branding, SMTP) |
|
||
|
||
---
|
||
|
||
## Static Files
|
||
|
||
| Path | Description |
|
||
|------|-------------|
|
||
| `/uploads/events/*` | Event cover images |
|
||
| `/uploads/branding/*` | Site logo |
|
||
| `/uploads/event-files/*` | Event attachments (PDFs, docs, etc.) |
|
||
| `/uploads/tickets-temp/*` | Temporarily hosted ticket PDFs for WhatsApp delivery (auto-deleted after 5 min) |
|
||
|
||
---
|
||
|
||
## Auth error responses
|
||
|
||
| Status | Body | Meaning |
|
||
|--------|------|---------|
|
||
| 401 | `{ message: "Not authorised, no token" }` | Missing or malformed `Authorization` header |
|
||
| 401 | `{ message: "Not authorised, token failed" }` | Invalid or expired JWT |
|
||
| 401 | `{ message: "Session has been revoked" }` | Token version mismatch (sessions revoked) |
|
||
| 403 | `{ message: "Not authorised as an admin" }` | Insufficient role |
|
||
|
||
---
|
||
|
||
*For full interactive docs with expandable examples visit `GET /docs?token=<admin-jwt>` on the running backend.* |