API and auth

Public HTTP surfaces

NextUp’s HTTP API is intentionally thin. These are the routes under /api that exist in the app today.

Outbox drain

POST or GET /api/cron/outbox

AuthHeader x-cron-secret must match CRON_SECRET
EffectProcesses up to 50 pending notification outbox rows; also runs trial expiry
ResponseJSON { processed, sent, failed, trials: { businesses, memberships } }

Call this from your scheduler (platform cron is not bundled in-repo). Unauthorized requests return 401.

Trial expiry

POST or GET /api/cron/trials

AuthHeader x-cron-secret must match CRON_SECRET
EffectMarks businesses with past trial_ends_at as trial_expired, suspends their memberships, purges expired trial OTP rows
ResponseJSON { businesses, memberships, otpsPurged }

Console access is also blocked on load via enforceTrialAccess when a trial has ended.

Twilio SMS status

POST /api/webhooks/twilio

AuthNone in current code (no signature validation)
Bodyapplication/x-www-form-urlencoded Twilio fields: MessageSid, MessageStatus, optional ErrorMessage
EffectUpdates matching outbox row by providerMessageId
ResponsePlain text ok

Map: delivered / sent → sent; failed / undelivered → failed.

Site realtime snapshot

GET /api/realtime/site/{siteId}

AuthNone (anyone with the site UUID can read)
?once=1Single JSON snapshot
Omit querySSE text/event-stream; snapshot every ~2s; connection closes after ~25s (reconnect)

Snapshot fields include active queues, now serving, waiting list, waiting counts by queue, today’s missed (no-show) tickets, and updatedAt.

TV displays use this endpoint (SSE plus a short poll fallback).

Analytics CSV

GET /api/export/analytics?businessId=…&days=7

AuthSession cookie; caller must be a manager for that business
ResponseCSV attachment (metric,value rows)

Org upload asset

GET /api/uploads/orgs/{key}

AuthPublic, cacheable
KeyPattern like org-…-….jpg / .png / .webp / .gif
CacheLong Cache-Control with stale-while-revalidate

CDN and cache behavior (Netlify)

SurfaceCache
Marketing /, /pricing, /docs/*, /privacy, /termsAnonymous HTML: public, s-maxage=3600, stale-while-revalidate=86400 when no session cookie
/_app/immutable/*Immutable assets (1 year)
/api/realtime/site/*no-cache (live queue state)
Authenticated /b/*, /auth/*private, no-store + Vary: Cookie
RBAC console pathsRedis cache-aside (~90s) with version bump on staff/group/bootstrap mutations

Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in production for cross-instance cache, rate limits, ticket queue locks, and cron idempotency. Without Redis, the app falls back to in-process memory (single instance only).

Auth HTTP

better-auth also mounts its usual /api/auth/* handlers for the product session. Integrators should use the documented login UI, not treat that surface as a QMS API.

enmyja