HTTP API
The routes the interface uses. They're stable within a major version, but they exist for the UI, not as a public integration API.
All mutating requests need a same-origin Origin header (browsers send it). Errors are JSON: { "error": "<code>", "details"?: { field: message } }.
| Code | Status | Meaning |
|---|---|---|
bad_request | 400 | Invalid input, see details |
unauthorized | 401 | Not signed in as admin, or wrong password |
invalid_code | 401 | Wrong event access code |
forbidden | 403 | Cross-site request, or the guest hasn't joined the event |
uploads_closed | 403 | Uploads are off or past their closing time |
quota_reached | 403 | The guest reached the per-guest limit |
not_found | 404 | Doesn't exist, or exists but isn't yours |
too_large | 413 | Body above MAX_UPLOAD_MB |
invalid_image | 415 | Not a supported image |
rate_limited | 429 | Slow down; see Retry-After |
Guest routes
Authenticated by the gr_guest cookie.
| Route | Body | Response |
|---|---|---|
POST /api/e/:slug/join | none | { ok }. Events without an access code only. |
POST /api/e/:slug/unlock | { code } | { ok } |
POST /api/e/:slug/guest | { name } | { name }. Stored in the cookie. |
POST /api/e/:slug/booth | { key } | { ok }: sets this device up as the event's booth; 403 for a wrong or replaced key |
POST /api/e/:slug/photos?at=<spot id> | multipart, field photo, optional challenge (a challenge id) | 201 { photo: PhotoDto }; an unknown challenge, or a place of another event, is ignored |
DELETE /api/e/:slug/photos/:id | { ok }: the guest's own photo, files included; 404 otherwise | |
GET /api/e/:slug/photos?before=<ms>&scope=all | { photos: PhotoDto[], nextCursor }: the guest's own photos; with scope=all and a shared album, the others' approved ones too | |
GET /api/e/:slug/logo?v=<n> | The event logo (PNG), public with the slug | |
GET /api/media/:id?v=thumb|display|original[&download=1] | The image, if the viewer may see it |
| POST /api/e/:slug/contact | { email } | { email }. Only on events that collect emails. | | DELETE /api/e/:slug/contact | | { ok }, the address is deleted |
| GET /api/e/:slug/messages | | { messages: MessageDto[] }, the guest's own | | POST /api/e/:slug/messages | multipart: text and/or audio, seconds | 201 { message } | | DELETE /api/e/:slug/messages/:id | | { ok }, the guest's own only | | GET /api/messages/:id/audio[?download=1] | | The voice note, if the viewer may hear it |
Gallery link routes
Authenticated by the gr_share cookie, set by unlock.
| Route | Body | Response |
|---|---|---|
POST /api/g/:token/unlock | { password } | { ok }, or invalid_code |
GET /api/g/:token/photos?challenge=&before=<ms> | { photos, nextCursor }, visible photos only; 403 until unlocked | |
GET /api/g/:token/messages?before= | { messages, nextCursor }, when the hosts share the guestbook | |
GET /api/g/:token/archive | Streamed ZIP, if the link allows downloads |
interface PhotoDto {
id: string;
width: number;
height: number;
guestName: string | null;
status: "visible" | "pending" | "hidden";
mine: boolean;
downloads?: number; // admins only
favorite?: boolean; // admins only
challengeId: string | null;
createdAt: number; // epoch ms
}Admin routes
Authenticated by the gr_admin cookie. Routes under /api/admin/events/:id answer 404 for events outside an organizer's scope; event creation and deletion, accounts and email settings are for owners (403 otherwise).
| Route | Body | Response |
|---|---|---|
POST /api/admin/login | { username, password } | { ok }. Without username, the first owner (ADMIN_USERNAME). |
POST /api/admin/login/code | { code } (authenticator or recovery code) | { ok }; required when login answered { codeRequired: true } |
GET /api/admin/me/two-factor | { enabled, recoveryCodesLeft } | |
POST /api/admin/me/two-factor | { action: "start" }, { action: "confirm", code }, { action: "recovery", code } | { secret, uri, qr } / { enabled, recoveryCodes } / { recoveryCodes } |
DELETE /api/admin/me/two-factor | { password, code } | { enabled: false } |
POST /api/admin/logout | { ok } | |
POST /api/admin/password | { current, next } | { ok }, for the signed-in account |
GET /api/admin/me / PATCH | { name?, email? } | { account } |
GET /api/admin/accounts | Owners: { accounts } | |
POST /api/admin/accounts | { username, role, name?, email?, eventIds?, password? } | 201 { account, password }; password is the generated one, else null |
PATCH /api/admin/accounts/:id | same fields, or { resetPassword: true }, { resetTwoFactor: true } | { account, password } |
DELETE /api/admin/accounts/:id | { ok } | |
GET /api/admin/events | optional ?page, perPage (1–100, default 20), q (name contains) | without page or q: { events: EventSummary[] }, every event; with them: { events, total, page, pageCount } |
POST /api/admin/events | event fields, name required | 201 { event } |
GET /api/admin/events/:id | { event } | |
PATCH /api/admin/events/:id | any event fields | { event } |
DELETE /api/admin/events/:id | { ok }, photos deleted from disk | |
POST /api/admin/events/:id/rotate | { event } with a new slug | |
POST /api/admin/events/:id/logo | multipart, field logo | { event } |
DELETE /api/admin/events/:id/logo | { event } | |
GET /api/admin/events/:id/qr?format=svg|png&size=512..4096&download=1&spot=<id> | The styled QR code; with spot, that place's code (/e/<slug>?at=<id>) captioned with its name | |
GET /api/admin/invitations | { invitations }: pending ones (owners) | |
POST /api/admin/invitations | { email, role, eventIds } | 201 { invitation, url, emailed } |
DELETE /api/admin/invitations/:id | { ok } | |
POST /api/admin/invite/:token | { username, name, password } (public, the token is the credential) | 201 { ok } and signed in; 404 for a spent, expired or revoked link |
GET /api/admin/events/:id/album?layout=1|2|4&scope=visible|favorites | The printable album, a streamed PDF; 404 without photos, 429 while another album is being made | |
GET /api/metrics | Authorization: Bearer <METRICS_TOKEN> | Prometheus text format; 404 without the token |
GET /api/admin/events/:id/cloud | { configured, exports }: the latest cloud exports with their progress | |
POST /api/admin/events/:id/cloud | { scope: "visible" | "favorites" } | 202 { export }: queued, sent in the background |
GET/PUT/DELETE /api/admin/cloud | { url, user, password, folder, autoExport } | The WebDAV settings (owners; the password is never returned) |
POST /api/admin/cloud/test | same body | { ok, message? } |
POST /api/admin/events/:id/booth | { boothKey }: turns the photo booth on, or replaces its link | |
DELETE /api/admin/events/:id/booth | { ok }: turns it off | |
GET /api/admin/events/:id/spots | { spots: [{ id, label, photos }] } | |
POST /api/admin/events/:id/spots | { label } (1 to 40 characters, up to 100 places) | 201 { spot } |
PATCH /api/admin/events/:id/spots/:spotId | { label } | { spot } |
DELETE /api/admin/events/:id/spots/:spotId | { ok }; its photos stay, untagged | |
GET /api/admin/events/:id/photos?status=&favorites=1&challenge=&before= | { photos, nextCursor } | |
POST /api/admin/events/:id/photos | { action: "approve"|"hide"|"favorite"|"unfavorite"|"delete", ids } | { changed } |
POST /api/admin/events/:id/photos/import | multipart: photo, optional credit | 201 { photo }, visible at once |
POST /api/admin/events/:id/duplicate | { name? } | Owners: 201 { event }, settings without photos |
GET /api/admin/events/:id/archive[?all=1|?favorites=1] | Streamed ZIP; all=1 includes pending and hidden, favorites=1 is the visible favorites | |
GET /api/admin/webhooks | Owners: { webhooks, deliveries } | |
POST /api/admin/webhooks | { name, type: "ntfy"|"discord"|"slack"|"json", url, kinds, secret?, enabled? } | 201 { webhook } |
PATCH /api/admin/webhooks/:id / DELETE | same fields; empty url keeps it | { webhook } / { ok } |
POST /api/admin/webhooks/:id/test | { ok }, or 502 { error: "webhook_failed", message } | |
GET /api/admin/backup[?download=1] | Owners: { files, bytes, storage, retentionDays }, or the backup ZIP | |
POST /api/admin/backup/restore | the ZIP as the body | { ok, manifest }; 400 { details: { file } } if refused |
PUT /api/admin/backup/retention | { days }: 0, or 14 to 3650 | { retentionDays } |
GET /api/admin/updates | Owners: { current, latest, available, checkEnabled, history, updater } | |
POST /api/admin/updates/check | { available }, or 502 when GitHub can't be reached | |
POST /api/admin/updates/apply | { version, skipBackup? } (the available version only) | 202 { job, backup }; 409/502 if the backup or the updater fails |
GET /api/admin/stats[?eventId=] | Totals, time series, top guests | |
GET /api/admin/events/:id/messages?status=&before= | { messages, nextCursor } | |
POST /api/admin/events/:id/messages | { action: "approve"|"hide"|"delete", ids } | { changed } |
GET /api/admin/events/:id/messages/archive | ZIP: guestbook.txt and the voice notes | |
GET /api/admin/events/:id/shares | { shares } | |
POST /api/admin/events/:id/shares | { password?, label?, allowDownload?, favoritesOnly?, expiresInDays? } | 201 { share, password }; password is the generated one, else null |
DELETE /api/admin/events/:id/shares/:shareId | { ok }, revoked | |
POST /api/admin/events/:id/end | { event, result }; 400 if already ended | |
GET /api/admin/mail | SMTP settings (never the password), alerts, queue | |
PUT /api/admin/mail | { smtp?, adminEmail?, alerts? } | Same as GET |
POST /api/admin/mail/test | { to? } | { ok, to }, or 502 { error: "smtp_failed", message } |
POST /api/admin/mail/retry | { requeued, sent, failed }: failed emails requeued, then the queue is processed |
Event fields: name, description, eventDate (YYYY-MM-DD), accentColor (#rrggbb), accessCode (string to set, null to remove), uploadsOpen, uploadsCloseAt (epoch ms or null), moderation (post|pre), captureMode (camera|picker|both), askGuestName, maxPhotosPerGuest, defaultLocale (en|fr|it|es|null), collectGuestEmails, notifyAdminSummary, emailGuestsGallery, galleryLinkDays (0–3650), galleryLinkDownload, galleryMailIncludesPassword, galleryLinkFavorites, guestbookEnabled, guestbookVoice, guestbookInGallery, challenges (list of { id?, label } or strings; ids are kept, new ones assigned), wallStyle (object, see src/lib/wall.ts), frame (none|print|border|stamp), captureFilters, qrStyle (object, see src/lib/qr/style.ts).
Health
GET /api/health returns 200 {"status":"ok"}, or 503 when the database can't be read.