Admins and supervisors without 2FA are sent a code at their next login, and verifying it turns 2FA on. They can change the channel but not turn it off. Older sessions without 2FA are rejected with TWO_FACTOR_REQUIRED, and promoting a user ends their sessions so they enrol at next login. The requirement is skipped when no email or WhatsApp channel can deliver a code, and TWO_FACTOR_REQUIRED_ROLES=false switches it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
22 KiB
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
- Events
- Event Options & Variants
- Event Attachments
- Registrations
- Payments
- Tickets
- Broadcasts (Email)
- WhatsApp Broadcasts
- Scheduled Messages
- Automations
- Reports
- Uploads
- Sections
- Banner
- Forms
- Yoco Transactions
- WhatsApp Admin
- Webhooks
- Settings
- Setup
- 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
{
"name": "Jane Smith",
"email": "jane@example.com",
"password": "securepass123",
"phoneNumber": "+27821234567"
}
Returns: { _id, name, email, role, token }
POST /api/users/login
{ "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:
{ "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
{ "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 withenable/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
{ "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
{
"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
{
"name": "Adult",
"price": 450,
"isMainTicket": false,
"stockLimit": 100
}
PUT /api/events/options/:id — with variants and early-bird tiers
{
"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 |
POST /api/registrations
{
"eventId": "ev-uuid-...",
"options": [
{ "eventOptionId": "opt-uuid-...", "quantity": 2 },
{ "eventOptionId": "opt-uuid-...", "quantity": 1, "variantId": "variant-uuid-..." }
]
}
For unauthenticated (guest) registration on events with requiresAuth: false:
{
"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 |
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)
{
"registrationId": "reg-uuid-...",
"amount": 450,
"method": "cash",
"note": "Paid at the door"
}
POST /api/payments/yoco-checkout
{
"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
{
"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
{
"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
{
"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 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
{
"host": "smtp.office365.com",
"port": 587,
"secure": false,
"user": "noreply@yourchurch.org",
"pass": "app-password-here",
"from": "noreply@yourchurch.org"
}
Success (200):
{ "message": "SMTP connection verified — a test email has been sent to admin@yourchurch.org" }
Failure (400):
{
"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.