Storage API
Upload, list, download and delete files from your own projects with an API key. Uploaded files also appear on the web and in the sync folder.
Create an API keyGetting started
- Create a key under Account → API keys and copy the full key once. Create an API key
- Upload your first file:
curl -X POST "https://cloud104.com/api/v1/files" \ -H "Authorization: Bearer sk_..." \ -F "file=@logo.png" \ -F "path=/apps/blog/logo.png" \ -F "visibility=public"
- The response describes the uploaded file; with
visibility=public,urlis its permanent CDN URL:{ "ok": true, "id": "cm3f8k2x10001", "parentId": "cm3f8k2x10000", "name": "logo.png", "kind": "file", "size": 48213, "sha256": "9f2c…", "mime": "image/png", "visibility": "public", "url": "https://cdn.cloud104.com/…/logo.png", "revision": 1, "mtime": "2026-10-28T10:00:00.000Z", "source": "api", "sourceName": "Blog", "starred": false, "trashed": false, "createdAt": "2026-10-28T10:00:00.000Z", "updatedAt": "2026-10-28T10:00:00.000Z", "path": "/apps/blog/logo.png" }
Authentication
- Send an
Authorization: Bearer sk_…header with every request. Without it you get401 unauthorized; a wrong or deleted key gets401 invalid_key. - Scope: a
files(Files only) key can use the file and folder endpoints; sharing, “shared with me” and trash endpoints needall(Full account), otherwise403 insufficient_scope. - Folder lock: a key locked to a folder only sees and writes under that folder; anything outside is
403 folder_not_allowed. Requests without a folder work in the locked folder;pathis still written from the account root. - Use the key on the server only: never embed it in a browser, a mobile app or a public repository. For browser uploads, issue an upload token (below).
Endpoints
All endpoints live under https://cloud104.com/api/v1. Requests with a body send JSON (Content-Type: application/json); successful responses carry "ok": true. :id is the id field from list and upload responses.
Upload a file
POST/api/v1/files
Uploads a file into a folder. Accepts a multipart form (multipart/form-data, up to 100 MB) or a raw body; with a raw body the parameters go in the query and Content-Length is required. Returns 201 for a new file and 200 for a new version of an existing one.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| file | form | file | yes | The file itself (with a raw body, the whole body). |
| path | form | string | no | Target path including the file name (/apps/blog/logo.png), from the account root; missing folders are created. |
| folderId | form | string | no | Target folder ID (instead of path). Without either, the root or the key’s locked folder. |
| name | query | string | no | File name (with folderId or a raw body); defaults to the uploaded file’s own name. |
| visibility | form | string | no | public or private (default). |
| onConflict | form | string | no | If a file with the same name exists: rename (default, numbered name), replace (new version) or fail (409). |
Example request
curl -X POST "https://cloud104.com/api/v1/files" \ -H "Authorization: Bearer sk_..." \ -F "file=@logo.png" \ -F "path=/apps/blog/logo.png" \ -F "visibility=public"
curl -X POST "https://cloud104.com/api/v1/files?path=/yedek/db.sql.gz" \ -H "Authorization: Bearer sk_..." \ -H "Content-Type: application/octet-stream" \ --data-binary @db.sql.gz
Example response
{
"ok": true,
"id": "cm3f8k2x10001",
"parentId": "cm3f8k2x10000",
"name": "logo.png",
"kind": "file",
"size": 48213,
"sha256": "9f2c…",
"mime": "image/png",
"visibility": "public",
"url": "https://cdn.cloud104.com/…/logo.png",
"revision": 1,
"mtime": "2026-10-28T10:00:00.000Z",
"source": "api",
"sourceName": "Blog",
"starred": false,
"trashed": false,
"createdAt": "2026-10-28T10:00:00.000Z",
"updatedAt": "2026-10-28T10:00:00.000Z",
"path": "/apps/blog/logo.png"
}List a folder
GET/api/v1/files
Returns the folder’s contents, folders first, then by name. Paginated: when nextCursor is set, send it as cursor for the next page.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| folderId | query | string | no | Folder ID. |
| path | query | string | no | Folder path (/apps/blog). Without either, the root or the key’s locked folder. |
| cursor | query | string | no | The nextCursor value of the previous response. |
| limit | query | integer | no | Page size; 50 by default, at most 500. |
Example request
curl "https://cloud104.com/api/v1/files?path=/apps/blog&limit=50" \ -H "Authorization: Bearer sk_..."
Example response
{
"ok": true,
"folderId": "cm3f8k2x10000",
"items": [
{
"id": "cm3f8k2x10001",
"parentId": "cm3f8k2x10000",
"name": "logo.png",
"kind": "file",
"size": 48213,
"sha256": "9f2c…",
"mime": "image/png",
"visibility": "public",
"url": "https://cdn.cloud104.com/…/logo.png",
"revision": 1,
"mtime": "2026-10-28T10:00:00.000Z",
"source": "api",
"sourceName": "Blog",
"starred": false,
"trashed": false,
"createdAt": "2026-10-28T10:00:00.000Z",
"updatedAt": "2026-10-28T10:00:00.000Z"
}
],
"nextCursor": null
}Possible error codes
File or folder metadata
GET/api/v1/files/:id
Returns the item’s fields, your role (owner, editor, viewer) and the location chain from the root to the item (path).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | File or folder ID. |
Example request
curl "https://cloud104.com/api/v1/files/cm3f8k2x10001" \ -H "Authorization: Bearer sk_..."
Example response
{
"ok": true,
"id": "cm3f8k2x10001",
"parentId": "cm3f8k2x10000",
"name": "logo.png",
"kind": "file",
"size": 48213,
"sha256": "9f2c…",
"mime": "image/png",
"visibility": "public",
"url": "https://cdn.cloud104.com/…/logo.png",
"revision": 1,
"mtime": "2026-10-28T10:00:00.000Z",
"source": "api",
"sourceName": "Blog",
"starred": false,
"trashed": false,
"createdAt": "2026-10-28T10:00:00.000Z",
"updatedAt": "2026-10-28T10:00:00.000Z",
"role": "owner",
"path": [
{
"id": "cm3f8k2x0zzzz",
"name": ""
},
{
"id": "cm3f8k2x10000",
"name": "blog"
},
{
"id": "cm3f8k2x10001",
"name": "logo.png"
}
]
}Possible error codes
Download a file
GET/api/v1/files/:id/content
Redirects with 302: to a 60-second signed storage URL for private files (supports Range), to the permanent CDN URL for public ones. Your client must follow redirects (-L in curl).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | File ID. |
| version | query | string | no | ID of an older version (see version history). |
| inline | query | string | no | With 1 the file opens as a browser preview (ignored for types that can run in a browser). |
Example request
curl -L -o logo.png "https://cloud104.com/api/v1/files/cm3f8k2x10001/content" \ -H "Authorization: Bearer sk_..."
Example response
HTTP/1.1 302 Found Location: https://… (60 s)
Possible error codes
Rename, move, visibility, star
PATCH/api/v1/files/:id
Changes only the fields you send. If both name and folder are given, the item is moved first, then renamed.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | File or folder ID. |
| name | body | string | no | New name. |
| folderId | body | string | no | ID of the folder to move into. |
| visibility | body | string | no | public or private. |
| starred | body | boolean | no | Whether the item is starred. |
| expectedRevision | body | integer | no | If given and the item is not at this revision, 409 revision_conflict. |
Example request
curl -X PATCH "https://cloud104.com/api/v1/files/cm3f8k2x10001" \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"name":"logo-2026.png","visibility":"private"}'Example response
{
"ok": true,
"id": "cm3f8k2x10001",
"parentId": "cm3f8k2x10000",
"name": "logo-2026.png",
"kind": "file",
"size": 48213,
"sha256": "9f2c…",
"mime": "image/png",
"visibility": "private",
"url": null,
"revision": 1,
"mtime": "2026-10-28T10:00:00.000Z",
"source": "api",
"sourceName": "Blog",
"starred": false,
"trashed": false,
"createdAt": "2026-10-28T10:00:00.000Z",
"updatedAt": "2026-10-28T10:00:00.000Z"
}Delete
DELETE/api/v1/files/:id
Moves the item to the trash; a folder goes with its contents. permanent=true deletes permanently (also for items already in the trash). Responds 204.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | File or folder ID. |
| permanent | query | boolean | no | With true, delete permanently. |
Example request
curl -X DELETE "https://cloud104.com/api/v1/files/cm3f8k2x10001" \ -H "Authorization: Bearer sk_..."
Example response
HTTP/1.1 204 No Content
Restore from trash
POST/api/v1/files/:id/restore
Puts the item back where it was; into the root if that folder no longer exists, with a numbered name if the name is taken.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | ID of the trashed item. |
Example request
curl -X POST "https://cloud104.com/api/v1/files/cm3f8k2x10001/restore" \ -H "Authorization: Bearer sk_..."
Example response
{
"ok": true,
"id": "cm3f8k2x10001",
"parentId": "cm3f8k2x10000",
"name": "logo.png",
"kind": "file",
"size": 48213,
"sha256": "9f2c…",
"mime": "image/png",
"visibility": "public",
"url": "https://cdn.cloud104.com/…/logo.png",
"revision": 1,
"mtime": "2026-10-28T10:00:00.000Z",
"source": "api",
"sourceName": "Blog",
"starred": false,
"trashed": false,
"createdAt": "2026-10-28T10:00:00.000Z",
"updatedAt": "2026-10-28T10:00:00.000Z"
}Possible error codes
Version history
GET/api/v1/files/:id/versions
Returns the file’s versions, newest first (at most 200); the current one has current: true.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | File ID. |
Example request
curl "https://cloud104.com/api/v1/files/cm3f8k2x10001/versions" \ -H "Authorization: Bearer sk_..."
Example response
{
"ok": true,
"items": [
{
"id": "cm3f9v0000002",
"nodeId": "cm3f8k2x10001",
"sha256": "9f2c…",
"size": 48213,
"source": "api",
"sourceName": "Blog",
"deviceId": null,
"apiKeyId": "cm3a…",
"createdAt": "2026-10-28T10:00:00.000Z",
"current": true
},
{
"id": "cm3f9v0000001",
"nodeId": "cm3f8k2x10001",
"sha256": "41ab…",
"size": 51007,
"source": "web",
"sourceName": null,
"deviceId": null,
"apiKeyId": null,
"createdAt": "2026-10-20T08:12:00.000Z",
"current": false
}
]
}Possible error codes
Restore a version
POST/api/v1/files/:id/versions
Makes the chosen version current. It is added as a new version; no version is deleted.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | File ID. |
| versionId | body | string | yes | The id from the version history. |
Example request
curl -X POST "https://cloud104.com/api/v1/files/cm3f8k2x10001/versions" \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"versionId":"cm3f9v0000001"}'Example response
{
"ok": true,
"id": "cm3f8k2x10001",
"parentId": "cm3f8k2x10000",
"name": "logo.png",
"kind": "file",
"size": 51007,
"sha256": "9f2c…",
"mime": "image/png",
"visibility": "public",
"url": "https://cdn.cloud104.com/…/logo.png",
"revision": 3,
"mtime": "2026-10-28T10:00:00.000Z",
"source": "api",
"sourceName": "Blog",
"starred": false,
"trashed": false,
"createdAt": "2026-10-28T10:00:00.000Z",
"updatedAt": "2026-10-28T10:00:00.000Z"
}Possible error codes
Read the public link
GET/api/v1/files/:id/link
Returns the item’s public share link, or link: null. Only the owner can read it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | File or folder ID. |
Example request
curl "https://cloud104.com/api/v1/files/cm3f8k2x10001/link" \ -H "Authorization: Bearer sk_..."
Example response
{
"ok": true,
"link": {
"url": "https://cloud104.com/s/Xk29…",
"expiresAt": null,
"allowDownload": true,
"protected": false,
"createdAt": "2026-10-28T10:00:00.000Z"
}
}Create or change the public link
PUT/api/v1/files/:id/link
Creates the link if there is none, otherwise changes its settings. Password and expiry are Pro features; on the Free plan 402 needs_pro.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | File or folder ID. |
| password | body | string | null | no | Password, at most 128 characters; null removes it. |
| expiresAt | body | string | null | no | ISO date in the future; null removes the expiry. |
| allowDownload | body | boolean | no | Whether the link allows downloading. |
Example request
curl -X PUT "https://cloud104.com/api/v1/files/cm3f8k2x10001/link" \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"allowDownload":false}'Example response
{
"ok": true,
"link": {
"url": "https://cloud104.com/s/Xk29…",
"expiresAt": null,
"allowDownload": false,
"protected": false,
"createdAt": "2026-10-28T10:00:00.000Z"
}
}Possible error codes
Close the public link
DELETE/api/v1/files/:id/link
Closes the link; the old URL returns 404 right away. Responds 204.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string | yes | File or folder ID. |
Example request
curl -X DELETE "https://cloud104.com/api/v1/files/cm3f8k2x10001/link" \ -H "Authorization: Bearer sk_..."
Example response
HTTP/1.1 204 No Content
Create a folder
POST/api/v1/folders
Creates a folder; if a folder with that name already exists it is returned, so re-running a script is safe.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| path | body | string | no | Full path from the account root; intermediate folders are created too. |
| parentId | body | string | no | Parent folder ID (with name); defaults to the root or the key’s locked folder. |
| name | body | string | no | Folder name; required when path is not given. |
Example request
curl -X POST "https://cloud104.com/api/v1/folders" \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"path":"/apps/blog"}'Example response
{
"ok": true,
"id": "cm3f8k2x10000",
"parentId": "cm3f8k2x0zzzz",
"name": "blog",
"kind": "folder",
"size": 0,
"sha256": null,
"mime": null,
"visibility": "private",
"url": null,
"revision": 1,
"mtime": "2026-10-28T10:00:00.000Z",
"source": "api",
"sourceName": "Blog",
"starred": false,
"trashed": false,
"createdAt": "2026-10-28T10:00:00.000Z",
"updatedAt": "2026-10-28T10:00:00.000Z"
}Possible error codes
Search
GET/api/v1/search
Searches files and folders by name (trash excluded). The query needs at least two characters; a shorter one returns an empty list.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| q | query | string | yes | Text to search for. |
| kind | query | string | no | document, image, video, audio or pdf. |
Example request
curl "https://cloud104.com/api/v1/search?q=logo&kind=image" \ -H "Authorization: Bearer sk_..."
Example response
{
"ok": true,
"items": [
{
"id": "cm3f8k2x10001",
"parentId": "cm3f8k2x10000",
"name": "logo.png",
"kind": "file",
"size": 48213,
"sha256": "9f2c…",
"mime": "image/png",
"visibility": "public",
"url": "https://cdn.cloud104.com/…/logo.png",
"revision": 1,
"mtime": "2026-10-28T10:00:00.000Z",
"source": "api",
"sourceName": "Blog",
"starred": false,
"trashed": false,
"createdAt": "2026-10-28T10:00:00.000Z",
"updatedAt": "2026-10-28T10:00:00.000Z"
}
]
}Download as zip
GET/api/v1/zip
Streams the chosen files and folders (folders with their contents) as a single zip; files are stored without compression.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| ids | query | string | yes | Comma-separated IDs, at most 200. |
Example request
curl -L -o files.zip "https://cloud104.com/api/v1/zip?ids=cm3f8k2x10001,cm3f8k2x10000" \ -H "Authorization: Bearer sk_..."
Example response
HTTP/1.1 200 OK Content-Type: application/zip Content-Disposition: attachment; filename="cloud104-2026-10-28.zip"
Possible error codes
Usage and limits
GET/api/v1/usage
Returns the plan, storage used, quota, the largest file you can upload right now (maxFileSize: the smaller of the plan limit and the space left), the file count and the per-minute request limit. Sizes are in bytes.
Parameters
No parameters.
Example request
curl "https://cloud104.com/api/v1/usage" \ -H "Authorization: Bearer sk_..."
Example response
{
"ok": true,
"plan": "pro",
"extraGb": 0,
"used": 52428800,
"limit": 1073741824,
"maxFileSize": 1021313024,
"files": 128,
"rateLimit": 600,
"retentionDays": 30
}Create an upload token
POST/api/v1/upload-tokens
A ten-minute, single-use token for direct browser uploads. Can only be called with an API key; with a session, 403 api_key_required.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| path | body | string | no | Folder path for the upload. Defaults to the key’s locked folder; with an unlocked key the token is not bound to a folder. |
| maxSize | body | integer | no | Largest file size in bytes; cannot exceed the plan limit. |
| visibility | body | string | no | public or private (default). |
Example request
curl -X POST "https://cloud104.com/api/v1/upload-tokens" \
-H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"path":"/apps/blog","maxSize":5000000,"visibility":"public"}'Example response
{
"ok": true,
"token": "ut_…",
"expiresAt": "2026-10-28T10:10:00.000Z"
}Possible error codes
Trash
GET/api/v1/trashNeeds the all scope
Returns the items that were moved to the trash directly.
Parameters
No parameters.
Example request
curl "https://cloud104.com/api/v1/trash" \ -H "Authorization: Bearer sk_..."
Example response
{
"ok": true,
"items": [
{
"id": "cm3f8k2x10001",
"parentId": "cm3f8k2x10000",
"name": "logo.png",
"kind": "file",
"size": 48213,
"sha256": "9f2c…",
"mime": "image/png",
"visibility": "public",
"url": "https://cdn.cloud104.com/…/logo.png",
"revision": 1,
"mtime": "2026-10-28T10:00:00.000Z",
"source": "api",
"sourceName": "Blog",
"starred": false,
"trashed": true,
"createdAt": "2026-10-28T10:00:00.000Z",
"updatedAt": "2026-10-28T10:00:00.000Z"
}
]
}Possible error codes
Empty the trash
DELETE/api/v1/trashNeeds the all scope
Permanently deletes everything in the trash; returns the number of deleted items (count) and the space freed (freed, bytes).
Parameters
No parameters.
Example request
curl -X DELETE "https://cloud104.com/api/v1/trash" \ -H "Authorization: Bearer sk_..."
Example response
{
"ok": true,
"count": 12,
"freed": 3145728
}Possible error codes
Public files and CDN URLs
- A file uploaded with
visibility=public, or switched later withPATCHand"visibility": "public", gets a permanent URL underhttps://cdn.cloud104.com/…. The URL is in the response’surlfield; opening it needs no key or session. - A private file has
url: null; its content is downloaded throughGET /api/v1/files/:id/contentfrom a 60-second signed URL. - Types that can run in a browser, such as HTML, SVG and JavaScript, are downloaded from the CDN, not opened.
Uploading from the browser
The key never reaches the browser. Your server issues a ten-minute, single-use token; the browser uses it to upload the file straight to cloud104.
- Your servercloud104Calls
POST /api/v1/upload-tokenswith the key. - cloud104Your serverReturns
{ token, expiresAt }. - Your serverBrowserPuts the token in the page or in its own API response.
- Browsercloud104Uploads to
POST /api/v1/fileswithAuthorization: Upload <token>.
The token’s folder, maximum size and visibility are fixed when it is issued; the browser cannot go beyond them. Token uploads are open to CORS (Access-Control-Allow-Origin: *) and limited to 120 uploads per minute per key.
// Your server:
const r = await fetch('https://cloud104.com/api/v1/upload-tokens', {
method: 'POST',
headers: { Authorization: 'Bearer sk_...', 'Content-Type': 'application/json' },
body: JSON.stringify({ path: '/apps/blog', maxSize: 5_000_000 }),
})
const { token } = await r.json()
// Browser:
await fetch('https://cloud104.com/api/v1/files?name=' + encodeURIComponent(file.name), {
method: 'POST',
headers: { Authorization: 'Upload ' + token },
body: file,
})Quota and limits
Free: 1 MB total, 1 MB per file, 60 requests per minute. Pro: 1 GB + extra GB, up to 5 GB per file, 600 requests per minute. Requests over the limit get 429 with a Retry-After header.
- When storage is full you get
413 quota_exceeded; your files are not deleted, only new uploads stop. - A multipart form is limited to 100 MB; upload larger files with a raw body (
Content-Type: application/octet-stream). - For current values, call
GET /api/v1/usage.
Rate limit headers
The limit is counted per key over a one-minute window. Every response to a keyed request carries the headers below; over the limit you get 429 rate_limited and Retry-After.
| Header | Description |
|---|---|
| X-RateLimit-Limit | Your plan’s requests per minute. |
| X-RateLimit-Remaining | Requests left in this window. |
| X-RateLimit-Reset | When the window resets (Unix time, seconds). |
| Retry-After | Only on 429: seconds to wait before retrying. |
Error codes
Errors come back as { "error": { "code": "<code>", "message": "…" } }; some codes carry extra fields (field, limit, used). code is stable, so branch on it; message is English or Turkish by Accept-Language.
| Code | HTTP | Meaning |
|---|---|---|
| invalid_request | 400 | Missing or malformed parameter; the response names it in field. |
| invalid_name | 400 | The name cannot be used (\ / : * ? " < > |, a trailing dot or space, reserved names). |
| invalid_move | 400 | A folder cannot be moved into itself. |
| not_a_folder | 400 | The target, or an item on the path, is not a folder. |
| not_a_file | 400 | The operation only works on files. |
| unauthorized | 401 | No Authorization header. |
| invalid_key | 401 | The key is wrong or deleted. |
| invalid_upload_token | 401 | Upload token is invalid, used or expired. |
| needs_pro | 402 | The feature is on the Pro plan (e.g. a password-protected or expiring link). |
| insufficient_scope | 403 | The key’s scope is not enough; sharing, “shared with me” and trash endpoints need all. |
| folder_not_allowed | 403 | Outside the folder the key is locked to. |
| forbidden | 403 | Not allowed (e.g. writing with a viewer role, deleting the root folder). |
| api_key_required | 403 | The endpoint can only be called with an API key. |
| not_found | 404 | No such item, or no access. |
| name_conflict | 409 | An item with this name exists (onConflict=fail). |
| revision_conflict | 409 | The item is not at the expected revision; read it again and retry. |
| length_required | 411 | Raw body without Content-Length. |
| file_too_large | 413 | The file exceeds the plan’s or the token’s file limit; the response carries limit. |
| quota_exceeded | 413 | Storage is full; the response carries used and limit. |
| rate_limited | 429 | Rate limit exceeded; wait for Retry-After. |
| storage_unavailable | 503 | Storage is unavailable right now; retry a bit later. |