API
All documented routes come from server/src/routes/api.ts.
Base paths
| Base path | Purpose |
|---|---|
/api | JSON API |
/thumbnails | Static thumbnail derivatives |
/previews | Static preview derivatives |
Authentication and protected routes
Foldergram can optionally require a shared-password session.
When password protection is enabled:
- most
/apiroutes return401until the browser logs in GET /api/health,GET /api/auth/status,POST /api/auth/login, andPOST /api/auth/logoutstay reachable without an authenticated sessionPUT /api/auth/passwordis public only when password protection is currently disabled, so the first password can be set/thumbnails/...and/previews/...also require the same authenticated session
The frontend sends same-origin credentials automatically and uses a signed cookie-based session.
Mutation requirements
All mutating API routes are protected by requireTrustedMutationRequest.
Required header
x-foldergram-intent: 1Local-origin checks
For mutating requests:
- if
Originis present, it must belocalhost,127.0.0.1, or::1 - in development and test, the origin port must match
DEV_SERVER_PORTor the reserved dev-client range fromDEV_CLIENT_PORTthroughDEV_CLIENT_PORT + 3 - in production, loopback origins are allowed and same-host origins are allowed when they match the host serving the app on
SERVER_PORT - if
Originis absent butRefereris present, the same loopback check is applied to the referer origin
The shipped frontend adds x-foldergram-intent: 1 automatically for POST, PUT, PATCH, and DELETE requests.
Common error behavior
| Status | When it happens |
|---|---|
401 | Password protection is enabled and the request is not authenticated. |
400 | Validation or request-shape errors surfaced through the Express error handler. |
403 | Missing intent header or failed local-origin check on a mutating route. |
404 | Missing folder, post, moment, or original media. |
409 | A scan or thumbnail rebuild was requested while the library requires a full rebuild after a gallery-root change. |
Read endpoints
GET /api/health
Returns process-level health plus storage state.
Example shape:
{
"ok": true,
"timestamp": "2026-03-16T12:34:56.000Z",
"storage": {
"available": true,
"reason": null,
"usingInMemoryDatabase": false
}
}GET /api/auth/status
Returns the current auth state for the browser session.
Example shape:
{
"enabled": true,
"authenticated": false
}GET /api/feed
Query parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
page | integer | 1 | Minimum 1. |
limit | integer | 24 | Minimum 1, maximum 60. |
mode | `recent | rediscover | random` |
seed | integer | unset | Optional random seed for random mode. |
Response shape:
{
"mode": "recent",
"items": [],
"page": 1,
"limit": 24,
"total": 0,
"hasMore": false
}Notes:
itemscontain both images and videos.thumbnailUrlpoints to/thumbnails/....previewUrlpoints to/previews/...unless a video is served directly from its original file.
GET /api/feed/moments
Returns the current home rail definition.
The rail can be:
momentshighlights
Response shape:
{
"railKind": "moments",
"railTitle": "Moments",
"railDescription": "Memory capsules shaped by real capture dates from your library.",
"railSingularLabel": "Moment",
"items": []
}GET /api/feed/moments/:id
Path parameters:
| Param | Type |
|---|---|
id | string |
Query parameters:
| Param | Type | Default |
|---|---|---|
page | integer | 1 |
limit | integer | 24 |
Returns 404 with {"message":"Feed capsule not found"} when the ID does not exist in the currently selected rail.
GET /api/folders
Returns:
{
"items": []
}Each item is a folder summary with:
idslugnamefolderPathbreadcrumbimageCountvideoCountlatestImageMtimeMsavatarUrl
GET /api/folders/:slug
Returns one folder summary.
Errors:
404with{"message":"Folder not found"}if the slug is missing
GET /api/folders/:slug/images
Query parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
page | integer | 1 | Minimum 1. |
limit | integer | 24 | Minimum 1, maximum 60. |
mediaType | `image | video` | unset |
Response shape:
{
"folder": {
"id": 1,
"slug": "oslo",
"name": "oslo",
"folderPath": "trips/oslo",
"breadcrumb": "trips",
"imageCount": 12,
"videoCount": 3,
"latestImageMtimeMs": 1700000000000,
"avatarUrl": "/thumbnails/trips/oslo/IMG_0001.webp?v=4"
},
"items": [],
"page": 1,
"limit": 24,
"total": 12,
"hasMore": false
}Errors:
404with{"message":"Folder not found"}
GET /api/likes
Returns:
{
"items": []
}Items are ordered by like timestamp descending.
GET /api/images/:id
Query parameters:
| Param | Type | Notes |
|---|---|---|
mediaType | `image | video` |
Returns one post detail payload with:
- feed-item fields
relativePathmimeTypefileSizeoriginalUrlnextImageIdpreviousImageId
nextImageId and previousImageId are resolved within the same folder and the same active mediaType filter when one is supplied.
Errors:
404with{"message":"Post not found"}
GET /api/originals/:id
Serves the original file from disk by image ID only.
Rules:
- the indexed path must still exist
- the resolved path must stay within
GALLERY_ROOT - deleted posts do not resolve
Errors:
404with{"message":"Original media not found"}
GET /api/admin/stats
Returns aggregated operational state.
Notable fields:
| Field | Notes |
|---|---|
folders | Active indexed folders. |
indexedImages | Active indexed posts. The name is historical and still includes videos in the total feed count. |
indexedVideos | Active indexed videos only. |
deletedImages | Soft-deleted post count. |
thumbnailCount | Active posts with a thumbnail path. |
previewCount | Active items with a preview output. Videos served directly from originals are excluded here. |
scan | Live scan progress snapshot. |
storage | Availability and in-memory-database state. |
libraryIndex | Current and previous gallery-root state, plus rebuild requirement. |
lastScan | Last completed scan run. |
Mutating endpoints
POST /api/auth/login
Body:
{
"password": "your-shared-password"
}Success:
{
"ok": true,
"auth": {
"enabled": true,
"authenticated": true
}
}Errors:
400if password protection is not enabled401if the password is incorrect403when trust requirements are missing
POST /api/auth/logout
Clears the current browser session cookie.
Success:
{
"ok": true,
"auth": {
"enabled": true,
"authenticated": false
}
}PUT /api/auth/password
Sets or changes the shared password.
Body when protection is disabled:
{
"password": "new-password"
}Body when protection is already enabled:
{
"currentPassword": "old-password",
"password": "new-password"
}Notes:
- password minimum length is
8 - changing the password invalidates older sessions
DELETE /api/auth/password
Disables shared-password protection.
Body:
{
"currentPassword": "current-password"
}Errors:
400if protection is already disabled401if the password is incorrect
POST /api/images/:id/like
Marks a post as liked.
Success:
{
"ok": true,
"id": 42,
"liked": true
}Errors:
404with{"message":"Image not found"}when the post does not exist or is deleted403when trust requirements are missing
DELETE /api/images/:id/like
Removes a like.
Success:
{
"ok": true,
"id": 42,
"liked": false
}DELETE /api/images/:id
Permanently deletes:
- the source file from disk
- its thumbnail derivative
- its preview derivative
Then it updates the index and folder avatar state.
Success:
{
"ok": true,
"id": 42,
"folderSlug": "oslo"
}Errors:
404with{"message":"Image not found"}403when trust requirements are missing
DELETE /api/folders/:slug
Query parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
deleteSourceFolder | boolean | false | Accepts true or false. |
When deleteSourceFolder=false:
- direct posts in that folder are permanently deleted
- child folders below that path are kept
- Foldergram tries to remove now-empty directories
When deleteSourceFolder=true:
- the source folder subtree is removed from disk
- matching derivative subtrees are removed
- all indexed child folders under that subtree are removed
Success shape:
{
"ok": true,
"slug": "oslo",
"deletedImageCount": 12,
"deletedFolderCount": 1,
"deletedSourceFolder": false
}Errors:
404with{"message":"Folder not found"}403when trust requirements are missing
POST /api/admin/rescan
Runs a manual scan against the current library and then starts the development watcher.
Success:
{
"ok": true,
"lastScan": {
"id": 7,
"status": "completed",
"scanned_files": 120
}
}Errors:
409with the library-rebuild-required message when the gallery root changed500for scan failures surfaced from the scanner
POST /api/admin/rebuild-index
Stops the watcher, clears the indexed library tables, rescans the current gallery root, and then restarts the watcher.
This resets:
likesimagesfoldersfolder_scan_statescan_runs
POST /api/admin/rebuild-thumbnails
Stops the watcher, clears the thumbnail cache, regenerates thumbnails and video poster images from indexed media, and restarts the watcher.
It does not reset:
- previews
- likes
- folder records
- scan history
Errors:
409with the library-rebuild-required message when a full rebuild is required
Client helpers
The frontend wraps these endpoints in client/src/api/gallery.ts.
Those helpers are the best reference for current client-side usage, including:
- default page sizes
- when
mediaTypeis sent - which routes are expected to return
itemsarrays versus single objects