Base URL: VITE_API_URL (local default http://localhost:4000). All JSON APIs under /api/.
Protected routes require a valid access token from (in order):
wm_accesshttpOnly cookie (primary)Authorization: Bearer <token>tokenrequest header (legacy)
Send cookies with every SPA request: credentials: "include". Include device-id header for session binding.
Refresh: POST /api/user/refresh with wm_refresh cookie.
Error shape:
{ "success": false, "message": "...", "code": "OPTIONAL_CODE" }Common auth code values: AUTH_REQUIRED, ACCESS_TOKEN_EXPIRED, INVALID_TOKEN, SESSION_EXPIRED, SESSION_REVOKED, REFRESH_EXPIRED, RATE_LIMITED.
SESSION_REVOKED means this device was signed out remotely (Profile → Devices). Cookies are cleared; the client must OAuth again.
Rate-limit responses use HTTP 429 and code: "RATE_LIMITED".
| Method | Path | Auth | Limiter | Handler |
|---|---|---|---|---|
| GET | /auth/google |
— | auth | Start OAuth (?deviceId=) |
| GET | /auth/google/callback |
— | auth | OAuth callback |
| GET | /devices/pending |
pending code | deviceLogin | Device conflict UI data |
| POST | /devices/continue-login |
pending code | deviceLogin | Revoke + complete login |
| POST | /refresh |
refresh cookie | refresh | Rotate session |
| POST | /userdata |
access | — | Current user / onboarding flag |
| POST | /complete-onboarding |
access | — | Set username + seed defaults |
| POST | /logout |
access | — | Clear this device session |
| POST | /devices/revoke |
access | — | Sign out another device |
| POST | /profile |
access | — | Profile + devices + credits |
| GET | /profile/analytics |
access | — | ?range=7d|30d click series |
| PUT | /profile |
access | — | Update name/picture |
{ "code": "pending_code", "revokeDeviceId": "device_id" }{ "deviceId": "device_id" }Cannot revoke the current device; use /logout instead.
{ "name": "Display Name", "picture": "https://..." }| Method | Path | Auth | Limiter | Handler |
|---|---|---|---|---|
| GET | /save |
bookmarklet | — | HTML popup save |
| GET | /categories |
access | — | Categories + embedded bookmarks |
| GET | /categories-with-bookmarks |
access | — | Same as /categories |
| POST | /category |
access | — | Create category |
| PUT | /category |
access | — | Update category |
| PUT | /categories/reorder |
access | — | Reorder categories |
| DELETE | /category |
access | — | Delete category (+ bookmarks) |
| GET | /bookmarks/:categoryId |
access | — | Bookmarks for one category |
| POST | /bookmark |
access | — | Create bookmark |
| PUT | /bookmark |
access | — | Update bookmark |
| DELETE | /bookmark |
access | — | Delete bookmark |
| PUT | /reorder |
access | — | Reorder within one category |
| PUT | /reorder-layout |
access | — | Batch cross-category layout |
| POST | /import |
access | import | HTML import |
| POST | /ai/sort |
access | ai | Bulk AI sort |
| POST | /ai/sort/revert |
access | ai | Revert last AI sort |
{
"category": "Reading",
"bgcolor": "#f7fee7",
"hcolor": "#4d7c0f",
"emoji": "📚"
}{
"categoryId": "...",
"name": "Example",
"link": "https://example.com",
"logo": "https://example.com/favicon.ico"
}Batch payload of categories with ordered bookmark IDs (see reorderBookmarkLayout / client getLayoutUpdates).
{ "mode": "all" }or { "mode": "uncategorized" }.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /track |
access | Increment bookmark + user click stats |
| POST | /stats |
access | User click statistics |
{ "bookmarkId": "..." }| Method | Path | Auth | Limiter | Purpose |
|---|---|---|---|---|
| GET | /public |
— | publicStats | Landing-page aggregates (?range=) |
| GET | /historical/:type |
— | publicStats | Historical series (daily/weekly/monthly) |
Cached in-memory for PUBLIC_STATS_CACHE_TTL_SECONDS (default 300).
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /stats |
Authorization: Bearer ${CRON_SECRET} in production |
Collect public stats |
Scheduled daily via Vercel Cron (0 0 * * *).
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /:id |
— | Public tweet fetch for landing testimonials (1h memory cache) |
| Name | Window | Max | Applied to |
|---|---|---|---|
authRateLimit |
15 min | 30 | Google auth start + callback |
refreshRateLimit |
15 min | 60 | /refresh |
deviceLoginRateLimit |
15 min | 20 | pending + continue-login |
aiRateLimit |
1 hour | 15 | AI sort + revert |
importRateLimit |
1 hour | 6 | Import |
publicStatsRateLimit |
1 min | 60 | Public/historical stats |
Defined in server/middleware/rateLimit.js.