.dev
REST API

Storage

Image and video uploads plus the per-user file browser — list, upload, move, delete and inspect files on the storage CDN.

Files live in object storage (Cloudflare R2) behind a CDN. Every user gets a private namespace — all keys start with u/{userId}/ — and uploads come back as absolute CDN URLs (e.g. https://storage.feeef.net/u/{userId}/…). Processed videos are served from https://storage.feeef.org/videos/….

There are three surfaces:

SurfaceBasePurpose
Image upload/v1/services/storage/uploadOptimized image ingestion (resize, quality, format)
File browser/v1/services/filesMedia library: list, upload any type, move, delete, metadata
Video upload/storage/upload (server root — see below)Multi-quality video processing

At a glance

Image upload

MethodPathDescriptionAuth
POST/v1/services/storage/uploadMultipart image upload, returns { "url": "…" }Bearer

Multipart fields: file (required), folder, width, height, quality, fit (cover, contain, fill, inside, outside). Limits: 20 MB, extensions jpg jpeg png webp gif avif tiff svg. width/height below 300 are clamped up to 300. The folder is always treated as relative and stored under u/{userId}/ (default uploads/any) — don't pass a u/… prefix here.

File browser

The media library API. All paths are validated against your u/{userId}/ namespace; relative folders are prefixed automatically on single upload.

MethodPathDescriptionAuth
GET/v1/services/filesList one directory level (path, recursive, paginationToken, limit) — returns files and directoriesBearer
GET/v1/services/files/objectsFlat object list, no directory groupingBearer
POST/v1/services/filesUpload one file (file + same image options as above) — returns url, path, name, typeBearer
POST/v1/services/files/manyUpload many (files field); folder must be absolute (u/{userId}/…)Bearer
DELETE/v1/services/files?path=…Delete a file or a whole directory (recursive), returns deletedCountBearer
DELETE/v1/services/files/many?paths[]=…Delete many paths, per-path resultsBearer
GET/v1/services/files/metadata?path=…exists, size, contentType, lastModifiedBearer
GET/v1/services/files/size?path=…Total size of a file or directory in bytes (bare number)Bearer
POST/v1/services/files/move?source=…&destination=…Move/rename a file or directory (copy + delete)Bearer
GET/v1/filesAlias of GET /v1/services/filesBearer
GET/v1/files/objectsAlias of GET /v1/services/files/objectsBearer

File-browser uploads accept up to 100 MB per file. Images (jpg jpeg webp png gif avif tiff heic heif) are processed like the image endpoint (optimized, renamed to a generated id); every other allowed type is stored as-is with its sanitized filename:

CategoryExtensions
Documentspdf doc docx txt csv xls xlsx
Archiveszip rar 7z tar gz
Videomp4 webm mov avi mkv
Audiomp3 wav ogg m4a aac

Video upload

MethodPathDescriptionAuth
POST/storage/uploadMultipart video upload; processed asynchronously into multiple qualitiesPublic

Limits: 5 GB, extensions mp4 webm mov avi mkv. The response returns immediately while processing continues in the background:

{
  "id": "vid_abc123",
  "url": "https://storage.feeef.org/videos/vid_abc123/metadata.json",
  "status": "success"
}

The metadata.json (and the quality renditions next to it) appear once processing finishes.

This route is registered at the server root, outside the versioned API group: it is not available under https://api.feeef.org/v1/…. Call it on the path-prefixed backend origin without the /v1 suffix (in local dev: http://localhost:3333/storage/upload). It currently requires no authentication — treat it as unstable until it moves under /v1.

Examples

Upload an optimized image:

curl -X POST "https://api.feeef.org/v1/services/storage/upload" \
  -H "Authorization: Bearer $FEEEF_TOKEN" \
  -F "file=@banner.png" \
  -F "folder=stores/STORE_ID/banners" \
  -F "width=1200" -F "quality=80"

Browse your media library:

curl -H "Authorization: Bearer $FEEEF_TOKEN" \
  "https://api.feeef.org/v1/services/files?path=uploads/any&limit=100"

Upload any file type through the file browser (here: a PDF):

curl -X POST "https://api.feeef.org/v1/services/files" \
  -H "Authorization: Bearer $FEEEF_TOKEN" \
  -F "file=@invoice.pdf" \
  -F "folder=documents"

Delete a file (or an entire directory) by path:

curl -X DELETE -H "Authorization: Bearer $FEEEF_TOKEN" \
  "https://api.feeef.org/v1/services/files?path=u/USER_ID/documents/old.pdf"

Upload a video for processing (path-prefixed origin, no /v1 — see the callout above):

curl -X POST "$API_ORIGIN/storage/upload" \
  -F "file=@promo.mp4"

Notes

  • There is no temp-storage endpoint today. Uploads are permanent immediately: the backend contains a temp-file registry and a storage/clear cleanup route, but both are commented out. Remove unused files yourself via the file-browser delete endpoints.
  • Delete endpoints take paths in the query string (not the body), and directory deletes are recursive — deleting u/{userId}/documents removes everything under it.
  • folder semantics differ per endpoint: image upload always prefixes u/{userId}/; single file-browser upload prefixes relative folders; …/files/many rejects folders that don't already start with u/{userId}.
  • The JavaScript SDK ships upload helpers only (ff.storage.upload, uploadBytes, uploadStoreFile, uploadProductImage, uploadStoreLogo…); the file browser is not wrapped yet. The Dart SDK wraps both: Feeef.instance.storage (uploads) and Feeef.instance.files (list/objects/upload/uploadMany/delete/deleteMany/metadata/move).
  • Upload responses and stored objects are served from the storage CDN — URLs are permanent and publicly readable; treat them as unguessable but not access-controlled.

On this page