Skip to content

API

All documented routes come from server/src/routes/api.ts.

Base paths

Base pathPurpose
/apiJSON API
/thumbnailsProtected thumbnail derivatives, served from cache or generated on demand in lazy mode
/previewsProtected preview derivatives, served from cache or generated on demand in lazy mode

Authentication and protected routes

Foldergram can optionally require a role-bearing password session.

When password protection is enabled:

  • most /api routes return 401 until the browser logs in, except for safe read routes in public viewer mode
  • GET /api/health, GET /api/auth/status, POST /api/auth/login, POST /api/auth/unlock-admin, and POST /api/auth/logout stay reachable without an authenticated session
  • PUT /api/auth/password is public only when password protection is currently disabled, so the first admin password can be set
  • /thumbnails/... and /previews/... also require the same authenticated session unless public viewer mode is enabled
  • authenticated sessions carry admin or viewer role data
  • anonymous public reads use role: "anonymous" and likesMode: "local"
  • admin-only mutations return 403 for viewer sessions

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

http
x-foldergram-intent: 1

Local-origin checks

For mutating requests:

  • if Origin is present, it must be localhost, 127.0.0.1, or ::1
  • in development and test, the origin port must match DEV_SERVER_PORT or the reserved dev-client range from DEV_CLIENT_PORT through DEV_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 Origin is absent but Referer is 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

StatusWhen it happens
401Password protection is enabled and the request is not authenticated.
400Validation or request-shape errors surfaced through the Express error handler.
403Missing intent header, failed local-origin check, or a viewer session hitting an admin-only route.
404Missing folder, post, moment, or original media.
409A scan or thumbnail rebuild was requested while the library requires a library-index rebuild after a gallery-root change.

Read endpoints

GET /api/health

Returns process-level health plus storage state.

Example shape:

json
{
  "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:

json
{
  "enabled": true,
  "authenticated": false,
  "role": "anonymous",
  "accessMode": "public",
  "likesMode": "local",
  "capabilities": {
    "canManageLibrary": false,
    "canDeleteMedia": false,
    "canAccessSettings": false,
    "canUseSharedLikes": false,
    "canUseLocalFavorites": true
  }
}

GET /api/feed

Query parameters:

ParamTypeDefaultNotes
pageinteger1Minimum 1.
limitinteger24Minimum 1, maximum 60.
mode`recentrediscoverrandom`
seedintegerunsetOptional random seed for random mode.

Response shape:

json
{
  "mode": "recent",
  "items": [],
  "page": 1,
  "limit": 24,
  "total": 0,
  "hasMore": false
}

Notes:

  • items contain both images and videos.
  • thumbnailUrl points to /thumbnails/....
  • previewUrl points to /previews/....

GET /api/reels

Query parameters:

ParamTypeDefaultNotes
pageinteger1Minimum 1.
limitinteger24Minimum 1, maximum 60. The current client helper requests 6 per page.
mode`recommendedrecentrandom`
seedintegerunsetOptional seed used to keep recommended and random queues stable while paging.
lastFolderstringunsetOptional recent-navigation hint used by recommended.
recentFolderscomma-separated stringunsetOptional recent-navigation hints used by recommended.

Response shape:

json
{
  "mode": "recommended",
  "items": [],
  "page": 1,
  "limit": 6,
  "total": 0,
  "hasMore": false
}

Notes:

  • items contain videos only.
  • recommended uses lastFolder and recentFolders to bias the queue toward folders you recently opened.
  • recent ignores recommendation hints and returns newest indexed videos first.
  • random and recommended stay stable across pages when the same seed is reused.

GET /api/feed/moments

Returns the current Home Moments or Highlights definition.

The response can be:

  • moments
  • highlights

Response shape:

json
{
  "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:

ParamType
idstring

Query parameters:

ParamTypeDefault
pageinteger1
limitinteger24

Returns 404 with {"message":"Feed capsule not found"} when the ID does not exist in the currently selected set.

GET /api/folders

Returns:

json
{
  "items": []
}

Each item is a folder summary with:

  • id
  • slug
  • name
  • description
  • folderPath
  • breadcrumb
  • imageCount
  • videoCount
  • latestImageMtimeMs
  • hasAvatarStory
  • avatarImageId
  • avatarUrl

GET /api/folders/:slug

Returns one folder summary.

The response uses the same shape as folder items from GET /api/folders, including hasAvatarStory so the client can decide whether the folder avatar should open a story entry point.

Errors:

  • 404 with {"message":"Folder not found"} if the slug is missing

GET /api/folders/:slug/images

Query parameters:

ParamTypeDefaultNotes
pageinteger1Minimum 1.
limitinteger24Minimum 1, maximum 60.
mediaType`imagevideo`unset

Response shape:

json
{
  "folder": {
    "id": 1,
    "slug": "oslo",
    "name": "oslo",
    "description": null,
    "folderPath": "trips/oslo",
    "breadcrumb": "trips",
    "imageCount": 12,
    "videoCount": 3,
    "latestImageMtimeMs": 1700000000000,
    "hasAvatarStory": false,
    "avatarImageId": 42,
    "avatarUrl": "/thumbnails/ab/abcd1234ef567890abcd1234ef567890.webp?v=4"
  },
  "items": [],
  "page": 1,
  "limit": 24,
  "total": 12,
  "hasMore": false
}

Errors:

  • 404 with {"message":"Folder not found"}

GET /api/folders/:slug/stories

Returns the stories payload for one folder.

Response shape:

json
{
  "railKind": "stories",
  "railTitle": "Stories",
  "railDescription": "Stories and highlights for AnimalPlanet.",
  "railSingularLabel": "Story",
  "hasAvatarStory": true,
  "avatarStoryId": "animalplanet-stories",
  "items": [],
  "highlights": []
}

Notes:

  • items contains the avatar story first when one exists, followed by highlight capsules.
  • highlights contains only highlight capsules.
  • each capsule item includes id, title, subtitle, dateContext, imageCount, coverImage, and presentation
  • presentation is avatar or highlight
  • avatarStoryId can be a synthetic fallback id when the folder has highlight media but no direct avatar-story files

Errors:

  • 404 with {"message":"Folder not found"}

GET /api/folders/:slug/stories/:id

Path parameters:

ParamType
slugstring
idstring

Query parameters:

ParamTypeDefault
pageinteger1
limitinteger24

Response shape:

json
{
  "railKind": "stories",
  "railTitle": "Stories",
  "railDescription": "Stories and highlights for AnimalPlanet.",
  "railSingularLabel": "Story",
  "story": {
    "id": "animalplanet-stories",
    "title": "AnimalPlanet",
    "subtitle": "AnimalPlanet story set",
    "dateContext": "Today",
    "imageCount": 8,
    "presentation": "avatar",
    "coverImage": {}
  },
  "items": [],
  "page": 1,
  "limit": 24,
  "total": 8,
  "hasMore": false
}

Notes:

  • the paginated items array contains normal feed-item payloads
  • if the requested avatar story is a fallback capsule, the server returns recent highlight media for that synthetic capsule

Errors:

  • 404 with {"message":"Story capsule not found"}

GET /api/likes

Returns:

json
{
  "items": []
}

Items are ordered by like timestamp descending.

Notes:

  • shared likes are available to admin and viewer sessions
  • anonymous public sessions use browser-local favorites instead of this endpoint

GET /api/images/:id

Query parameters:

ParamTypeNotes
mediaType`imagevideo`

Returns one post detail payload with:

  • feed-item fields
  • relativePath
  • mimeType
  • fileSize
  • originalUrl
  • playbackStrategy for videos when an original MP4 is browser-compatible
  • nextImageId
  • previousImageId

nextImageId and previousImageId are resolved within the same folder and the same active mediaType filter when one is supplied.

Detail media rules:

  • if IMAGE_DETAIL_SOURCE=original and the item is an image, previewUrl points to /api/originals/:id
  • videos keep previewUrl on /previews/...
  • compatible original MP4 videos can set playbackStrategy: "original" so the client can expose an optional original-quality switch

Errors:

  • 404 with {"message":"Post not found"}

GET /api/originals/:id

Serves the original file from disk by image ID only.

Query parameters:

ParamTypeDefaultNotes
download1unsetWhen set to 1, the server responds with an attachment using the indexed original filename.

Rules:

  • the indexed path must still exist
  • the resolved path must stay within GALLERY_ROOT
  • deleted posts do not resolve
  • without download=1, the route streams the original inline/open behavior
  • with download=1, the route serves the same file as a download attachment

Errors:

  • 404 with {"message":"Original media not found"}

GET /api/status

Returns viewer-safe operational state for the shell.

When access protection is enabled:

  • admin and viewer sessions can always read it
  • anonymous visitors can also read it when viewer_access_mode=public

Notable fields:

FieldNotes
foldersActive indexed folders.
indexedImagesActive indexed posts. The name is historical and still includes videos in the total feed count.
indexedVideosActive indexed videos only.
scanViewer-safe live scan progress snapshot. It uses the same shape as GET /api/scan-progress, with currentFolder and currentFile redacted and lastCompletedScan.error_text forced to null.
storageAvailability with a generic unavailable message only.
libraryIndexRebuild requirement plus ignored root-media count. Gallery-root paths are omitted.
preferences.defaultHomeFeedModeCurrent app-wide default home feed mode.
preferences.defaultReelsFeedModeCurrent app-wide default reels mode used when /reels opens.
preferences.treatStoriesAsFoldersWhether folders literally named stories are treated as ordinary app folders instead of reserved story sources.
storiesMigrationMigration hint with hasLegacyStoriesCandidates and decisionPending.

GET /api/scan-progress

Returns the standalone viewer-safe scan payload used for lightweight polling.

Access rules match GET /api/status:

  • admin and viewer sessions can read it when access protection is enabled
  • anonymous visitors can also read it when viewer_access_mode=public

Notable fields:

FieldNotes
isScanningWhether a scan or rebuild is currently running.
phaseidle, migration, discovery, or derivatives.
scanReasonCurrent trigger such as startup, manual rescan, index rebuild, or thumbnail rebuild.
currentPhaseMessageShort phase summary for the UI.
currentOperationCurrent action label such as derivative repair, derivative move, metadata discovery, or derivative generation.
migrationTotalRows / processedMigrationRowsProgress through legacy derivative migration checks.
migratedDerivativeFiles / repairedDerivativeFiles / missingDerivativeFiles / backfilledAssetKeysMigration counters for moved, repaired, missing, and backfilled assets.
discoveredFolders / processedFoldersFolder discovery counters.
discoveredImages / processedImagesIndexed-post discovery counters.
queuedDerivativeJobs / processedDerivativeJobsDerivative job counters once the queue is known.
generatedThumbnails / generatedPreviewsCompleted derivative outputs in the current run.
currentFolder / currentFileAlways null in this viewer-safe route.
lastCompletedScanLatest completed scan summary with error_text forced to null.

Notes:

  • discovery remains open-ended while folders and media are still being found, so clients should treat that phase as indeterminate even though the counters continue to advance
  • migration and derivative phases expose determinate totals once the work queue is known

GET /api/admin/scan-progress

Returns the admin-detailed live scan payload.

This route is read-only but admin-only.

It uses the same overall shape as GET /api/scan-progress, with the following differences:

  • currentFile and currentFolder are populated when the active operation can identify a source path
  • lastCompletedScan is the full scan record, including error_text

GET /api/admin/stats

Returns the full admin operational payload.

This route is read-only but admin-only. It extends GET /api/status with the additional fields below and uses the same admin-detailed scan payload as GET /api/admin/scan-progress:

FieldNotes
deletedImagesSoft-deleted post count.
thumbnailCountActual generated thumbnail files currently present on disk.
previewCountActual generated preview files currently present on disk.
storage.usingInMemoryDatabaseWhether SQLite had to fall back to in-memory mode.
libraryIndex.currentGalleryRootCurrent configured gallery root.
libraryIndex.previousGalleryRootPrior configured gallery root when the root changed.
libraryIndex.lastSuccessfulGalleryRootGallery root from the last completed successful scan.
lastScanLast completed scan run.

The same preferences.treatStoriesAsFolders and storiesMigration fields from GET /api/status are also present here.

Mutating endpoints

POST /api/auth/login

Body:

json
{
  "password": "your-admin-or-viewer-password"
}

Success:

json
{
  "ok": true,
  "auth": {
    "enabled": true,
    "authenticated": true,
    "role": "viewer",
    "accessMode": "password",
    "likesMode": "shared",
    "capabilities": {
      "canManageLibrary": false,
      "canDeleteMedia": false,
      "canAccessSettings": false,
      "canUseSharedLikes": true,
      "canUseLocalFavorites": false
    }
  }
}

Errors:

  • 400 if password protection is not enabled
  • 401 if the password is incorrect
  • 403 when trust requirements are missing

POST /api/auth/unlock-admin

Elevates the current browser into an admin session by verifying the admin password.

This route stays reachable without an existing authenticated session so it can be used from viewer mode and public mode.

Body:

json
{
  "password": "your-admin-password"
}

Success:

json
{
  "ok": true,
  "auth": {
    "enabled": true,
    "authenticated": true,
    "role": "admin",
    "accessMode": "public",
    "likesMode": "shared",
    "capabilities": {
      "canManageLibrary": true,
      "canDeleteMedia": true,
      "canAccessSettings": true,
      "canUseSharedLikes": true,
      "canUseLocalFavorites": false
    }
  }
}

Errors:

  • 400 if password protection is not enabled
  • 401 if the admin password is incorrect
  • 403 when trust requirements are missing

POST /api/auth/logout

Clears the current browser session cookie.

Success:

json
{
  "ok": true,
  "auth": {
    "enabled": true,
    "authenticated": false
  }
}

PUT /api/auth/password

Sets or changes the admin password.

Body when protection is disabled:

json
{
  "password": "new-password"
}

Body when protection is already enabled:

json
{
  "currentPassword": "old-password",
  "password": "new-password"
}

Notes:

  • password minimum length is 8
  • viewer sessions receive 403
  • changing the password invalidates older sessions

DELETE /api/auth/password

Disables password protection entirely.

Body:

json
{
  "currentPassword": "current-password"
}

Errors:

  • 400 if protection is already disabled
  • 401 if the password is incorrect
  • 403 if the current session is not admin

PUT /api/auth/viewer-access

Configures viewer access mode.

Body for admin-only access:

json
{
  "mode": "off"
}

Body for password-protected viewer access:

json
{
  "mode": "password",
  "viewerPassword": "viewer-password"
}

Notes:

  • viewerPassword is required when mode=password
  • viewer and admin passwords must differ
  • changing viewer access invalidates older sessions
  • mode=public enables anonymous browsing immediately
  • anonymous public sessions use local browser favorites instead of shared /api/likes

PUT /api/admin/settings/home-feed-default

Sets the default mode used when Home opens.

Body:

json
{
  "defaultMode": "recent"
}

Allowed values:

  • recent
  • rediscover
  • random

Success:

json
{
  "defaultMode": "recent"
}

PUT /api/admin/settings/reels-feed-default

Sets the app-wide default mode used when /reels opens.

Body:

json
{
  "defaultMode": "recommended"
}

Allowed values:

  • recommended
  • recent
  • random

Success:

json
{
  "defaultMode": "recommended"
}

PUT /api/admin/settings/stories-mode

Sets how folders literally named stories are interpreted.

Body:

json
{
  "treatStoriesAsFolders": false
}

Success:

json
{
  "treatStoriesAsFolders": false
}

Notes:

  • false enables the default reserved-stories mode
  • true keeps stories/ folders behaving like ordinary app folders
  • the Settings UI immediately follows this write with POST /api/admin/rescan, but this endpoint itself only saves the setting

POST /api/images/:id/like

Marks a post as liked.

Success:

json
{
  "ok": true,
  "id": 42,
  "liked": true
}

Errors:

  • 404 with {"message":"Image not found"} when the post does not exist or is deleted
  • 403 when trust requirements are missing

DELETE /api/images/:id/like

Removes a like.

Success:

json
{
  "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:

json
{
  "ok": true,
  "id": 42,
  "folderSlug": "oslo"
}

Errors:

  • 404 with {"message":"Image not found"}
  • 403 for viewer sessions
  • 403 when trust requirements are missing

DELETE /api/folders/:slug

Query parameters:

ParamTypeDefaultNotes
deleteSourceFolderbooleanfalseAccepts 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:

json
{
  "ok": true,
  "slug": "oslo",
  "deletedImageCount": 12,
  "deletedFolderCount": 1,
  "deletedSourceFolder": false
}

Errors:

  • 404 with {"message":"Folder not found"}
  • 403 for viewer sessions
  • 403 when trust requirements are missing

POST /api/admin/rescan

Runs a manual scan against the current library and then starts the development watcher.

Success:

json
{
  "ok": true,
  "lastScan": {
    "id": 7,
    "status": "completed",
    "scanned_files": 120
  }
}

Errors:

  • 409 with the library-rebuild-required message when the gallery root changed
  • 403 for viewer sessions
  • 500 for 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.

Matching cached derivatives already on disk are left in place and can be reused. In DERIVATIVE_MODE=lazy, this rebuild does not pre-generate missing thumbnails or previews.

This resets:

  • likes
  • images
  • folders
  • folder_scan_state
  • scan_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:

  • 409 with the library-rebuild-required message when a library-index rebuild is required
  • 403 for viewer sessions

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 mediaType is sent
  • which routes are expected to return items arrays versus single objects

Released under the AGPL-3.0 License.