API · Endpoints

Files

REST endpoints for the Files surface. Upload (direct or server-proxied), list with sort / search / starred / recent views, rename, move, archive, restore, share tokens, versioned replacement, bulk zip download, server-side code preview, and per-file metadata (starred · tags · description · EXIF readout). Comments thread on files via the universal /api/workspaces/{slug}/comments endpoint with target.type = "file".

All endpoints below sit under /api/workspaces/{slug}/files. Every route is gated by the files-surface flag plus workspace membership; archived files reject as 404 from any write endpoint.

Upload

Two paths. Pick the direct-to-blob path for most files; pick the proxy path when the upload needs server-side processing (SVG sanitization, image EXIF strip).

Direct upload

POST /api/workspaces/{slug}/files
Content-Type: application/json
Authorization: Bearer <api_key>

# Body: HandleUploadBody from @vercel/blob/client.
# clientPayload (string): JSON-encoded:
{
  "surfaceSlug": "library",
  "parentFolderId": null,
  "name": "hero.png",
  "size": 248192,
  "contentType": "image/png"
}

Returns a one-shot upload URL. The client streams bytes to Vercel Blob; the server fires file.uploaded through onUploadCompleted after the row lands.

Proxy upload

POST /api/workspaces/{slug}/files/proxy-upload
Content-Type: multipart/form-data

surface_slug=library
parent_folder_id=  # or a folder id
file=<binary>

Capped at 25 MB by the serverless body limit. Use this path for SVG uploads (DOMPurify sanitization) or raster images where you want the server to strip EXIF + generate a 256-px webp thumbnail before persisting.

List

GET /api/workspaces/{slug}/files
  ?surfaceSlug=library
  &parentFolderId=null
  &view=                # null (default) | "starred" | "recent"
  &q=                   # search query, surface-wide when non-empty
  &sort=                # name | name_desc | newest | oldest | largest | smallest
  &order=               # asc | desc (advanced override)

Returns { folders: Folder[], files: File[] }. Folders are omitted from starred and recent views (those are flat surface-wide lists). Each file carries a thumbUrl (server- generated 256-px webp for raster types, null otherwise), starred, starredAt, description, tags, and exif (width, height, orientation for raster uploads).

Rename / move / restore

PATCH /api/workspaces/{slug}/files/{fileId}
{ "name": "renamed.png" }            # rename
{ "parentFolderId": "fld_..." }      # move into folder
{ "parentFolderId": null }           # move to root
{ "restore": true }                  # restore a soft-deleted file

Rename keeps the blob key stable so deep-links never break. Move rejects cross-surface targets. Restore + name in the same body is rejected; do them in separate PATCH calls.

Archive (soft delete)

DELETE /api/workspaces/{slug}/files/{fileId}

# Soft-delete. Sets archivedAt + archivedByPrincipal*; blob stays
# in storage and the org's quota counter stays incremented until
# the 30-day trash cleanup cron hard-deletes it. Restorable via
# the PATCH endpoint above.

Metadata (starred · description · tags)

PATCH /api/workspaces/{slug}/files/{fileId}/metadata
{
  "starred": true,                      # toggle star
  "description": "Hero asset for v2",   # null clears; max 2000 chars
  "tags": ["Design", "Hero"]            # max 16 × 32 chars
}

Tags are normalised server-side: lowercased, trimmed, de-duplicated, empties dropped. Empty description clears the field. Each call accepts any subset of the three keys.

Share tokens (public anonymous read)

POST   /api/workspaces/{slug}/files/{fileId}/share
       # mint a token; returns { id, url, createdAt }

GET    /api/workspaces/{slug}/files/{fileId}/share
       # list active (non-revoked) tokens

DELETE /api/workspaces/{slug}/files/{fileId}/share?tokenId=...
       # soft-revoke (sets revokedAt); idempotent

Each token is 32 random bytes (URL-safe base64, ~256 bits of entropy). Public consumption happens at /share/files/{token} — a server-rendered page with the preview + a Download button.

Versions (replace bytes, keep id)

POST /api/workspaces/{slug}/files/{fileId}/versions
Content-Type: multipart/form-data

file=<binary>

# Replaces the file bytes. Same File.id, same comments, same
# share tokens, same deep-link URL. New blobKey + new sha256
# under the hood. MIME category must match the previous version
# (image → image, video → video, etc.); cross-category swaps
# reject 400.

Bulk zip download

POST /api/workspaces/{slug}/files/zip
{
  "fileIds": ["fil_a", "fil_b", "fil_c"],
  "surfaceSlug": "library"
}

# 200 OK
# Content-Type: application/zip
# Content-Disposition: attachment; filename="library.zip"

Capped at 50 files per request and 500 MB total uncompressed. Duplicate filenames are deduped server-side (photo.jpg, photo (2).jpg) so extractors don't silently overwrite.

Code preview

GET /api/workspaces/{slug}/files/{fileId}/preview

# Returns the raw text bytes for small (≤256 KB) text-like files
# so the client can render syntax-highlighted code. MIME must be
# in PREVIEW_TEXT_MIMES (text/*, application/json, yaml, xml, js,
# ts, sh, etc). Non-text MIME or oversize rejects 400.

Comments on a file

POST /api/workspaces/{slug}/comments
{
  "target": { "type": "file", "fileId": "fil_..." },
  "body": "Replace the hero, the contrast is too low.",
  "mentions": [
    { "kind": "user", "id": "usr_...", "label": "Govind" }
  ]
}

File-targeted comments use the universal /comments endpoint. See /docs/api/comments for the full target catalog, replies, resolve / unresolve, reactions, and inbox routing rules (the file uploader is the default target author for the notification fan-out).

Webhook events

Every files mutation route emits a workspace-scoped event the webhook + SSE fan-out picks up. Subscribers wire onto the org webhook with the action name:

  • file.uploaded — new file row landed
  • file.renamed, file.moved
  • file.archived, file.restored
  • file.version_replaced — bytes replaced via POST /versions
  • file.starred, file.tagged, file.described
  • file.shared, file.share_revoked
  • folder.created, folder.archived

Each payload carries fileId, name, and surfaceId so subscribers can deep-link or fetch metadata without a second request. file.shared adds shareTokenId + url. file.version_replaced carries the previous and new sha256.

Storage quota

Quota is org-scoped, not per-workspace. Free orgs get 5 GB, Pro 100 GB, Scale 500 GB. The upload pre-flight rejects a 413 when the bundled size would exceed the cap; in-flight uploads commit through an atomic Org.storageUsedBytes increment so concurrent uploads can't race past the cap.