Events can take a waitlist for sold-out lines. When places free up (cancellation, smaller booking, higher stock limit) the next person is offered them with a claim link and the places are held for a set time; unclaimed offers expire and move on, with optional partial offers. Stock is now counted in one place (utils/stock.js), including held places, and manual registrations and booking edits are now stock-checked. Adds a supervisor waitlist page, My Events waitlists, and the daily summary count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
29 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
- Coupons
- Parental consent
- Waitlist
- 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 (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
{
"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:
{
"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 |
Waitlist
Events with waitlistEnabled (plus waitlistOfferHours, default 24, and waitlistPartialOffers, default true) take a queue for sold-out lines.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/waitlist |
optional | Join: { eventId, eventOptionId, variantId?, quantity } (+ guestName, guestEmail, guestPhone when not logged in on a guest event). Only when the line can't cover the quantity |
| GET | /api/waitlist/mine |
user+ | My active entries |
| DELETE | /api/waitlist/:id |
user+ | Leave (an open offer moves on) |
| GET | /api/waitlist/claim/:token |
public | What a claim link offers |
| GET | /api/waitlist/events/:eventId |
staff+ | The queue for an event |
| POST | /api/waitlist/:id/offer |
supervisor+ | Offer now ({ quantity? }), even over the stock limit |
| POST | /api/waitlist/:id/move |
supervisor+ | { direction: "up" | "down" } |
| POST | /api/waitlist/:id/remove |
supervisor+ | Take someone off |
How offers work:
- Places held for a live offer count as taken everywhere stock is counted:
availableCount,isSoldOutand registration stock checks. - Offers are made automatically when places free up: a cancellation, a smaller booking, or a raised stock limit.
- A job every 10 minutes (
WAITLIST_JOB_ENABLED,WAITLIST_JOB_INTERVAL_MS) expires unclaimed offers and re-offers. - Claiming means registering with
POST /api/registrationspluswaitlistToken. The token stands in for logging in, and the held places are released to the claimer. - Manual registration and adding places to an existing booking are now checked against stock too.
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=falseto disable) sends reminders (day 2, then every 3 days) and deletes themconsent_medical_retention_days(default 30) after the event.
Check-in rules:
GET /api/tickets/scan-preview/:qrCodereturnsconsentStatus(not_required,missing,pending,givenordeclined).POST /api/tickets/scan/:qrCodereturns403 { code: "CONSENT_REQUIRED" }unless consent is given.- Supervisors can pass
overrideConsent: true, which is audited asconsent_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) andoptions: [{ eventOptionId, variantId? }](none = the whole registration). - Restrictions:
userIds,restrictRolesandrestrictDomain. A match on any one is enough, and restricted codes need a known account. - Limits:
validFrom,validUntil,maxUses,maxUsesPerUser(default 1) andminQuantity. - Early-bird:
stackEarlyBird: falsemeans the better price wins;truediscounts 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)
{
"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.