Skip to content

API Reference

Generated from the server's source by cd server && npm run generate-api-docs; do not edit it by hand. Last updated: 2026-10-13

Authentication names who may call a route: None (anyone), Session (a signed-in Peek user), Admin (a signed-in admin), Session or signed link (a session, or the personal link the external player gets), or None until setup starts, then Admin (open to the setup wizard while Peek has no user and no Stash server).

Contents

Server

Health check, version, and server statistics.

GET /api/health

Health check (no auth). Proves Node answers through nginx and nothing more: no database query, per the homelab health-check convention.

Authentication: None

Handler: inline in server/initializers/api.ts


GET /api/version

Version endpoint (no auth required)

Authentication: None

Handler: inline in server/initializers/api.ts


GET /api/stats

Server stats endpoint (admin only - authenticated)

Authentication: Admin

Response:

interface GetStatsResponse {
  system: StatsSystemInfo;
  process: StatsProcessInfo;
  cache: StatsCacheInfo;
  database: StatsDatabaseInfo;
}

Handler: getStats in server/controllers/stats.ts


POST /api/stats/refresh-cache

Refresh cache endpoint (admin only)

Authentication: Admin

Response:

interface RefreshCacheResponse {
  success: boolean;
  message: string;
  error?: string;
}

Handler: refreshCache in server/controllers/stats.ts


Auth

Sign in and out, the signed-in user, and password recovery with a recovery key.

POST /api/auth/login

Login endpoint

Authentication: None

Handler: inline in server/routes/auth.ts


POST /api/auth/logout

Logout endpoint

Authentication: None

Handler: inline in server/routes/auth.ts


GET /api/auth/me

Get current user

Authentication: Session

Handler: inline in server/routes/auth.ts


GET /api/auth/check

Check if authenticated

Authentication: Session

Handler: inline in server/routes/auth.ts


POST /api/auth/forgot-password/init

Forgot password - check username and get recovery method

Authentication: None

Handler: inline in server/routes/auth.ts


POST /api/auth/forgot-password/reset

Forgot password - verify recovery key and set new password

Authentication: None

Handler: inline in server/routes/auth.ts


Setup

The setup wizard, and the admin's management of Stash servers.

GET /api/setup/status

Authentication: None

Response:

interface GetSetupStatusResponse {
  setupComplete: boolean;
  hasUsers: boolean;
  hasStashInstance: boolean;
  stashInstanceCount: number;
}

Handler: getSetupStatus in server/controllers/setup.ts


POST /api/setup/create-admin

Authentication: None

Request Body:

interface CreateFirstAdminRequest {
  username: string;
  password: string;
}

Response:

interface CreateFirstAdminResponse {
  success: true;
  user: {
    id: number;
    username: string;
    role: string;
    createdAt: string;
  };
}

Handler: createFirstAdmin in server/controllers/setup.ts


POST /api/setup/test-stash-connection

Authentication: None until setup starts, then Admin

Request Body:

interface TestStashConnectionRequest {
  url: string;
  apiKey: string;
}

Response:

interface TestStashConnectionResponse {
  success: boolean;
  message?: string;
  error?: string;
  version?: string;
  /** Stash's own error text; only the admin-only test of a saved instance sends it */
  details?: string;
}

Handler: testStashConnection in server/controllers/setup.ts


POST /api/setup/create-stash-instance

Authentication: None until setup starts, then Admin

Request Body:

interface CreateFirstStashInstanceRequest {
  name?: string;
  url: string;
  uiUrl?: string;
  apiKey: string;
}

Response:

interface CreateFirstStashInstanceResponse {
  success: true;
  instance: {
    id: string;
    name: string;
    url: string;
    uiUrl: string | null;
    enabled: boolean;
    createdAt: string;
  };
}

Handler: createFirstStashInstance in server/controllers/setup.ts


GET /api/setup/stash-instance

The instance record carries Stash's address: admin only, like the Server settings tab that shows it

Authentication: Admin

Response:

interface GetStashInstanceResponse {
  instance: {
    id: string;
    name: string;
    url: string;
    uiUrl: string | null;
    enabled: boolean;
    priority: number;
    createdAt: string;
    updatedAt: string;
    /**
     * The VR tag the admin chose for this server, a bare tag id on it; null
     * uses Stash's own
     */
    vrTagId: string | null;
    /**
     * The chosen tag's name; null when none is chosen or the tag was deleted in
     * Stash (Peek then uses Stash's tag)
     */
    vrTagName: string | null;
    /**
     * Stash's own VR tag (its `ui.vrTag` setting) by name, as last read by a
     * sync; null when Stash has none
     */
    stashVrTag: string | null;
  } | null;
  instanceCount: number;
}

Handler: getStashInstance in server/controllers/setup.ts


GET /api/setup/stash-instances

Authentication: Admin

Response:

interface GetAllStashInstancesResponse {
  instances: StashInstanceData[];
}

Handler: getAllStashInstances in server/controllers/setup.ts


POST /api/setup/stash-instance

Authentication: Admin

Request Body:

interface CreateStashInstanceRequest {
  name: string;
  description?: string;
  url: string;
  uiUrl?: string;
  apiKey: string;
  enabled?: boolean;
  priority?: number;
}

Response:

interface CreateStashInstanceResponse {
  success: true;
  instance: StashInstanceData;
  /**
   * The instance's first sync: "started", "queued" to start once the running
   * sync ends, or "none" for a disabled instance
   */
  sync: "started" | "queued" | "none";
}

Handler: createStashInstance in server/controllers/setup.ts


PUT /api/setup/stash-instance/:id

Authentication: Admin

Request Body:

interface UpdateStashInstanceRequest {
  name?: string;
  description?: string;
  url?: string;
  uiUrl?: string;
  apiKey?: string;
  enabled?: boolean;
  priority?: number;
  /**
   * The server's VR tag: a bare id of a live tag on it (400 otherwise), or
   * null to use Stash's own. A new `url` clears it.
   */
  vrTagId?: string | null;
}

URL Parameters:

interface UpdateStashInstanceParams extends Record<string, string> {
  id: string;
}

Response:

interface UpdateStashInstanceResponse {
  success: true;
  instance: StashInstanceData;
  /**
   * The re-sync a new URL or API key needs, or the first sync of an instance
   * enabled before one ever finished: "started", "queued" to start once the
   * running sync ends, or "none" (nothing to fetch, or disabled)
   */
  sync: "started" | "queued" | "none";
}

Handler: updateStashInstance in server/controllers/setup.ts


POST /api/setup/stash-instance/:id/test-connection

Authentication: Admin

Request Body:

interface TestSavedStashInstanceRequest {
  url?: string;
  apiKey?: string;
}

URL Parameters:

interface TestSavedStashInstanceParams extends Record<string, string> {
  id: string;
}

Response:

interface TestStashConnectionResponse {
  success: boolean;
  message?: string;
  error?: string;
  version?: string;
  /** Stash's own error text; only the admin-only test of a saved instance sends it */
  details?: string;
}

Handler: testSavedStashInstance in server/controllers/setup.ts


DELETE /api/setup/stash-instance/:id

Authentication: Admin

Request Body: never

URL Parameters:

interface DeleteStashInstanceParams extends Record<string, string> {
  id: string;
}

Response:

interface DeleteStashInstanceResponse {
  success: true;
  message: string;
}

Handler: deleteStashInstance in server/controllers/setup.ts


User

The signed-in user's settings, filter presets, hidden items, Stash server selection and permissions; user management for admins.

GET /api/user/settings

Authentication: Session

Response:

interface GetUserSettingsResponse {
  settings: {
    preferredPreviewQuality: string | null;
    /** A built-in key, the user's own `custom-<id>`, or null when none is chosen */
    theme: string | null;
    carouselPreferences: CarouselPreference[];
    navPreferences: NavPreference[] | null;
    minimumPlayPercent: number;
    syncToStash: boolean;
    hideConfirmationDisabled: boolean;
    unitPreference: string;
    wallPlayback: string;
    tableColumnDefaults: Record<string, TableColumnsConfig> | null;
    cardDisplaySettings: CardDisplaySettings;
    landingPagePreference: LandingPagePreference;
    lightboxDoubleTapAction: string;
    recommendationPreferences: RecommendationPreferences;
  };
}

Handler: getUserSettings in server/controllers/user.ts


PUT /api/user/settings

Authentication: Session

Request Body:

interface UpdateUserSettingsBody {
  preferredPreviewQuality?: string;
  /** A built-in key or the target user's own `custom-<id>`; null clears it */
  theme?: string | null;
  carouselPreferences?: CarouselPreference[];
  navPreferences?: NavPreference[];
  minimumPlayPercent?: number;
  syncToStash?: boolean;
  unitPreference?: string;
  wallPlayback?: string;
  tableColumnDefaults?: Record<string, TableColumnsConfig> | null;
  cardDisplaySettings?: CardDisplaySettings;
  landingPagePreference?: LandingPagePreference | null;
  lightboxDoubleTapAction?: string;
  recommendationPreferences?: RecommendationPreferences;
}

URL Parameters:

interface UpdateUserSettingsParams extends Record<string, string> {
  userId: string;
}

Response:

interface UpdateUserSettingsResponse {
  success: true;
  settings: {
    theme: string | null;
    carouselPreferences: CarouselPreference[];
    navPreferences: NavPreference[] | null;
    minimumPlayPercent: number;
    syncToStash: boolean;
    wallPlayback: string;
    tableColumnDefaults: Record<string, TableColumnsConfig> | null;
    cardDisplaySettings: CardDisplaySettings;
    landingPagePreference: LandingPagePreference;
    lightboxDoubleTapAction: string;
    recommendationPreferences: RecommendationPreferences;
  };
}

Handler: updateUserSettings in server/controllers/user.ts


POST /api/user/change-password

Authentication: Session

Request Body:

interface ChangePasswordBody {
  currentPassword: string;
  newPassword: string;
}

Response:

interface ChangePasswordResponse {
  success: true;
  message: string;
}

Handler: changePassword in server/controllers/user.ts


GET /api/user/recovery-key

Authentication: Session

Response:

interface GetRecoveryKeyResponse {
  hasRecoveryKey: boolean;
}

Handler: getRecoveryKey in server/controllers/user.ts


POST /api/user/recovery-key/regenerate

Authentication: Session

Request Body:

interface RegenerateRecoveryKeyBody {
  currentPassword: string;
}

Response:

interface RegenerateRecoveryKeyResponse {
  recoveryKey: string;
}

Handler: regenerateRecoveryKey in server/controllers/user.ts


GET /api/user/setup-status

Authentication: Session

Response:

{ setupCompleted: boolean; instances: unknown[]; instanceCount: number; }

Handler: getSetupStatus in server/controllers/user.ts


POST /api/user/complete-setup

Authentication: Session

Request Body:

interface CompleteSetupBody {
  selectedInstanceIds?: string[];
}

Response:

interface CompleteSetupResponse {
  success: true;
  recoveryKey: string | null;
}

Handler: completeSetup in server/controllers/user.ts


GET /api/user/filter-presets

Authentication: Session

Response:

interface GetFilterPresetsResponse {
  presets: FilterPresets;
}

Handler: getFilterPresets in server/controllers/user.ts


POST /api/user/filter-presets

Authentication: Session

Request Body:

interface SaveFilterPresetBody {
  artifactType: string;
  context?: string;
  name: string;
  filters: unknown;
  sort: string;
  direction: string;
  viewMode?: string;
  zoomLevel?: string;
  gridDensity?: string;
  tableColumns?: TableColumnsConfig | null;
  perPage?: number | null;
  setAsDefault?: boolean;
}

Response:

interface SaveFilterPresetResponse {
  success: true;
  preset: FilterPreset;
}

Handler: saveFilterPreset in server/controllers/user.ts


PUT /api/user/filter-presets/:artifactType/:presetId

Authentication: Session

Request Body:

type OverwriteViewBody = Omit<
  SaveFilterPresetBody,
  "artifactType" | "context" | "name" | "setAsDefault"
>;

URL Parameters:

interface DeleteFilterPresetParams extends Record<string, string> {
  artifactType: string;
  presetId: string;
}

Response:

interface SaveFilterPresetResponse {
  success: true;
  preset: FilterPreset;
}

Handler: overwriteFilterPreset in server/controllers/user.ts


PATCH /api/user/filter-presets/:artifactType/:presetId

Authentication: Session

Request Body:

interface RenameViewBody {
  name: string;
}

URL Parameters:

interface DeleteFilterPresetParams extends Record<string, string> {
  artifactType: string;
  presetId: string;
}

Response:

interface SaveFilterPresetResponse {
  success: true;
  preset: FilterPreset;
}

Handler: renameFilterPreset in server/controllers/user.ts


DELETE /api/user/filter-presets/:artifactType/:presetId

Authentication: Session

Request Body: never

URL Parameters:

interface DeleteFilterPresetParams extends Record<string, string> {
  artifactType: string;
  presetId: string;
}

Response:

interface DeleteFilterPresetResponse {
  success: true;
}

Handler: deleteFilterPreset in server/controllers/user.ts


GET /api/user/default-presets

Authentication: Session

Response:

interface GetDefaultFilterPresetsResponse {
  defaults: DefaultFilterPresets;
}

Handler: getDefaultFilterPresets in server/controllers/user.ts


PUT /api/user/default-preset

Authentication: Session

Request Body:

interface SetDefaultFilterPresetBody {
  context: string;
  presetId?: string;
}

Response:

interface SetDefaultFilterPresetResponse {
  success: true;
  defaults: DefaultFilterPresets;
}

Handler: setDefaultFilterPreset in server/controllers/user.ts


GET /api/user/filter-pins

Authentication: Session

Response:

interface GetFilterPinsResponse {
  pins: FilterPins;
}

Handler: getFilterPins in server/controllers/user.ts


PUT /api/user/filter-pins/:list

Authentication: Session

Request Body:

type PutFilterPinsBody = ListPins;

URL Parameters:

interface FilterPinsParams extends Record<string, string> {
  list: string;
}

Response:

interface FilterPinsListResponse {
  pins: ListPins;
}

Handler: putFilterPins in server/controllers/user.ts


DELETE /api/user/filter-pins/:list

Authentication: Session

Request Body: never

URL Parameters:

interface FilterPinsParams extends Record<string, string> {
  list: string;
}

Response:

interface FilterPinsListResponse {
  pins: ListPins;
}

Handler: resetFilterPins in server/controllers/user.ts


GET /api/user/permissions

User's own permissions

Authentication: Session

Response:

interface GetMyPermissionsResponse {
  permissions: {
    canShare: boolean;
    canDownloadFiles: boolean;
    canDownloadPlaylists: boolean;
    sources: {
      canShare: string;
      canDownloadFiles: string;
      canDownloadPlaylists: string;
    };
  };
}

Handler: getUserPermissions in server/controllers/user.ts


GET /api/user/all

Authentication: Admin

Response:

interface GetAllUsersResponse {
  users: Array<{
    id: number;
    username: string;
    role: string;
    createdAt: string;
    updatedAt: string;
    syncToStash: boolean;
    groups: Array<{ id: number; name: string }>;
  }>;
}

Handler: getAllUsers in server/controllers/user.ts


POST /api/user/create

Authentication: Admin

Request Body:

interface CreateUserBody {
  username: string;
  password: string;
  role?: string;
}

Response:

interface CreateUserResponse {
  success: true;
  user: {
    id: number;
    username: string;
    role: string;
    createdAt: string;
  };
}

Handler: createUser in server/controllers/user.ts


DELETE /api/user/:userId

Authentication: Admin

Request Body: never

URL Parameters:

interface DeleteUserParams extends Record<string, string> {
  userId: string;
}

Response:

interface DeleteUserResponse {
  success: true;
  message: string;
}

Handler: deleteUser in server/controllers/user.ts


PUT /api/user/:userId/role

Authentication: Admin

Request Body:

interface UpdateUserRoleBody {
  role: string;
}

URL Parameters:

interface UpdateUserRoleParams extends Record<string, string> {
  userId: string;
}

Response:

interface UpdateUserRoleResponse {
  success: true;
  user: {
    id: number;
    username: string;
    role: string;
    updatedAt: string;
  };
}

Handler: updateUserRole in server/controllers/user.ts


PUT /api/user/:userId/settings

Admin can update any user's settings

Authentication: Admin

Request Body:

interface UpdateUserSettingsBody {
  preferredPreviewQuality?: string;
  /** A built-in key or the target user's own `custom-<id>`; null clears it */
  theme?: string | null;
  carouselPreferences?: CarouselPreference[];
  navPreferences?: NavPreference[];
  minimumPlayPercent?: number;
  syncToStash?: boolean;
  unitPreference?: string;
  wallPlayback?: string;
  tableColumnDefaults?: Record<string, TableColumnsConfig> | null;
  cardDisplaySettings?: CardDisplaySettings;
  landingPagePreference?: LandingPagePreference | null;
  lightboxDoubleTapAction?: string;
  recommendationPreferences?: RecommendationPreferences;
}

URL Parameters:

interface UpdateUserSettingsParams extends Record<string, string> {
  userId: string;
}

Response:

interface UpdateUserSettingsResponse {
  success: true;
  settings: {
    theme: string | null;
    carouselPreferences: CarouselPreference[];
    navPreferences: NavPreference[] | null;
    minimumPlayPercent: number;
    syncToStash: boolean;
    wallPlayback: string;
    tableColumnDefaults: Record<string, TableColumnsConfig> | null;
    cardDisplaySettings: CardDisplaySettings;
    landingPagePreference: LandingPagePreference;
    lightboxDoubleTapAction: string;
    recommendationPreferences: RecommendationPreferences;
  };
}

Handler: updateUserSettings in server/controllers/user.ts


POST /api/user/:userId/sync-from-stash

Admin can sync Stash data for any user

Authentication: Admin

Request Body:

interface SyncFromStashBody {
  options?: {
    [K in keyof SyncFromStashOptions]?: Partial<SyncFromStashOptions[K]>;
  };
}

URL Parameters:

interface SyncFromStashParams extends Record<string, string> {
  userId: string;
}

Response:

interface SyncFromStashResponse {
  /** False when an instance's import failed; the others still ran */
  success: boolean;
  message: string;
  stats: SyncStats;
  /** The instances whose import failed (the name falls back to the id) */
  failedInstances: Array<{ id: string; name: string }>;
}

Handler: syncFromStash in server/controllers/user.ts


GET /api/user/:userId/permissions

Authentication: Admin

Request Body: never

URL Parameters:

interface GetUserPermissionsParams extends Record<string, string> {
  userId: string;
}

Response:

interface GetMyPermissionsResponse {
  permissions: {
    canShare: boolean;
    canDownloadFiles: boolean;
    canDownloadPlaylists: boolean;
    sources: {
      canShare: string;
      canDownloadFiles: string;
      canDownloadPlaylists: string;
    };
  };
}

Handler: getAnyUserPermissions in server/controllers/user.ts


PUT /api/user/:userId/permissions

Authentication: Admin

Request Body:

interface UpdatePermissionOverridesBody {
  canShareOverride?: boolean | null;
  canDownloadFilesOverride?: boolean | null;
  canDownloadPlaylistsOverride?: boolean | null;
}

URL Parameters:

interface GetUserPermissionsParams extends Record<string, string> {
  userId: string;
}

Response:

{ success: true; permissions: unknown }

Handler: updateUserPermissionOverrides in server/controllers/user.ts


GET /api/user/:userId/groups

Admin: get user's group memberships

Authentication: Admin

Request Body: never

URL Parameters:

interface GetUserGroupMembershipsParams extends Record<string, string> {
  userId: string;
}

Response:

interface GetUserGroupMembershipsResponse {
  groups: UserGroupSummary[];
}

Handler: getUserGroupMemberships in server/controllers/user.ts


POST /api/user/:userId/reset-password

Admin: reset user password

Authentication: Admin

Request Body:

interface AdminResetPasswordBody {
  newPassword: string;
}

URL Parameters:

interface AdminResetPasswordParams extends Record<string, string> {
  userId: string;
}

Response:

interface AdminResetPasswordResponse {
  success: true;
}

Handler: adminResetPassword in server/controllers/user.ts


POST /api/user/:userId/regenerate-recovery-key

Admin: regenerate user recovery key

Authentication: Admin

Request Body: never

URL Parameters:

interface AdminRegenerateRecoveryKeyParams extends Record<
  string,
  string
> {
  userId: string;
}

Response:

interface AdminRegenerateRecoveryKeyResponse {
  recoveryKey: string;
}

Handler: adminRegenerateRecoveryKey in server/controllers/user.ts


GET /api/user/:userId/restrictions

Get user's content restrictions

Authentication: Admin

Request Body: never

URL Parameters:

interface GetUserRestrictionsParams extends Record<string, string> {
  userId: string;
}

Response:

interface GetUserRestrictionsResponse {
  restrictions: StoredRestriction[];
}

Handler: getUserRestrictions in server/controllers/user.ts


PUT /api/user/:userId/restrictions

Update user's content restrictions

Authentication: Admin

Request Body:

interface UpdateUserRestrictionsBody {
  restrictions: UserRestriction[];
}

URL Parameters:

interface GetUserRestrictionsParams extends Record<string, string> {
  userId: string;
}

Response:

interface UpdateUserRestrictionsResponse {
  success: true;
  message: string;
  restrictions: unknown[];
}

Handler: updateUserRestrictions in server/controllers/user.ts


DELETE /api/user/:userId/restrictions

Delete all user's content restrictions

Authentication: Admin

Request Body: never

URL Parameters:

interface DeleteUserRestrictionsParams extends Record<string, string> {
  userId: string;
}

Response:

interface DeleteUserRestrictionsResponse {
  success: true;
  message: string;
}

Handler: deleteUserRestrictions in server/controllers/user.ts


POST /api/user/hidden-entities

Hide an entity

Authentication: Session

Request Body:

interface HideEntityBody {
  entityType: string;
  entityId: string;
  /** The entity's Stash instance; a hide without one answers 400 */
  instanceId: string;
}

Response:

interface HideEntityResponse {
  success: true;
  message: string;
}

Handler: hideEntity in server/controllers/user.ts


POST /api/user/hidden-entities/bulk

Hide multiple entities

Authentication: Session

Request Body:

interface HideEntitiesBody {
  entities: Array<{
    entityType: string;
    entityId: string;
    /** The entity's Stash instance; a target without one answers 400 */
    instanceId: string;
  }>;
}

Response:

interface HideEntitiesResponse {
  success: true;
  message: string;
  successCount: number;
  failCount: number;
}

Handler: hideEntities in server/controllers/user.ts


DELETE /api/user/hidden-entities/all

Unhide all entities (optionally filtered by type) - must be before :entityType/:entityId route

Authentication: Session

Request Body: never

URL Parameters: Record<string, string>

Query Parameters:

interface UnhideAllEntitiesQuery extends Record<
  string,
  string | string[] | undefined
> {
  entityType?: string;
}

Response:

interface UnhideAllEntitiesResponse {
  success: true;
  message: string;
  count: number;
}

Handler: unhideAllEntities in server/controllers/user.ts


DELETE /api/user/hidden-entities/:entityType/:entityId

Unhide an entity

Authentication: Session

Request Body: never

URL Parameters:

interface UnhideEntityParams extends Record<string, string> {
  entityType: string;
  entityId: string;
}

Query Parameters:

interface UnhideEntityQuery extends Record<
  string,
  string | string[] | undefined
> {
  instanceId?: string;
}

Response:

interface UnhideEntityResponse {
  success: true;
  message: string;
}

Handler: unhideEntity in server/controllers/user.ts


GET /api/user/hidden-entities

One page of hidden entities, with counts per type

Authentication: Session

Request Body: never

URL Parameters: Record<string, string>

Query Parameters:

interface GetHiddenEntitiesQuery extends Record<
  string,
  string | string[] | undefined
> {
  entityType?: string;
  /** 1-based, default 1 */
  page?: string;
  /** 1 to 100, default 50 */
  per_page?: string;
}

Response:

interface GetHiddenEntitiesResponse {
  /** The page's rows, newest first */
  items: HiddenEntityItem[];
  /** Rows of the requested type (every hideable type when none) */
  total: number;
  /** The user's rows per hideable type, whatever type was requested */
  counts: Record<HiddenEntityType, number>;
}

Handler: getHiddenEntities in server/controllers/user.ts


PUT /api/user/hide-confirmation

Update hide confirmation preference

Authentication: Session

Request Body:

interface UpdateHideConfirmationBody {
  hideConfirmationDisabled: boolean;
}

Response:

interface UpdateHideConfirmationResponse {
  success: true;
  hideConfirmationDisabled: boolean;
}

Handler: updateHideConfirmation in server/controllers/user.ts


GET /api/user/stash-instances

Get user's selected instances

Authentication: Session

Response:

{ selectedInstanceIds: string[]; availableInstances: unknown[] }

Handler: getUserStashInstances in server/controllers/user.ts


PUT /api/user/stash-instances

Update user's instance selection

Authentication: Session

Request Body:

interface UpdateUserStashInstancesBody {
  instanceIds: string[];
}

Response:

{ success: true; selectedInstanceIds: string[] }

Handler: updateUserStashInstances in server/controllers/user.ts


User Groups

User groups and their members (admin only, except your own).

GET /api/groups/user/mine

User-facing route (must be before /:id to avoid conflicts)

Authentication: Session

Response:

interface GetCurrentUserGroupsResponse {
  groups: UserGroupSummary[];
}

Handler: getUserGroups in server/controllers/groups.ts


GET /api/groups

Authentication: Admin

Response:

interface GetAllUserGroupsResponse {
  groups: GroupData[];
}

Handler: getAllGroups in server/controllers/groups.ts


GET /api/groups/:id

Authentication: Admin

Request Body: never

URL Parameters:

interface GetUserGroupParams extends Record<string, string> {
  id: string;
}

Response:

interface GetUserGroupResponse {
  group: GroupWithMembers;
}

Handler: getGroup in server/controllers/groups.ts


POST /api/groups

Authentication: Admin

Request Body:

interface CreateUserGroupBody {
  name: string;
  /** null or empty clears it */
  description?: string | null;
  canShare?: boolean;
  canDownloadFiles?: boolean;
  canDownloadPlaylists?: boolean;
}

Response:

interface CreateUserGroupResponse {
  group: {
    id: number;
    name: string;
    description: string | null;
    canShare: boolean;
    canDownloadFiles: boolean;
    canDownloadPlaylists: boolean;
    createdAt: string;
    updatedAt: string;
  };
}

Handler: createGroup in server/controllers/groups.ts


PUT /api/groups/:id

Authentication: Admin

Request Body:

interface UpdateUserGroupBody {
  name?: string;
  /** null or empty clears it */
  description?: string | null;
  canShare?: boolean;
  canDownloadFiles?: boolean;
  canDownloadPlaylists?: boolean;
}

URL Parameters:

interface UpdateUserGroupParams extends Record<string, string> {
  id: string;
}

Response:

interface UpdateUserGroupResponse {
  group: {
    id: number;
    name: string;
    description: string | null;
    canShare: boolean;
    canDownloadFiles: boolean;
    canDownloadPlaylists: boolean;
    createdAt: string;
    updatedAt: string;
  };
}

Handler: updateGroup in server/controllers/groups.ts


DELETE /api/groups/:id

Authentication: Admin

Request Body: never

URL Parameters:

interface DeleteUserGroupParams extends Record<string, string> {
  id: string;
}

Response:

interface DeleteUserGroupResponse {
  success: true;
}

Handler: deleteGroup in server/controllers/groups.ts


POST /api/groups/:id/members

Authentication: Admin

Request Body:

interface AddMemberBody {
  userId: number;
}

URL Parameters:

interface AddMemberParams extends Record<string, string> {
  id: string;
}

Response:

interface AddMemberResponse {
  membership: {
    id: number;
    userId: number;
    groupId: number;
    createdAt: string;
  };
}

Handler: addMember in server/controllers/groups.ts


DELETE /api/groups/:id/members/:userId

Authentication: Admin

Request Body: never

URL Parameters:

interface RemoveMemberParams extends Record<string, string> {
  id: string;
  userId: string;
}

Response:

interface RemoveMemberResponse {
  success: true;
}

Handler: removeMember in server/controllers/groups.ts


Library

Browsing scenes, performers, studios, tags, collections, galleries and images, with the user's restrictions and hidden items applied.

GET /api/library/ready

Authentication: Session

Response:

interface LibraryReadyResponse {
  ready: boolean;
}

Handler: getLibraryReady in server/routes/library/ready.ts


POST /api/library/scenes

Find scenes with filters

Authentication: Session

Request Body:

type FindScenesRequest = ListRequestInput<"scene">;

Response: FindScenesResponse<ListCount> | AmbiguousLookupResponse

Handler: findScenes in server/controllers/library/scenes.ts


POST /api/library/scenes/minimal

Minimal data for the scene picker (a clip filter's scenes)

Authentication: Session

Request Body:

type FindScenesMinimalRequest = Omit<
  MinimalRequest,
  "scope" | "count_filter"
>;

Response:

interface FindScenesMinimalResponse {
  scenes: MinimalEntity[];
}

Handler: findScenesMinimal in server/controllers/library/scenes.ts


GET /api/library/scenes/:id/similar

Find similar scenes

Authentication: Session

URL Parameters:

interface FindSimilarScenesParams extends Record<string, string> {
  id: string;
}

Query Parameters:

interface FindSimilarScenesQuery extends Record<
  string,
  string | undefined
> {
  page?: string;
  /** The seed's instance (required; a request without it answers 400) */
  instanceId?: string;
}

Response:

interface FindSimilarScenesResponse {
  scenes: NormalizedScene[];
  count: number;
  page: number;
  perPage: number;
}

Handler: findSimilarScenes in server/controllers/library/scenes.ts


POST /api/library/scenes/recommended

Recommended: the scene list request within the user's ranked scenes

Authentication: Session

Request Body:

type FindRecommendedScenesRequest = Omit<
  ListRequestInput<"scene">,
  "filter" | "ids"
> & {
  filter?: Omit<ListPageInput<"scene">, "sort"> & {
    sort?: RecommendedSort | RandomSortKey;
  };
};

Response:

interface GetRecommendedScenesResponse {
  scenes: WithStashUrl<NormalizedScene>[];
  /** The scenes matching the request within the ranked list; null when it asked for none (`filter.count: false`) */
  count: number | null;
  page: number;
  perPage: number;
  message?: string;
  criteria?: {
    favoritedPerformers: number;
    ratedPerformers: number;
    favoritedStudios: number;
    ratedStudios: number;
    favoritedTags: number;
    ratedTags: number;
    favoritedScenes: number;
    ratedScenes: number;
    /** Performers, studios and tags the user's viewing ranks in its top half */
    rankedEntities: number;
  };
}

Handler: findRecommendedScenes in server/controllers/library/scenes.ts


POST /api/library/scenes/recommended/count

How many scenes that request matches (the filter sheet's "Show N results")

Authentication: Session

Request Body:

type FindRecommendedScenesRequest = Omit<
  ListRequestInput<"scene">,
  "filter" | "ids"
> & {
  filter?: Omit<ListPageInput<"scene">, "sort"> & {
    sort?: RecommendedSort | RandomSortKey;
  };
};

Response:

interface ListCountResponse {
  count: number;
}

Handler: countRecommendedScenes in server/controllers/library/scenes.ts


GET /api/library/scenes/recommended

Get recommended scenes: for a tab still on the beta.8 bundle; remove in the release after 3.4.0-beta.9

Authentication: Session

URL Parameters: Record<string, string>

Query Parameters:

interface GetRecommendedScenesQuery extends Record<
  string,
  string | undefined
> {
  page?: string;
  per_page?: string;
}

Response:

interface GetRecommendedScenesResponse {
  scenes: WithStashUrl<NormalizedScene>[];
  /** The scenes matching the request within the ranked list; null when it asked for none (`filter.count: false`) */
  count: number | null;
  page: number;
  perPage: number;
  message?: string;
  criteria?: {
    favoritedPerformers: number;
    ratedPerformers: number;
    favoritedStudios: number;
    ratedStudios: number;
    favoritedTags: number;
    ratedTags: number;
    favoritedScenes: number;
    ratedScenes: number;
    /** Performers, studios and tags the user's viewing ranks in its top half */
    rankedEntities: number;
  };
}

Handler: getRecommendedScenes in server/controllers/library/scenes.ts


POST /api/library/performers

Find performers with filters

Authentication: Session

Request Body:

type FindPerformersRequest = ListRequestInput<"performer">;

Response: FindPerformersResponse<ListCount> | AmbiguousLookupResponse

Handler: findPerformers in server/controllers/library/performers.ts


POST /api/library/performers/minimal

Minimal data for filter dropdowns

Authentication: Session

Request Body:

type FindPerformersMinimalRequest = MinimalRequest;

Response:

interface FindPerformersMinimalResponse {
  performers: MinimalEntity[];
}

Handler: findPerformersMinimal in server/controllers/library/performers.ts


GET /api/library/performers/:id/counts

The detail page's tab counts, as the viewer sees them

Authentication: Session

Handler: getPerformerCounts in server/controllers/library/performers.ts


POST /api/library/studios

Find studios with filters

Authentication: Session

Request Body:

type FindStudiosRequest = ListRequestInput<"studio">;

Response: FindStudiosResponse<ListCount> | AmbiguousLookupResponse

Handler: findStudios in server/controllers/library/studios.ts


POST /api/library/studios/minimal

Minimal data for filter dropdowns

Authentication: Session

Request Body:

type FindStudiosMinimalRequest = MinimalRequest;

Response:

interface FindStudiosMinimalResponse {
  studios: MinimalEntity[];
}

Handler: findStudiosMinimal in server/controllers/library/studios.ts


GET /api/library/studios/:id/counts

The detail page's tab counts, as the viewer sees them

Authentication: Session

Handler: getStudioCounts in server/controllers/library/studios.ts


POST /api/library/tags

Find tags with filters

Authentication: Session

Request Body:

type FindTagsRequest = ListRequestInput<"tag">;

Response: FindTagsResponse<ListCount> | AmbiguousLookupResponse

Handler: findTags in server/controllers/library/tags.ts


POST /api/library/tags/minimal

Minimal data for filter dropdowns

Authentication: Session

Request Body:

type FindTagsMinimalRequest = MinimalRequest;

Response:

interface FindTagsMinimalResponse {
  tags: MinimalEntity[];
}

Handler: findTagsMinimal in server/controllers/library/tags.ts


POST /api/library/tags/tree

The compact tag tree (hierarchy and folder views), optionally scoped

Authentication: Session

Request Body: FindTagTreeRequest | undefined

Response:

interface FindTagTreeResponse {
  tags: TagTreeRow[];
  /**
   * With `untagged` in the request: the items of that type the user can see
   * with no tag of their own, the total of the Untagged folder's list
   * (`tag_count` EQUALS 0). With a scope, the scope's scenes (galleries and
   * images 0, as the scoped rows count).
   */
  untagged?: number;
}

Handler: findTagTree in server/controllers/library/tags.ts


GET /api/library/tags/:id/counts

The detail page's tab counts, as the viewer sees them

Authentication: Session

Handler: getTagCounts in server/controllers/library/tags.ts


POST /api/library/groups

Find groups with filters

Authentication: Session

Request Body:

type FindGroupsRequest = ListRequestInput<"group">;

Response: FindGroupsResponse<ListCount> | AmbiguousLookupResponse

Handler: findGroups in server/controllers/library/groups.ts


POST /api/library/groups/minimal

Minimal data for filter dropdowns

Authentication: Session

Request Body:

type FindGroupsMinimalRequest = MinimalRequest;

Response:

interface FindGroupsMinimalResponse {
  groups: MinimalEntity[];
}

Handler: findGroupsMinimal in server/controllers/library/groups.ts


GET /api/library/groups/:id/counts

The detail page's tab counts, as the viewer sees them

Authentication: Session

Handler: getGroupCounts in server/controllers/library/groups.ts


POST /api/library/galleries

Gallery list: filter, sort and page

Authentication: Session

Request Body:

type FindGalleriesRequest = ListRequestInput<"gallery">;

Response: FindGalleriesResponse<ListCount> | AmbiguousLookupResponse

Handler: findGalleries in server/controllers/library/galleries.ts


POST /api/library/galleries/minimal

Authentication: Session

Request Body:

type FindGalleriesMinimalRequest = MinimalRequest;

Response:

interface FindGalleriesMinimalResponse {
  galleries: MinimalEntity[];
}

Handler: findGalleriesMinimal in server/controllers/library/galleries.ts


GET /api/library/galleries/:id/counts

The detail page's tab counts, as the viewer sees them

Authentication: Session

Handler: getGalleryCounts in server/controllers/library/galleries.ts


POST /api/library/images

Find images (with filters, pagination, sorting)

Authentication: Session

Request Body:

type FindImagesRequest = ListRequestInput<"image">;

Response: FindImagesResponse<ListCount> | AmbiguousLookupResponse

Handler: findImages in server/controllers/library/images.ts


POST /api/library/clips

Find clips with the clip filter body (GET /api/clips keeps the old parameters)

Authentication: Session

Request Body:

type FindClipsRequest = ClipListRequestInput;

Response: FindClipsResponse<ListCount>

Handler: findClips in server/controllers/clips.ts


POST /api/library/scenes/count

Authentication: Session

Handler: inline in server/routes/library/counts.ts


POST /api/library/performers/count

Authentication: Session

Handler: inline in server/routes/library/counts.ts


POST /api/library/studios/count

Authentication: Session

Handler: inline in server/routes/library/counts.ts


POST /api/library/tags/count

Authentication: Session

Handler: inline in server/routes/library/counts.ts


POST /api/library/groups/count

Authentication: Session

Handler: inline in server/routes/library/counts.ts


POST /api/library/galleries/count

Authentication: Session

Handler: inline in server/routes/library/counts.ts


POST /api/library/images/count

Authentication: Session

Handler: inline in server/routes/library/counts.ts


POST /api/library/clips/count

Authentication: Session

Handler: inline in server/routes/library/counts.ts


Clips

Clips (scene markers from Stash).

GET /api/scenes/:id/clips

Scene clips endpoint (get clips for a specific scene)

Authentication: Session

Request Body: never

URL Parameters:

interface GetClipsForSceneParams extends Record<string, string> {
  id: string;
}

Query Parameters:

interface GetClipsForSceneQuery extends Record<
  string,
  string | string[] | undefined
> {
  includeUngenerated?: string;
  instanceId?: string;
}

Response:

interface GetClipsForSceneResponse {
  clips: ClipWithRelations[];
}

Handler: getClipsForScene in server/controllers/clips.ts


GET /api/clips

Authentication: Session

Request Body: never

URL Parameters: Record<string, string>

Query Parameters:

interface GetClipsQuery extends Record<
  string,
  string | string[] | undefined
> {
  page?: string;
  perPage?: string;
  sortBy?: string;
  sortDir?: string;
  isGenerated?: string;
  sceneId?: string;
  tagIds?: string;
  sceneTagIds?: string;
  performerIds?: string;
  studioId?: string;
  q?: string;
  instanceId?: string;
  /** "false": the page alone, `total` and `totalPages` null */
  count?: string;
}

Response: GetClipsResponse<ListCount>

Handler: getClips in server/controllers/clips.ts


GET /api/clips/:id

Authentication: Session

Request Body: never

URL Parameters:

interface GetClipByIdParams extends Record<string, string> {
  id: string;
}

Response: GetClipByIdResponse | AmbiguousLookupResponse

Handler: getClipById in server/controllers/clips.ts


Timeline

Date distribution for the timeline view.

POST /api/timeline/:entityType/distribution

The bars of a list: the list's own request (filter, search, ids) plus the period

Authentication: Session

Request Body:

type PostDateDistributionRequest<
  E extends TimelineEntityType = TimelineEntityType,
> = ListRequestInput<E> & {
  /** Default "months" */
  granularity?: TimelineGranularity;
};

URL Parameters:

interface GetDateDistributionParams extends Record<string, string> {
  entityType: string;
}

Response:

interface GetDateDistributionResponse {
  distribution: DateDistributionEntry[];
}

Handler: postDateDistribution in server/controllers/timelineController.ts


GET /api/timeline/:entityType/distribution

The documented form: one entity parameter (performerId, tagId, ...)

Authentication: Session

Request Body: never

URL Parameters:

interface GetDateDistributionParams extends Record<string, string> {
  entityType: string;
}

Query Parameters:

interface GetDateDistributionQuery extends Record<
  string,
  string | string[] | undefined
> {
  granularity?: string;
  performerId?: string;
  tagId?: string;
  studioId?: string;
  groupId?: string;
  galleryId?: string;
}

Response:

interface GetDateDistributionResponse {
  distribution: DateDistributionEntry[];
}

Handler: getDateDistribution in server/controllers/timelineController.ts


Playback

The stream and caption proxy, and the external player's personal signed link.

GET /api/scene/:sceneId/proxy-stream/:streamPath/:subPath

Authentication: Session or signed link

Request Body: never

URL Parameters:

{ sceneId: string; streamPath: string; subPath?: string }

Query Parameters:

{ instanceId?: string }

Handler: proxyStashStream in server/controllers/video.ts


GET /api/scene/:sceneId/proxy-stream/:streamPath

Authentication: Session or signed link

Request Body: never

URL Parameters:

{ sceneId: string; streamPath: string; subPath?: string }

Query Parameters:

{ instanceId?: string }

Handler: proxyStashStream in server/controllers/video.ts


GET /api/scene/:sceneId/caption

Caption/subtitle proxy

Authentication: Session or signed link

Request Body: never

URL Parameters:

{ sceneId: string }

Query Parameters:

{ lang?: string; type?: string; instanceId?: string }

Handler: getCaption in server/controllers/video.ts


GET /api/scene/:sceneId/poster

Scene poster (Stash's screenshot), for a Cast receiver's media link too

Authentication: Session or signed link

Request Body: never

URL Parameters:

{ sceneId: string }

Query Parameters:

{ instanceId?: string }

Handler: proxyScenePoster in server/controllers/proxy.ts


POST /api/scene/:sceneId/external-player-link

Personal signed link for the external player button

Authentication: Session

Request Body:

interface ExternalPlayerLinkRequest {
  instanceId: string;
}

URL Parameters:

{ sceneId: string }

Response:

interface ExternalPlayerLinkResponse {
  /** `/api/scene/:id/proxy-stream/stream?instanceId=...&uid=...&exp=...&sig=...` */
  url: string;
  /** ISO timestamp, 12 hours after minting. */
  expiresAt: string;
  /**
   * The scene file's MIME type from its extension (`video/x-matroska` for an
   * mkv), `video/*` when unknown. The path itself is never returned.
   */
  mimeType: string;
}

Handler: createExternalPlayerLink in server/controllers/video.ts


POST /api/scene/:sceneId/media-link

One signed media link per scene: Direct and HLS, captions and poster, for a Cast receiver or Safari's native player

Authentication: Session

Request Body:

interface SceneMediaLinkRequest {
  instanceId: string;
}

URL Parameters:

{ sceneId: string }

Response:

interface SceneMediaLinkResponse {
  /** ISO timestamp, 12 hours after minting. */
  expiresAt: string;
  /** Direct and the HLS tiers only, in `buildSceneStreams`' order. */
  streams: { url: string; mime_type: string; label: string }[];
  /** The one source a Cast receiver is given; null when none plays there. */
  cast: { url: string; contentType: string; kind: "direct" | "hls" } | null;
  captions: { url: string; lang: string; type: string }[];
  poster: string | null;
}

Handler: createSceneMediaLink in server/controllers/video.ts


Media Proxy

Images and previews from Stash, served through Peek so no user gets Stash's address or API key.

GET /api/proxy/stash

Media proxy (requires a Peek session; per-entity access in the handler)

Authentication: Session

Request Body: never

URL Parameters: Record<string, string>

Query Parameters:

{ path?: string; instanceId?: string }

Handler: proxyStashMedia in server/controllers/proxy.ts


GET /api/proxy/scene/:id/preview

Authentication: Session

Request Body: never

URL Parameters:

{ id: string }

Query Parameters:

{ instanceId?: string }

Handler: proxyScenePreview in server/controllers/proxy.ts


GET /api/proxy/scene/:id/webp

Authentication: Session

Request Body: never

URL Parameters:

{ id: string }

Query Parameters:

{ instanceId?: string }

Handler: proxySceneWebp in server/controllers/proxy.ts


GET /api/proxy/image/:imageId/:type

Image proxy route (requires a Peek session; per-entity access in the handler)

Authentication: Session

Request Body: never

URL Parameters:

{ imageId: string; type: string }

Query Parameters:

{ instanceId?: string }

Handler: proxyImage in server/controllers/proxy.ts


GET /api/proxy/clip/:id/preview

Clip preview proxy route (requires a Peek session; per-entity access in the handler)

Authentication: Session

Request Body: never

URL Parameters:

{ id: string }

Query Parameters:

{ instanceId?: string }

Handler: proxyClipPreview in server/controllers/proxy.ts


Playlists

Playlists, their items, play queue and sharing.

GET /api/playlists/shared

Get playlists shared with current user

Authentication: Session

URL Parameters: Record<string, string>

Query Parameters:

interface GetUserPlaylistsQuery extends Record<
  string,
  string | undefined
> {
  containsScene?: string;
}

Response:

interface GetSharedPlaylistsResponse {
  playlists: SharedPlaylistData[];
}

Handler: getSharedPlaylists in server/controllers/playlist.ts


GET /api/playlists

Get all user playlists

Authentication: Session

URL Parameters: Record<string, string>

Query Parameters:

interface GetUserPlaylistsQuery extends Record<
  string,
  string | undefined
> {
  containsScene?: string;
}

Response:

interface GetUserPlaylistsResponse {
  playlists: PlaylistSummary[];
}

Handler: getUserPlaylists in server/controllers/playlist.ts


GET /api/playlists/:id

Get single playlist with a page of its items

Authentication: Session

URL Parameters:

interface GetPlaylistParams extends Record<string, string> {
  id: string;
}

Query Parameters:

interface GetPlaylistQuery extends Record<string, string | undefined> {
  page?: string;
  per_page?: string;
  sort?: string;
  direction?: string;
}

Response:

interface GetPlaylistResponse {
  playlist: PlaylistData & { items: PlaylistItemWithScene[] };
  /** How many of the playlist's items the viewer can see */
  totalItems: number;
  /**
   * The owner's count of items they cannot play (hidden, restricted,
   * deleted from Stash, or on an instance they do not use); 0 for anyone
   * else, who is told nothing about them
   */
  unavailableItems: number;
  /** The page read */
  page: number;
  perPage: number;
  /**
   * The sort the items came back in: one of `PLAYLIST_ITEM_SORTS`, a random
   * one as `random_<seed>` (a bare `random` names the seed it used)
   */
  sort: string;
  direction: "ASC" | "DESC";
  isOwner: boolean;
  accessLevel: "owner" | "shared";
  sharedViaGroups?: string[];
  /** Who owns the playlist, for "Shared by" */
  owner: { id: number; username: string };
}

Handler: getPlaylist in server/controllers/playlist.ts


GET /api/playlists/:id/queue

Get the play queue: every visible item in the shown order

Authentication: Session

URL Parameters:

interface GetPlaylistParams extends Record<string, string> {
  id: string;
}

Query Parameters:

interface GetPlaylistQueueQuery extends Record<
  string,
  string | undefined
> {
  sort?: string;
  direction?: string;
}

Response:

interface GetPlaylistQueueResponse {
  entries: PlaylistQueueEntry[];
}

Handler: getPlaylistQueue in server/controllers/playlist.ts


POST /api/playlists

Create new playlist

Authentication: Session

Request Body:

interface CreatePlaylistRequest {
  name: string;
  description?: string;
}

Response:

interface CreatePlaylistResponse {
  playlist: PlaylistData;
}

Handler: createPlaylist in server/controllers/playlist.ts


PUT /api/playlists/:id

Update playlist

Authentication: Session

Request Body:

interface UpdatePlaylistRequest {
  /** Not empty once trimmed */
  name?: string;
  /** A client may send null to clear the description. */
  description?: string | null;
  shuffle?: boolean;
  repeat?: PlaylistRepeatMode;
}

URL Parameters:

interface UpdatePlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface UpdatePlaylistResponse {
  playlist: PlaylistData;
}

Handler: updatePlaylist in server/controllers/playlist.ts


DELETE /api/playlists/:id

Delete playlist

Authentication: Session

URL Parameters:

interface DeletePlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface DeletePlaylistResponse {
  success: true;
  message: string;
}

Handler: deletePlaylist in server/controllers/playlist.ts


POST /api/playlists/:id/items

Add scene to playlist

Authentication: Session

Request Body:

interface AddSceneToPlaylistRequest {
  instanceId: string;
  sceneId: string;
}

URL Parameters:

interface AddSceneToPlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface AddSceneToPlaylistResponse {
  item: {
    id: number;
    playlistId: number;
    instanceId: string;
    sceneId: string;
    position: number;
    addedAt: Date;
  };
}

Handler: addSceneToPlaylist in server/controllers/playlist.ts


POST /api/playlists/:id/items/bulk

Add several scenes to playlist

Authentication: Session

Request Body:

interface AddScenesToPlaylistRequest {
  scenes: { sceneId: string; instanceId: string }[];
}

URL Parameters:

interface AddSceneToPlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface AddScenesToPlaylistResponse {
  added: number;
  alreadyInPlaylist: number;
  /** Scenes the requester cannot see or that no longer exist */
  unavailable: number;
}

Handler: addScenesToPlaylist in server/controllers/playlist.ts


POST /api/playlists/:id/items/remove-unavailable

Remove the items deleted from Stash (owner only)

Authentication: Session

URL Parameters:

interface GetPlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface RemoveUnavailableItemsResponse {
  removed: number;
}

Handler: removeUnavailablePlaylistItems in server/controllers/playlist.ts


POST /api/playlists/:id/items/remove

Remove several items by item id (owner only)

Authentication: Session

Request Body:

interface RemovePlaylistItemsRequest {
  itemIds: number[];
}

URL Parameters:

interface GetPlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface RemovePlaylistItemsResponse {
  removed: number;
}

Handler: removePlaylistItems in server/controllers/playlist.ts


PUT /api/playlists/:id/items/:itemId/position

Move one item to an index among the items the owner sees (owner only)

Authentication: Session

Request Body:

interface MovePlaylistItemRequest {
  index: number;
}

URL Parameters:

interface MovePlaylistItemParams extends Record<string, string> {
  id: string;
  itemId: string;
}

Response:

interface MovePlaylistItemResponse {
  success: true;
}

Handler: movePlaylistItem in server/controllers/playlist.ts


DELETE /api/playlists/:id/items/:sceneId

Remove scene from playlist

Authentication: Session

URL Parameters:

interface RemoveSceneFromPlaylistParams extends Record<string, string> {
  id: string;
  sceneId: string;
}

Query Parameters:

interface RemoveSceneFromPlaylistQuery extends Record<
  string,
  string | string[] | undefined
> {
  instanceId?: string | string[];
}

Response:

interface RemoveSceneFromPlaylistResponse {
  success: true;
  message: string;
}

Handler: removeSceneFromPlaylist in server/controllers/playlist.ts


POST /api/playlists/:id/sort

Save a view sort as the playlist's order (owner only)

Authentication: Session

Request Body:

interface SortPlaylistRequest {
  sort: string;
  direction: "ASC" | "DESC";
}

URL Parameters:

interface GetPlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface SortPlaylistResponse {
  success: true;
  itemCount: number;
}

Handler: sortPlaylist in server/controllers/playlist.ts


GET /api/playlists/:id/shares

Get sharing info for a playlist

Authentication: Session

URL Parameters:

interface GetPlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface GetPlaylistSharesResponse {
  shares: PlaylistShareInfo[];
}

Handler: getPlaylistShares in server/controllers/playlist.ts


PUT /api/playlists/:id/shares

Update playlist sharing

Authentication: Session

Request Body:

interface UpdatePlaylistSharesRequest {
  groupIds: number[];
}

URL Parameters:

interface GetPlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface UpdatePlaylistSharesResponse {
  shares: PlaylistShareInfo[];
}

Handler: updatePlaylistShares in server/controllers/playlist.ts


POST /api/playlists/:id/duplicate

Duplicate a playlist

Authentication: Session

URL Parameters:

interface GetPlaylistParams extends Record<string, string> {
  id: string;
}

Response:

interface DuplicatePlaylistResponse {
  playlist: PlaylistData;
}

Handler: duplicatePlaylist in server/controllers/playlist.ts


Downloads

Scene, image and playlist downloads.

GET /api/downloads

Get all user downloads

Authentication: Session

Response:

interface GetUserDownloadsResponse {
  downloads: SerializedDownload[];
}

Handler: getUserDownloads in server/controllers/download.ts


GET /api/downloads/:id

Get specific download status

Authentication: Session

Request Body: never

URL Parameters:

interface GetDownloadStatusParams extends Record<string, string> {
  id: string;
}

Response:

interface GetDownloadStatusResponse {
  download: SerializedDownload;
}

Handler: getDownloadStatus in server/controllers/download.ts


GET /api/downloads/:id/file

Get download file

Authentication: Session

Request Body: never

URL Parameters:

interface GetDownloadFileParams extends Record<string, string> {
  id: string;
}

Handler: getDownloadFile in server/controllers/download.ts


POST /api/downloads/scene/:sceneId

Start scene download

Authentication: Session

Request Body: Partial<StartEntityDownloadRequest> | undefined

URL Parameters:

interface StartSceneDownloadParams extends Record<string, string> {
  sceneId: string;
}

Response:

interface StartSceneDownloadResponse {
  download: SerializedDownload;
}

Handler: startSceneDownload in server/controllers/download.ts


POST /api/downloads/image/:imageId

Start image download

Authentication: Session

Request Body: Partial<StartEntityDownloadRequest> | undefined

URL Parameters:

interface StartImageDownloadParams extends Record<string, string> {
  imageId: string;
}

Response:

interface StartImageDownloadResponse {
  download: SerializedDownload;
}

Handler: startImageDownload in server/controllers/download.ts


POST /api/downloads/playlist/:playlistId

Start playlist download

Authentication: Session

Request Body: never

URL Parameters:

interface StartPlaylistDownloadParams extends Record<string, string> {
  playlistId: string;
}

Response: StartPlaylistDownloadResponse | PlaylistTooLargeResponse

Handler: startPlaylistDownload in server/controllers/download.ts


DELETE /api/downloads/:id

Delete download

Authentication: Session

Request Body: never

URL Parameters:

interface DeleteDownloadParams extends Record<string, string> {
  id: string;
}

Response:

interface DeleteDownloadResponse {
  success: true;
  message: string;
}

Handler: deleteDownload in server/controllers/download.ts


POST /api/downloads/:id/retry

Retry failed download

Authentication: Session

Request Body: never

URL Parameters:

interface RetryDownloadParams extends Record<string, string> {
  id: string;
}

Response:

interface RetryDownloadResponse {
  download: SerializedDownload;
}

Handler: retryDownload in server/controllers/download.ts


Ratings

The user's ratings and favorites.

PUT /api/ratings/scene/:sceneId

Authentication: Session

Handler: updateSceneRating in server/controllers/ratings.ts


PUT /api/ratings/performer/:performerId

Authentication: Session

Handler: updatePerformerRating in server/controllers/ratings.ts


PUT /api/ratings/studio/:studioId

Authentication: Session

Handler: updateStudioRating in server/controllers/ratings.ts


PUT /api/ratings/tag/:tagId

Authentication: Session

Handler: updateTagRating in server/controllers/ratings.ts


PUT /api/ratings/gallery/:galleryId

Authentication: Session

Handler: updateGalleryRating in server/controllers/ratings.ts


PUT /api/ratings/group/:groupId

Authentication: Session

Handler: updateGroupRating in server/controllers/ratings.ts


PUT /api/ratings/image/:imageId

Authentication: Session

Handler: updateImageRating in server/controllers/ratings.ts


Watch History

Plays, resume points and O counts of scenes.

POST /api/watch-history/save-activity

Save activity (called by track-activity plugin every 10 seconds)

Authentication: Session

Request Body:

interface SaveActivityRequest {
  /** The scene's instance: required, the server never guesses one */
  instanceId: string;
  sceneId: string;
  resumeTime?: number;
  playDuration?: number;
}

Response:

interface SaveActivityResponse {
  success: true;
  watchHistory: WatchHistoryData;
}

Handler: saveActivity in server/controllers/watchHistory.ts


POST /api/watch-history/increment-play-count

Increment play count (called when minimum play percentage reached)

Authentication: Session

Request Body:

interface IncrementPlayCountRequest {
  /** The scene's instance: required, the server never guesses one */
  instanceId: string;
  sceneId: string;
  /**
   * One viewing's token (1 to 64 characters), the same on every retry of its
   * request: the server counts a token once for 10 minutes. A request
   * without one always counts.
   */
  playToken?: string;
}

Response:

interface IncrementPlayCountResponse {
  success: true;
  watchHistory: WatchHistoryData;
}

Handler: incrementPlayCount in server/controllers/watchHistory.ts


POST /api/watch-history/increment-o

Increment O counter

Authentication: Session

Request Body:

interface IncrementOCounterRequest {
  instanceId: string;
  sceneId: string;
}

Response:

interface IncrementOCounterResponse {
  success: true;
  oCount: number;
  timestamp: string;
}

Handler: incrementOCounter in server/controllers/watchHistory.ts


POST /api/watch-history/decrement-o

Remove the newest O ("Remove last O")

Authentication: Session

Request Body:

interface DecrementOCounterRequest {
  instanceId: string;
  sceneId: string;
}

Response:

interface DecrementOCounterResponse {
  success: true;
  oCount: number;
}

Handler: decrementOCounter in server/controllers/watchHistory.ts


DELETE /api/watch-history

Clear all watch history for current user

Authentication: Session

Response:

interface ClearAllWatchHistoryResponse {
  success: true;
  deletedCounts: {
    watchHistory: number;
    performerStats: number;
    studioStats: number;
    tagStats: number;
    rankings: number;
  };
  message: string;
}

Handler: clearAllWatchHistory in server/controllers/watchHistory.ts


GET /api/watch-history/scenes

The viewer's watched scenes, paged, sorted and filtered in SQL (before /:sceneId, which would take "scenes" as an id)

Authentication: Session

URL Parameters: Record<string, string>

Query Parameters:

interface GetWatchedScenesQuery extends Record<
  string,
  string | undefined
> {
  /** A `WatchedScenesView`; default `all` */
  view?: string;
  /** A `WatchedScenesSort`; default `recent` */
  sort?: string;
  /** Default 1 */
  page?: string;
  /** 1 to 250; default 24 */
  per_page?: string;
  /** `false` skips the totals (both answer null); default `true` */
  count?: string;
}

Response:

interface GetWatchedScenesResponse {
  scenes: NormalizedScene[];
  /** Every scene in the view; null when the request sent `count=false` */
  total: number | null;
  /** Seconds watched over every scene in the view; null with `count=false` */
  totalPlayDuration: number | null;
}

Handler: getWatchedScenes in server/controllers/watchHistory.ts


GET /api/watch-history/:sceneId

Get watch history for specific scene

Authentication: Session

URL Parameters:

interface GetWatchHistoryParams extends Record<string, string> {
  sceneId: string;
}

Response:

interface GetWatchHistoryResponse {
  exists: boolean;
  resumeTime: number | null;
  playCount: number;
  playDuration?: number;
  lastPlayedAt?: Date | null;
  oCount: number;
  oHistory?: string[];
  playHistory?: string[];
}

Handler: getWatchHistory in server/controllers/watchHistory.ts


Image View History

Image views and O counts.

POST /api/image-view-history/increment-o

Increment O counter for image

Authentication: Session

Request Body:

interface IncrementImageOCounterRequest {
  instanceId: string;
  imageId: string;
}

Response:

interface IncrementImageOCounterResponse {
  success: true;
  oCount: number;
  timestamp: string;
}

Handler: incrementImageOCounter in server/controllers/imageViewHistory.ts


POST /api/image-view-history/decrement-o

Remove the newest O ("Remove last O")

Authentication: Session

Request Body:

interface DecrementImageOCounterRequest {
  instanceId: string;
  imageId: string;
}

Response:

interface DecrementImageOCounterResponse {
  success: true;
  oCount: number;
}

Handler: decrementImageOCounter in server/controllers/imageViewHistory.ts


POST /api/image-view-history/view

Record image view (when opened in Lightbox)

Authentication: Session

Request Body:

interface RecordImageViewRequest {
  instanceId: string;
  imageId: string;
}

Response:

interface RecordImageViewResponse {
  success: true;
  viewCount: number;
  lastViewedAt: Date | null;
}

Handler: recordImageView in server/controllers/imageViewHistory.ts


GET /api/image-view-history/:imageId

Get view history for specific image

Authentication: Session

URL Parameters:

interface GetImageViewHistoryParams extends Record<string, string> {
  imageId: string;
}

Response:

interface GetImageViewHistoryResponse {
  exists: boolean;
  viewCount: number;
  viewHistory?: string[];
  oCount: number;
  oHistory?: string[];
  lastViewedAt?: Date | null;
}

Handler: getImageViewHistory in server/controllers/imageViewHistory.ts


User Stats

The user's own statistics.

GET /api/user-stats

Get user stats

Authentication: Session

Response:

interface UserStatsResponse {
  library: LibraryStats;
  engagement: EngagementStats;
  topScenes: TopScene[];
  topPerformers: TopPerformer[];
  /** Performers of the viewer's partner genders; [] unless they set some */
  topPartners: TopPerformer[];
  topStudios: TopStudio[];
  topTags: TopTag[];
  mostWatchedScene: HighlightScene | null;
  mostViewedImage: HighlightImage | null;
  mostOdScene: HighlightScene | null;
  mostOdPerformer: HighlightPerformer | null;
}

Handler: getUserStats in server/controllers/userStats.ts


Carousels

Custom home page carousels.

GET /api/carousels

Get all user's custom carousels

Authentication: Session

Response:

interface GetUserCarouselsResponse {
  carousels: CarouselData[];
}

Handler: getUserCarousels in server/controllers/carousel.ts


POST /api/carousels/preview

Preview carousel results without saving. Scenes are listed only from the user's instances: none ready yet answers 503 ready:false, as the lists do

Authentication: Session

Request Body:

interface PreviewCarouselRequest {
  rules: CarouselRulesInput;
  sort?: string;
  direction?: string;
}

Response:

interface PreviewCarouselResponse {
  scenes: WithStashUrl<NormalizedScene>[];
}

Handler: previewCarousel in server/controllers/carousel.ts


GET /api/carousels/:id

Get single carousel by ID

Authentication: Session

URL Parameters:

interface GetCarouselParams extends Record<string, string> {
  id: string;
}

Response:

interface GetCarouselResponse {
  carousel: CarouselData;
}

Handler: getCarousel in server/controllers/carousel.ts


GET /api/carousels/:id/execute

Execute carousel query and get scenes (503 ready:false as above)

Authentication: Session

URL Parameters:

interface ExecuteCarouselByIdParams extends Record<string, string> {
  id: string;
}

Response:

interface ExecuteCarouselByIdResponse {
  carousel: {
    id: string;
    title: string;
    icon: string;
  };
  scenes: WithStashUrl<NormalizedScene>[];
}

Handler: executeCarouselById in server/controllers/carousel.ts


POST /api/carousels

Create new carousel

Authentication: Session

Request Body:

interface CreateCarouselRequest {
  title: string;
  icon?: string;
  rules: CarouselRulesInput;
  sort?: string;
  direction?: string;
}

Response:

interface CreateCarouselResponse {
  carousel: CarouselData;
}

Handler: createCarousel in server/controllers/carousel.ts


PUT /api/carousels/:id

Update carousel

Authentication: Session

Request Body:

interface UpdateCarouselRequest {
  title?: string;
  icon?: string;
  rules?: CarouselRulesInput;
  sort?: string;
  direction?: string;
}

URL Parameters:

interface UpdateCarouselParams extends Record<string, string> {
  id: string;
}

Response:

interface UpdateCarouselResponse {
  carousel: CarouselData;
}

Handler: updateCarousel in server/controllers/carousel.ts


DELETE /api/carousels/:id

Delete carousel

Authentication: Session

URL Parameters:

interface DeleteCarouselParams extends Record<string, string> {
  id: string;
}

Response:

interface DeleteCarouselResponse {
  success: true;
  message: string;
}

Handler: deleteCarousel in server/controllers/carousel.ts


Custom Themes

Custom themes.

GET /api/themes/custom

Get all user custom themes

Authentication: Session

Response:

interface GetUserCustomThemesResponse {
  themes: CustomThemeData[];
}

Handler: getUserCustomThemes in server/controllers/customTheme.ts


GET /api/themes/custom/:id

Get single custom theme

Authentication: Session

URL Parameters:

interface GetCustomThemeParams extends Record<string, string> {
  id: string;
}

Response:

interface GetCustomThemeResponse {
  theme: CustomThemeData & { userId: number };
}

Handler: getCustomTheme in server/controllers/customTheme.ts


POST /api/themes/custom

Create new custom theme

Authentication: Session

Request Body:

interface CreateCustomThemeRequest {
  name: string;
  config: ThemeConfig;
}

Response:

interface CreateCustomThemeResponse {
  theme: CustomThemeData & { userId: number };
}

Handler: createCustomTheme in server/controllers/customTheme.ts


PUT /api/themes/custom/:id

Update custom theme

Authentication: Session

Request Body:

interface UpdateCustomThemeRequest {
  name?: string;
  config?: ThemeConfig;
}

URL Parameters:

interface UpdateCustomThemeParams extends Record<string, string> {
  id: string;
}

Response:

interface UpdateCustomThemeResponse {
  theme: CustomThemeData & { userId: number };
}

Handler: updateCustomTheme in server/controllers/customTheme.ts


DELETE /api/themes/custom/:id

Delete custom theme

Authentication: Session

URL Parameters:

interface DeleteCustomThemeParams extends Record<string, string> {
  id: string;
}

Response:

interface DeleteCustomThemeResponse {
  success: true;
}

Handler: deleteCustomTheme in server/controllers/customTheme.ts


POST /api/themes/custom/:id/duplicate

Duplicate custom theme

Authentication: Session

URL Parameters:

interface DuplicateCustomThemeParams extends Record<string, string> {
  id: string;
}

Response:

interface DuplicateCustomThemeResponse {
  theme: CustomThemeData & { userId: number };
}

Handler: duplicateCustomTheme in server/controllers/customTheme.ts


Sync

Syncing the library cache from Stash (admin only).

GET /api/sync/status

Whether a sync runs, the sync settings, and every configured instance's entity sync states (admin only: only the Server settings tab shows them). Instances appear by id and name, never by address.

Authentication: Admin

Response:

interface SyncStatusResponse {
  /** A sync is running. */
  inProgress: boolean;
  /** What holds the lock, if anything: a sync or an instance's removal. */
  activeJob: SyncJob | null;
  settings: {
    syncIntervalMinutes: number;
    enableScanSubscription: boolean;
  };
  /** Every configured instance, enabled or not, in priority order. */
  instances: SyncInstanceStatus[];
}

Handler: inline in server/routes/sync.ts


POST /api/sync/trigger

Manually trigger a sync (admin only)

Body: { type?: 'full' | 'incremental' } Default: incremental

Authentication: Admin

Handler: inline in server/routes/sync.ts


POST /api/sync/abort

Abort the current sync (admin only)

Authentication: Admin

Handler: inline in server/routes/sync.ts


POST /api/sync/cleanup

The sync status's "Apply deletions" (admin only): one type's cleanup on one enabled instance, without the ratio guard, after a cleanup refused to soft-delete more than half of the type. It runs in the background under the sync lock, so it answers 409 while a sync or an instance deletion runs; its outcome goes to the type's lastError.

Body: { instanceId: string, entityType: "tag" | "studio" | ... }

Authentication: Admin

Request Body: Partial<ApplyDeletionsRequest> | undefined

Response:

interface ApplyDeletionsResponse {
  ok: true;
  message: string;
}

Handler: inline in server/routes/sync.ts


POST /api/sync/reprobe-clips

Re-probe clips that were synced before previews were generated (admin only)

Body: { instanceId?: string } If instanceId is not provided, uses the first enabled instance

Authentication: Admin

Handler: inline in server/routes/sync.ts


PUT /api/sync/settings

Update sync settings (admin only). A new interval re-arms the scheduler's timer; no sync starts, so the answer comes at once.

Body: { syncIntervalMinutes?: number, enableScanSubscription?: boolean }

Authentication: Admin

Handler: inline in server/routes/sync.ts


Exclusions

Recomputing the content-restriction exclusions (admin only).

POST /api/exclusions/recompute/:userId

Recompute exclusions for a single user (admin only)

Authentication: Admin

Handler: inline in server/routes/exclusions.ts


POST /api/exclusions/recompute-all

Recompute exclusions for all users (admin only)

Authentication: Admin

Handler: inline in server/routes/exclusions.ts


GET /api/exclusions/stats

Get exclusion statistics per user and entity type (admin only)

Authentication: Admin

Handler: inline in server/routes/exclusions.ts


Admin

Merge reconciliation and database backups (admin only).

GET /api/admin/orphaned-scenes

List all orphaned scenes with user activity

Authentication: Admin

Handler: inline in server/routes/mergeReconciliation.ts


GET /api/admin/orphaned-scenes/:ref/matches

Live scenes of the orphan's instance with a matching phash

Authentication: Admin

Handler: inline in server/routes/mergeReconciliation.ts


POST /api/admin/orphaned-scenes/:ref/reconcile

Transfer user data from the orphan to targetSceneId, a scene id on the orphan's instance. A target that is not a live scene there answers 400.

Authentication: Admin

Handler: inline in server/routes/mergeReconciliation.ts


POST /api/admin/orphaned-scenes/:ref/discard

Delete the orphan's history, ratings and playlist entries (on its instance only)

Authentication: Admin

Handler: inline in server/routes/mergeReconciliation.ts


POST /api/admin/reconcile-all

Reconcile every orphan with exactly one phash match on its instance, as sync does; orphans with several matches are left for the admin to pick.

Authentication: Admin

Handler: inline in server/routes/mergeReconciliation.ts


GET /api/admin/database/backups

List all database backups

Authentication: Admin

Response:

interface ListDatabaseBackupsResponse {
  backups: DatabaseBackup[];
  /** The directory backups are written to and listed from. */
  directory: string;
}

Handler: inline in server/routes/databaseBackup.ts


POST /api/admin/database/backup

Create a new database backup

Authentication: Admin

Response:

interface CreateDatabaseBackupResponse {
  backup: DatabaseBackup;
}

Handler: inline in server/routes/databaseBackup.ts


DELETE /api/admin/database/backups/:filename

Delete a specific backup

Authentication: Admin

Response:

interface DeleteDatabaseBackupResponse {
  ok: true;
}

Handler: inline in server/routes/databaseBackup.ts