API Reference

REST endpoints, chunked uploads, streaming previews, background jobs, and WebSocket messages.

API Reference

BoxBox exposes a REST API, streaming endpoints, and a WebSocket endpoint under /api/v1.

Base URLs

http://localhost:8080/api/v1   # if Docker maps 8080 -> container port 8080
http://localhost/api/v1        # inside a reverse proxy host

Health checks are available at both /health and /api/v1/health.

Authentication

All API routes except auth and health require a JWT access token.

Login

POST /api/v1/auth/login
Content-Type: application/json

{
  "username": "admin",
  "password": "your-password"
}

Response:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "expiresAt": "2026-05-13T10:15:00Z"
}

The refresh token is not in the body. The server sets it as the boxbox_refresh cookie (HttpOnly, SameSite=Strict, path /api/v1/auth, Secure over HTTPS). Send requests with cookies enabled so refresh and logout can use it.

Login creates a new session and saves it before returning the token pair. If the save fails before publication, login returns 500 without a token pair or Set-Cookie header. The browser's existing refresh cookie remains unchanged.

Use an Access Token

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Refresh

POST /api/v1/auth/refresh

The request has no body. The server reads the boxbox_refresh cookie, checks that its session is active and that the token is the session's current refresh token, then returns a new accessToken and expiresAt and rotates the refresh cookie. The access token keeps working until its own expiry while the session remains active.

Logout

POST /api/v1/auth/logout

The request has no body. Logout needs a valid current refresh cookie. It does not need an access-token header. Logout removes the entire session, so all access tokens issued by that session stop working, including tokens from earlier refresh rotations. Other logins for the same account remain active. A missing or invalid refresh cookie returns 401 TOKEN_INVALID and clears the cookie.

A still-unexpired refresh token that another request has replaced returns 409 REFRESH_SUPERSEDED from refresh or logout. The response contains no token pair and no Set-Cookie header, preserving the replacement cookie shared by browser tabs. Retry with the current cookie. A repeated conflict is retryable, not proof that the session has ended.

Login, refresh, and logout save session state before reporting success. Publication occurs at the file rename. Success requires syncing and closing the file, renaming it, and syncing the parent directory. Before rename, a failed write returns 500 INTERNAL_ERROR without publishing a token pair or a Set-Cookie header. Refresh and logout preserve the existing cookie so the client can retry. Login does not clear an unrelated cookie.

If a durability step fails after rename, the server keeps the installed in-memory state and returns 401 SESSION_RECOVERY_REQUIRED without a token pair. Refresh and logout clear the cookie. Login sends no new cookie and does not clear an unrelated existing cookie. Sign in again after this error. Logout did not report success.

Every access and refresh token must name an active session. If the registry file is missing at startup, or a valid version 1 registry has an empty sessions map, the server starts with no active sessions and previously issued tokens receive 401. A zero-byte or malformed file fails startup. Deleting the registry while the server is running does not clear its in-memory sessions; an access token can still receive 200, and a later successful write can persist the registry again.

Tokens without a session ID, including legacy tokens, are rejected. The server does not enroll them automatically.

Files

List Mount Points

GET /api/v1/files
{
  "roots": [
    { "name": "home", "readOnly": false },
    { "name": "backups", "readOnly": true }
  ]
}

Drive Stats

GET /api/v1/files/stats
{
  "drives": [
    {
      "name": "home",
      "path": "/home/user",
      "device": "/dev/sda2",
      "fsType": "ext4",
      "mountPoint": "/",
      "totalBytes": 250000000000,
      "freeBytes": 100000000000,
      "usedBytes": 150000000000,
      "usedPct": 60,
      "readOnly": false
    }
  ]
}

List a Directory

GET /api/v1/files/list/{path}

{path} starts with the mount name, for example home/projects.

The response contains every entry of the directory in one request, hidden entries included, in no particular order. Clients reconstruct each entry path as path + "/" + name, then sort and filter it. Refresh replaces that listing rather than appending pages. Directories with more than 10,000 entries return 413.

Response:

{
  "path": "home/projects",
  "items": [
    {
      "name": "BoxBox",
      "size": 4096,
      "isDir": true,
      "modTime": "2026-05-13T09:00:00Z",
      "permissions": "drwxr-xr-x"
    }
  ]
}

Get File Info

GET /api/v1/files/{path}

If the path is a file, the response is one FileInfo object. If it is a directory, the response is a FileList.

Create Directory

POST /api/v1/files/{basePath}
Content-Type: application/json

{
  "name": "new-folder"
}

Rename or Move

PUT /api/v1/files/{oldPath}
Content-Type: application/json

{
  "newPath": "home/projects/renamed.txt"
}

Delete

DELETE /api/v1/files/{path}

Directories require confirmation:

DELETE /api/v1/files/{path}?confirm=true

Streaming

Download

GET /api/v1/stream/download/{path}

The download endpoint supports HTTP range requests.

Folder Archive

GET /api/v1/stream/archive/{folderPath}

Requires authentication and streams the folder as an application/zip attachment.

Preview

GET /api/v1/stream/preview/{path}

Preview streams inline content for images, PDFs, audio, video, and text/code previews.

Chunked Upload

POST /api/v1/stream/upload/{destinationPath}
Content-Type: application/octet-stream
X-Upload-ID: upload_123
X-Chunk-Index: 0
X-Total-Chunks: 12
X-Chunk-Size: 10485760
X-Total-Size: 120000000
X-Checksum: sha256:abc123...

[binary chunk body]

X-Checksum is optional and normally sent on the final chunk.

Response:

{
  "uploadId": "upload_123",
  "chunkIndex": 0,
  "receivedChunks": 1,
  "totalChunks": 12,
  "complete": false
}

Final response:

{
  "uploadId": "upload_123",
  "chunkIndex": 11,
  "receivedChunks": 12,
  "totalChunks": 12,
  "complete": true,
  "path": "home/uploads/archive.zip"
}

Upload Status

GET /api/v1/stream/upload/status/?uploadId=upload_123

Share links expose an existing file or folder through a token URL. Management endpoints require a JWT; recipient endpoints are public and authenticate by share token alone.

Create a Share

POST /api/v1/shares
Content-Type: application/json

{
  "path": "home/projects/shared",
  "permissions": { "view": true, "download": true, "upload": true, "delete": false, "manage": false },
  "maxUploadBytes": 104857600,
  "expiresInSeconds": 3600
}

expiresInSeconds is optional; omit it for a share that never expires. Folder links always allow viewing and downloading. Set upload to allow new files, and set both upload and delete to allow replacing or removing items below the shared folder. The optional maxUploadBytes value is a per-file cap; omitting it uses the server's max_upload_mb limit. The server cap always applies. File links always use view/download permissions. Share responses include the effective canReplace capability; it is true when delete is allowed or a persisted legacy write permission retains replacement access. For existing v1 clients, deprecated write mirrors upload so upload-only links remain usable; new clients should use upload, delete, and canReplace. Requests cannot set canReplace.

Full-access folder links set manage, upload, and delete to true. manage permits folder creation and empty file edits. File links cannot grant these capabilities.

Response uses 201 Created:

{
  "id": "b0c1d2e3-...",
  "token": "7nArUufciyjMLLFstZGU_zIerVgTpr2Qr2eL1lUTJFY",
  "url": "/s/7nArUufciyjMLLFstZGU_zIerVgTpr2Qr2eL1lUTJFY",
  "fileName": "shared",
  "permissions": { "view": true, "download": true, "upload": true, "delete": false, "manage": false, "canReplace": false, "write": true },
  "maxUploadBytes": 104857600,
  "isFolder": true,
  "createdAt": "2026-09-02T09:00:00Z",
  "expiresAt": "2026-09-02T10:00:00Z"
}

List Active Shares

GET /api/v1/shares

Returns only the authenticated caller's active (non-revoked, non-expired) shares. Each entry includes the token, url, fileName, mount-relative path, permissions, maxUploadBytes, isFolder, and expiry.

Update a Folder Share

PATCH /api/v1/shares/{id}
Content-Type: application/json

{
  "permissions": { "view": true, "download": true, "upload": true, "delete": true, "manage": true },
  "maxUploadBytes": 52428800
}

Updates permissions and the per-file upload cap on an active folder share owned by the caller. delete requires upload, and manage requires both. A value of 0 for maxUploadBytes restores the server's configured maximum.

Revoke a Share

DELETE /api/v1/shares/{id}

Unknown ids and links owned by another user return 404. Revoked links stop working immediately.

Recipient Endpoints

Recipient endpoints are public (no JWT) and rate-limited per client IP. The share token in the path is the only credential.

GET /api/v1/share/{token}

Returns recipient-facing metadata only; mount names and internal paths are never included:

{
  "fileName": "shared",
  "size": 1024,
  "mimeType": "application/octet-stream",
  "permissions": { "view": true, "download": true, "upload": true, "delete": false, "manage": false, "canReplace": false, "write": true },
  "maxUploadBytes": 104857600,
  "isFolder": true,
  "expiresAt": "2026-09-02T10:00:00Z"
}
GET /api/v1/share/{token}/download
GET /api/v1/share/{token}/preview
GET /api/v1/share/{token}/items?path=reports
GET /api/v1/share/{token}/archive?path=reports
GET /api/v1/share/{token}/wallpaper
POST /api/v1/share/{token}/folders?path=reports/new-folder
POST /api/v1/share/{token}/upload?path=reports/new.txt
DELETE /api/v1/share/{token}/items?path=reports/old.txt

Folder item paths are relative to the share root. Archive downloads include only paths inside that root. Delete requires the delete permission and cannot remove the shared root. Download streams a file as an attachment; preview streams it inline for browser media. Both support HTTP range requests, set a sandboxed Content-Security-Policy, and force attachment disposition for active document formats (HTML, SVG, XML).

Unknown, expired, and revoked tokens all return the same 404, so recipients cannot distinguish between them. Requests the share's permissions do not allow return 403.

Folder creation requires manage and a writable mount. It creates missing parents below the shared root and returns 201 with {"success": true}. Traversal outside the shared root is denied.

The wallpaper endpoint serves the owner's image with the sandboxed content policy and returns 404 when no image is stored. Share metadata optionally includes wallpaper with mode, frostedGlass, and a numeric version for cache invalidation.

Upload Through a Folder Share

POST /api/v1/share/{token}/upload?path=reports/new.txt
Content-Type: application/octet-stream

[file body]

Requires the upload permission and a writable mount point. Upload-only links create new files but cannot replace an existing item; links with delete can also replace or delete items below the shared folder. Uploads are atomically published and capped by both the per-link maxUploadBytes and server max_upload_mb limits. If a revocation completes while the body is still uploading, the pending upload is discarded:

{
  "fileName": "notes.txt",
  "size": 2048
}

403 means the share does not allow this operation; 413 means the body exceeded a size limit. Existing persisted write links retain their historical replacement behavior but do not gain delete permission.

A zero-byte body can empty an existing file when the link has manage permission. Other zero-byte uploads are rejected. The server rechecks permissions and revocation before publishing the edit.

GET /api/v1/search?path=home&q=invoice
{
  "path": "home",
  "query": "invoice",
  "results": [],
  "count": 0
}

Both path and q are required.

Jobs

Background jobs handle copy, move, and delete operations.

GET /api/v1/jobs
GET /api/v1/jobs/{id}
DELETE /api/v1/jobs/{id}

Create a job:

POST /api/v1/jobs
Content-Type: application/json

{
  "type": "copy",
  "sourcePath": "home/movie.mkv",
  "destPath": "backups/movie.mkv"
}

Valid job types are copy, move, and delete. Copy and move require destPath.

Create response uses 202 Accepted:

{
  "id": "job_abc123",
  "type": "copy",
  "state": "pending",
  "progress": 0,
  "sourcePath": "home/movie.mkv",
  "destPath": "backups/movie.mkv",
  "createdAt": "2026-05-13T09:00:00Z"
}

System and Settings

System drives:

GET /api/v1/system/drives

Upload settings:

GET /api/v1/system/upload
{
  "chunkSizeBytes": 10485760
}

Custom drive names:

GET /api/v1/settings/drive-names
PUT /api/v1/settings/drive-names
DELETE /api/v1/settings/drive-names/{mountPoint}

Set a drive name:

{
  "mountPoint": "home",
  "customName": "Main Home"
}

Share-page wallpaper

Wallpaper settings endpoints require a JWT and operate on the authenticated user's wallpaper:

GET /api/v1/settings/wallpaper
PUT /api/v1/settings/wallpaper?mode=cover&frostedGlass=false&source=client-reference
PATCH /api/v1/settings/wallpaper
DELETE /api/v1/settings/wallpaper

PUT accepts a raw JPEG, PNG, GIF, WebP, AVIF, or SVG image up to 20 MiB. source is an optional client reference of at most 2,048 bytes. Display modes are cover, contain, stretch, center, and tile.

GET, PUT, and PATCH return mode, frostedGlass, source, sha256, contentType, and updatedAt. GET and PATCH return 404 when no wallpaper exists. PATCH accepts {"mode": "contain", "frostedGlass": true}. DELETE succeeds even when the wallpaper is already absent.

The browser syncs its wallpaper after login and explicit settings saves. An empty browser does not remove a stored wallpaper on login. Logout and session expiry cancel pending synchronization.

WebSocket

Connect with a token in the query string or Authorization header:

ws://localhost:8080/api/v1/ws?token=eyJhbGciOiJIUzI1NiIs...

Client messages:

{ "type": "subscribe", "jobId": "job_abc123" }
{ "type": "unsubscribe", "jobId": "job_abc123" }
{ "type": "ping" }

Server messages:

{
  "type": "job_update",
  "payload": {
    "jobId": "job_abc123",
    "state": "running",
    "progress": 45
  }
}

Terminal jobs use job_complete. Errors use error; ping replies use pong.

WebSocket authentication is checked at handshake. Logout does not close a connection that was already established.

Errors

Errors use:

{
  "error": "Path is required",
  "code": "VALIDATION_ERROR",
  "details": ""
}

Common codes include VALIDATION_ERROR, UNAUTHORIZED, TOKEN_INVALID, SESSION_RECOVERY_REQUIRED, ACCESS_DENIED, READ_ONLY, INVALID_PATH, NOT_FOUND, CONFLICT, JOB_NOT_FOUND, CHECKSUM_MISMATCH, and INTERNAL_ERROR.

Auth storage errors before rename return 500 with INTERNAL_ERROR and no token pair or Set-Cookie header. Refresh and logout preserve the existing cookie for retry. Login does not clear an unrelated cookie. SESSION_RECOVERY_REQUIRED returns 401 if a durability step fails after rename. It has no token pair; refresh and logout clear the cookie, while login does not clear an unrelated cookie. Logout does not count as successful in this case.