DeepStorage
Home

API Reference

Upload files, import from links and video sites, and read your folders and files programmatically. The API is JSON over HTTPS, authenticated with a Bearer key. Version 1 (stable).

Quickstart Authentication Conventions Rate limits Scopes Errors Endpoints Managing files OpenAPI spec

Quickstart

API access requires a Pro account.

  1. In the web app open your profile menu and choose API Keys. Create a key, pick its scopes, and copy it. The full key is shown once — store it securely.
  2. Verify it with a request to /api/v1/me:
curl -H "Authorization: Bearer YOUR_KEY" \
     "https://deepstorage.dev/api/v1/me"

Upload a file:

curl -H "Authorization: Bearer YOUR_KEY" \
     --data-binary @movie.mp4 \
     "https://deepstorage.dev/api/v1/upload?filename=movie.mp4&path=/Films/"

Authentication

Send your key in the Authorization header on every request:

Authorization: Bearer csb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys look like csb_live_…. We store only a one-way hash of your key, so it cannot be recovered — if you lose it, revoke it and create a new one. A request with a missing, invalid, revoked, or expired key returns 401; a non-Pro account returns 403 with code: "premium_required". You can hold up to 10 keys at once.

Base URL & conventions

  • Base URL: https://deepstorage.dev. All endpoints below are relative to it.
  • Every response is JSON with a top-level status of "ok" or "error".
  • Timestamps are ISO 8601 in UTC. Folder paths always start and end with / (e.g. /Films/).
  • A returned link is a direct, streamable URL to the stored file.

Rate limits

Your account is limited to a number of requests per minute in a 60-second sliding window. The window is shared by every key you hold, so creating more keys does not raise it. The default is 60 requests per minute; read your actual limit from X-RateLimit-Limit or /api/v1/me rather than assuming it. Every response includes:

HeaderMeaning
X-RateLimit-LimitRequests allowed per window.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix epoch second when a slot frees.

Over the limit returns 429 with code: "rate_limited" and a Retry-After header (seconds). Back off and retry.

The limit counts requests, not files. To move or delete many files, send one /api/v1/batch request rather than a loop of single calls.

Scopes

Each key carries a set of scopes, chosen at creation (default: all three). A request to an endpoint outside the key's scopes returns 403 with code: "insufficient_scope".

ScopeGrants
uploadPOST /api/v1/upload, /api/v1/uploads and its chunk, finish and delete
importPOST /api/v1/import, /import/status, /import/cancel
readGET /api/v1/files, /api/v1/folders, /api/v1/trash
writeEverything under Managing files: rename, move, copy, delete, folders, Recycle Bin, batch

GET /api/v1/me works with any valid key. Grant only the scopes a key needs: a key without write cannot delete anything, which is the safer default for a key that only uploads.

Keys created before write existed do not have it. Create a new key if you want one that can manage files.

Errors

Errors return a non-2xx status and a body with a stable, machine-readable code you can branch on without parsing messages:

{ "status": "error", "code": "insufficient_scope",
  "message": "This API key is missing the 'upload' scope." }
HTTPcodeWhen
400invalid_requestMissing/invalid parameter or body.
400folder_too_deepDestination folder is over 64 levels deep.
400folder_path_too_longDestination path is over 16,384 characters.
401missing_authNo/!malformed Authorization header.
401invalid_keyUnknown or revoked key.
401expired_keyKey past its expiry.
403premium_requiredAccount is not Pro.
403account_unavailableAccount missing or banned.
403insufficient_scopeKey lacks the endpoint's scope.
404not_foundTask, file or folder not found, or not yours.
403system_folderRecycle Bin and Shareable Folder cannot be renamed, moved or deleted.
409already_existsA folder is already at that path.
413too_largeA resumable chunk is larger than chunk_size.
429rate_limitedPer-key rate limit exceeded.
503feature_disabled / unavailableAPI off, or storage briefly unavailable.
500server_errorUnexpected server error.

Endpoints

GET /api/v1/me

Who the key belongs to, its scopes and expiry, the live rate-limit window, and account limits. Requires only a valid key.

{ "status": "ok", "username": "alice", "plan": "pro",
  "key": { "id": "...", "name": "My script",
           "scopes": ["upload","import","read","write"],
           "key_prefix": "csb_live_ab12cd...", "key_last4": "wxyz",
           "created_at": "2026-06-25T10:00:00+00:00", "expires_at": null },
  "rate_limit": { "limit": 60, "remaining": 59, "reset": 1782326145,
                  "window_seconds": 60, "scope": "account" },
  "limits": { "upload_max_bytes": null,
              "resumable": { "endpoint": "/api/v1/uploads",
                             "min_bytes": 33554432, "chunk_size": 16777216 } } }
POST /api/v1/upload scope: upload

Streams the raw request body into storage. Pass the file bytes as the body, the name as filename, and an optional destination path (default /). There is no size limit, but this is a single request with no way to resume: for anything large, use resumable uploads instead, so a dropped connection costs one chunk rather than the whole file.

Query
filenamerequiredDestination file name.
pathoptionalDestination folder, default /.
curl -H "Authorization: Bearer YOUR_KEY" \
     --data-binary @movie.mp4 \
     "https://deepstorage.dev/api/v1/upload?filename=movie.mp4&path=/Films/"

{ "status": "ok", "tracking_id": "1782...", "file_name": "movie.mp4",
  "size": 734003200, "link": "https://share.deepstorage.dev/1782.../movie.mp4", "folder": "/Films/" }
POST /api/v1/uploads scope: upload

Starts a resumable upload. The file goes up in chunks with one identity, so a dropped connection, a restart or a pause costs only the chunk in flight. This is the same machinery the website uses for large files, and it is why there is no size limit.

Body
namerequiredDestination file name.
sizerequiredTotal bytes. Must be exact.
folderoptionalDestination folder, default /.
content_typeoptionalDefaults to application/octet-stream.
curl -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json"      -d '{ "name": "movie.mkv", "size": 21474836480, "folder": "/Films/" }'      "https://deepstorage.dev/api/v1/uploads"

{ "mode": "resumable", "session_id": "...", "tracking_id": "rs_...",
  "chunk_size": 16777216, "part_size": 524288, "next_offset": 0,
  "size": 21474836480, "expires_at": 1782347745 }

A file below min_bytes answers { "mode": "one_shot" }: send it to /api/v1/upload instead, because one or two chunks gain nothing from resuming.

A folder more than 64 levels deep, or with a path over 16,384 characters, is refused with 400 folder_too_deep or folder_path_too_long. Choose a folder nearer the top, or shorten some folder names.

PUT /api/v1/uploads/{id}/chunk?offset=N scope: upload

Sends the next chunk as the raw body. Every chunk length must be a multiple of part_size except the last. Answers { "next_offset": N, "complete": false }.

Two different 409s, and they must be handled differently: bad_offset means the offset moved, so retry at once from the next_offset in the reply. busy means another chunk for this upload is still being written, the offset did not move, and you must wait, honouring Retry-After. Treating them alike is what produced 1,830 rejected requests in three hours.

curl -X PUT -H "Authorization: Bearer YOUR_KEY"      --data-binary @chunk0      "https://deepstorage.dev/api/v1/uploads/SESSION_ID/chunk?offset=0"
GET /api/v1/uploads/{id}   POST /api/v1/uploads/{id}/finish   DELETE /api/v1/uploads/{id}

GET reports where to resume from: read next_offset from the server after any interruption rather than trusting a local copy. finish finalizes and returns the share link, or answers 409 needs_bytes / needs_parts with what is still missing. DELETE aborts and frees the slot.

A session lives 6 hours from its last activity, and pauses 15 seconds after its last chunk: the slot is released and the file shows as waiting until the next chunk arrives. That is not stuck.

POST /api/v1/import scope: import

Starts an asynchronous import from a URL and returns a task_id to poll. type: "auto" (default) detects whether the URL is a direct file or a supported video page. Video imports use a per-title folder (path is ignored for them).

curl -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \
     -d '{ "url": "https://www.youtube.com/watch?v=...", "type": "auto",
           "video": { "quality": "1080", "subtitles": true } }' \
     "https://deepstorage.dev/api/v1/import"

{ "status": "ok", "type": "video", "task_id": "video_1782..." }

Body fields: url (required), type (auto|file|video), path and filename (file imports), and video: { quality: "best" | "audio" | a max height like "1080", subtitles, thumbnail }.

GET /api/v1/import/status scope: import

Poll an import by its task_id. Poll every few seconds until state is terminal.

curl -H "Authorization: Bearer YOUR_KEY" \
     "https://deepstorage.dev/api/v1/import/status?task_id=video_1782..."

{ "status": "ok",
  "task": { "id": "video_1782...", "type": "video", "state": "running", "progress": "42%" } }

state is one of running, completed, failed, cancelled. When complete: file imports include link; video imports include folder (then list it with /api/v1/files).

POST /api/v1/import/cancel scope: import

Cancel a running import. Body: { "task_id": "video_1782..." }. Returns { "status": "ok" }, or 404 if the task is unknown.

GET /api/v1/files scope: read

List files and immediate subfolders at path (default /), 50 files per page. Page with offset; has_more is true when another page may exist. Subfolders are returned only on the first page.

The id is how you address a file everywhere else: names repeat inside a folder and a link is shared by every copy, so neither identifies one file.

curl -H "Authorization: Bearer YOUR_KEY" \
     "https://deepstorage.dev/api/v1/files?path=/Films/&offset=0"

{ "status": "ok", "path": "/Films/", "folders": ["2026"],
  "files": [ { "id": "68b2f1c9a4d31e0012ab34cd", "name": "movie.mp4", "size": 734003200,
               "link": "https://share.deepstorage.dev/1782.../movie.mp4",
               "uploaded_at": "2026-06-25T10:00:00+00:00" } ],
  "limit": 50, "next_offset": 1, "has_more": false }
GET /api/v1/folders scope: read

The complete nested folder tree (structure only). Read a folder's contents with /api/v1/files.

{ "status": "ok",
  "tree": { "name": "", "path": "/",
            "folders": [ { "name": "Films", "path": "/Films/",
                           "folders": [ { "name": "2026", "path": "/Films/2026/", "folders": [] } ] } ] } }

Managing files

Everything below needs the write scope. Deleting sends the item to the Recycle Bin, where it can be restored; add permanent=true to skip the bin and remove the stored copy for good, which cannot be undone.

A destination folder has to exist. A path that does not is a 404 rather than a new folder, so a typo in a script cannot quietly scatter your files.

POST /api/v1/files/{id}/rename scope: write

Body: { "name": "final cut.mp4" }. No slashes in a name; use move to change folder. The share link carries the file name, so renaming changes it and the new link comes back in the response. Links you handed out under the old name stop working.

curl -X POST -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \
     -d '{ "name": "final cut.mp4" }' \
     "https://deepstorage.dev/api/v1/files/68b2f1c9a4d31e0012ab34cd/rename"

{ "status": "ok", "id": "68b2f1c9a4d31e0012ab34cd", "name": "final cut.mp4",
  "link": "https://share.deepstorage.dev/1782.../final%20cut.mp4" }
POST /api/v1/files/{id}/move scope: write

Body: { "path": "/Films/2026/" }. The share link does not change.

POST /api/v1/files/{id}/copy scope: write

Body: { "path": "/Archive/" }. Creates a second entry pointing at the same stored file. Copies use no extra storage and share one link; deleting one never breaks the other.

DELETE /api/v1/files/{id} scope: write

To the Recycle Bin by default. Add ?permanent=true to delete it outright.

curl -X DELETE -H "Authorization: Bearer YOUR_KEY" \
     "https://deepstorage.dev/api/v1/files/68b2f1c9a4d31e0012ab34cd"

{ "status": "ok", "id": "68b2f1c9a4d31e0012ab34cd", "deleted": "trash" }
POST /api/v1/folders scope: write

Body: { "path": "/Reports/2026/" }. Creates an empty folder; intermediate levels appear automatically, so a nested path needs one call. A folder already at that path returns 409.

POST /api/v1/folders/rename scope: write

Body: { "path": "/Reports/2025/", "name": "2026" }. Renames the folder and everything under it.

POST /api/v1/folders/move scope: write

Body: { "path": "/Reports/2026/", "to": "/Archive/" }. A folder cannot be moved inside itself.

DELETE /api/v1/folders scope: write

Query: path (percent-encoded) and optional permanent. The path is a query parameter rather than a body because a DELETE with a body is dropped by some proxies and clients.

curl -X DELETE -H "Authorization: Bearer YOUR_KEY" \
     "https://deepstorage.dev/api/v1/folders?path=%2FReports%2F2026%2F"

{ "status": "ok", "path": "/Reports/2026/", "deleted": "trash" }
GET /api/v1/trash scope: read

What is currently in the Recycle Bin, newest first. Query: offset, limit (up to 100).

{ "status": "ok",
  "items": [ { "id": "68b2...", "name": "movie.mp4", "size": 734003200,
               "path": "/Films/", "link": "https://share.deepstorage.dev/1782.../movie.mp4",
               "trashed_at": "2026-08-29T10:00:00+00:00" } ],
  "limit": 50, "next_offset": 1, "has_more": false }
POST /api/v1/trash/restore scope: write

Body: { "id": "68b2..." } for one file, or { "folder": "/Films/" } to restore everything trashed from that folder. Items go back where they were.

POST /api/v1/trash/empty scope: write

Permanently deletes everything in the Recycle Bin, stored copies included. This cannot be undone. Returns { "status": "ok", "deleted": 12 }.

POST /api/v1/batch scope: write

One request, up to 500 items, a result per item. Use this for bulk work: the rate limit counts requests, not files. An item that fails does not stop the rest, so read results rather than the status code alone.

Field
oprequiredmove, copy, trash, restore, or delete_forever.
itemsrequiredUp to 500 entries, each {"id": "..."} for a file or {"folder": "/a/b/"} for a folder.
destmove, copyDestination folder.
curl -X POST -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \
     -d '{ "op": "move", "dest": "/Archive/",
           "items": [ { "id": "68b2..." }, { "folder": "/Films/2025/" } ] }' \
     "https://deepstorage.dev/api/v1/batch"

{ "status": "ok", "ok": 1, "failed": 1,
  "results": [ { "id": "68b2...", "ok": true, "error": "" },
               { "folder": "/Films/2025/", "ok": false,
                 "error": "A folder cannot be moved into itself." } ] }

delete_forever only takes files, and only ones already in the Recycle Bin. Folders are refused with a per-item message.

Managing keys

Keys are created, listed, and revoked from the API Keys panel in the web app (these management endpoints use your web session, not a Bearer key). A key can be revoked at any time and stops working immediately. Set an expiry at creation if you want it to lapse automatically.

Versioning & changes

The current version is v1, served under /api/v1/. We add fields and endpoints without bumping the version; any breaking change would ship under a new version prefix. New response fields may appear over time — ignore unknown fields.

  • 2026-08-29: the write scope and file management (rename, move, copy, delete, folders, Recycle Bin, batch); id on every file in a listing; the rate limit is now per account, shared by all of your keys, and is set per account rather than fixed. Keys issued before this date do not have the write scope.
  • 2026-06-25 — v1: per-key scopes, X-RateLimit-* headers, machine-readable error code, /api/v1/me, file-listing pagination metadata, and this reference + OpenAPI spec.

Legal

ImpressumPrivacy PolicyCookie PolicyTerms of ServiceAcceptable UseSub-processorsSecurityDMCACookie settings

© Blaron. All rights reserved.