Skip to main content

API endpoints

Status: Scaffolded 2026-09-24

All routes live under /api on https://api.vegangrove.org; health is /healthz. Auth is Authorization: Bearer <session token>. Every error is { error: { code, message } } (error handling). Every list is { items, nextCursor } with an opaque cursor over _id; there is no page or skip, and a bad cursor is a 400 invalid_cursor.

Auth levels: public (no session), member (any session), organizer (grove organizer or organization admin for the host), admin (role: admin, checked in the database per request).

Scaffold status: auth, me, places, admin place approval, healthz, and stats are implemented with tests. Every other route is mounted, validates its input, and returns 501 { error: { code: 'not_implemented' } } until its milestone (milestones).

Auth and account (implemented)​

RouteAuthNote
POST /api/auth/register { email, password, handle }publicreturns { token, user }, rate limited
POST /api/auth/login { email, password }publicrate limited
POST /api/auth/magic-link { email }publicalways 202, no account enumeration
POST /api/auth/magic-link/verify { token }publicsingle use, 15 minute TTL
POST /api/auth/apple { identityToken, nonce }publicemail from the token only, 501 in scaffold
POST /api/auth/google { idToken }public501 in scaffold
POST /api/auth/logoutmemberdeletes the session
GET, PATCH, DELETE /api/mememberdelete is the hard delete from ADR 0010
GET /api/me/sessions, DELETE /api/me/sessions/:idmember
GET /api/me/notification-preferences, PUTmember501
POST, DELETE /api/push-tokensmember501

Health and stats (implemented)​

RouteAuthNote
GET /healthzpublic{ ok: true } after a DB ping, else 503
GET /api/statspublicaggregate counters, cached 5 minutes

Places (implemented)​

RouteAuthNote
GET /api/places?bbox=w,s,e,n&type=&q=&cursor=publicapproved places only
GET /api/places/:slugpublic
POST /api/placesmembercreated as pending
GET, POST /api/places/:id/reviewspublic, member501
GET, POST /api/place-lists, PATCH, DELETE /api/place-lists/:idmemberprivate by default, 501

Events, groves, organizations​

RouteAuthNote
GET /api/events?from=&to=&area=&groveId=&cursor=publicvisibility-filtered by caller
GET /api/events/:slugpublic
POST /api/events, PATCH /api/events/:idorganizer
POST, DELETE /api/events/:id/rsvpmember
GET /api/events/:id/attendeesorganizereveryone else sees counts only
GET /api/groves, GET /api/groves/:slugpublic
POST /api/groves/:id/join, DELETE /api/groves/:id/leavemember
GET /api/organizations, GET /api/organizations/:slugpublicadminUserIds never in the response

Friends, feed, posts​

RouteAuthNote
GET /api/friends, GET /api/friends/requestsmember
POST /api/friends/invitesmember{ code, expiresAt }
POST /api/friends/invites/:code/acceptmember
DELETE /api/friends/:userIdmember
GET /api/feed?scope=friends|grove:<id>|public&cursor=member
POST /api/posts, GET, DELETE /api/posts/:idmembera post the caller may not see is 404
POST, DELETE /api/posts/:id/reactionsmember
GET, POST /api/posts/:id/commentsmember
POST, DELETE /api/posts/:id/savemember
GET /api/handles/:handle/postspublicpublic posts only, per ADR 0004
POST /api/reportsmember

Uploads, messages, media, guides, actions, companion​

RouteAuthNote
POST /api/uploads/image/presign { contentType, purpose }member{ url, key }, S3 PUT
POST /api/uploads/video/create { title }member{ videoId, tusEndpoint, authorization }, Bunny
GET, POST /api/conversationsmember
GET, POST /api/conversations/:id/messagesmemberencrypted at rest, 90 day TTL
Socket.IO /messagesmemberrooms user:<id>, conversation:<id>
GET /api/media?kind=&tag=&cursor=, GET /api/media/:slugpublic
GET /api/guides?category=, GET /api/guides/:slugpublic
GET, POST /api/actions, DELETE /api/actions/:idmemberprivate log
POST /api/companion/chat { conversationId?, message }memberSSE stream, rate limited
GET /api/companion/conversations, POST .../:id/pin, DELETE .../:idmember

Admin​

RouteAuthNote
GET /api/admin/places/pending, PUT /api/admin/places/:id/approve, .../rejectadminimplemented
CRUD /api/admin/media, CRUD /api/admin/guidesadmin501
GET /api/admin/reports, PUT /api/admin/reports/:idadmin501