# Hope Events — API Documentation Full interactive docs (with request/response examples) are available at `GET /docs?token=` on the running backend. All authenticated requests require: ``` Authorization: Bearer ``` 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) - [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 | | 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 | | 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) | | 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" } ``` Returns: `{ _id, name, email, role, token }` ### 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: ""`). --- ## 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 ```json { "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`: ```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 | --- ## 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. | 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=` on the running backend.*