dosya.dev REST API
Everything you can do in the dashboard, you can do via the API. Upload terabytes, manage workspaces, and generate share links - all programmatically.
Authentication
# Base URL
https://api.dosya.dev/api
# All requests require an API key in the header
Authorization: Bearer dos_your-api-key
# Example: List your workspaces
$ curl https://api.dosya.dev/api/workspaces \
-H "Authorization: Bearer dos_your-api-key" Response & error format
Every endpoint on this page follows the same envelope, so you can handle errors generically.
# Success responses
{ "ok": true, ... }
# Error responses - the HTTP status conveys the category (400, 401, 403, 404, 409, 429, ...)
{ "ok": false, "error": "<message>" }
# Some errors use a machine-readable string for "error" and spread in extra fields (e.g. an entity id)
{ "ok": false, "error": "folder_locked", "folder_id": "fld_2jd7m4qr", "lock_mode": "full_lock" } API Reference
Click any endpoint to see its example request and response.
POST /api/upload/init
{
"file_name": "project-final.mp4",
"file_size": 214748364800,
"workspace_id": "ws_abc123",
"mime_type": "video/mp4"
} PUT /api/upload/:sessionId
Raw file bytes as the request body. Headers: Content-Type: application/octet-stream Stream the whole file as the PUT body - the server pipes it straight to storage. (Files over 50 MB should instead use the resumable multipart flow returned by /api/upload/init.)
GET /api/files/:id
No request body required.
GET /api/files/:id/download
No request body required. Query parameters (optional): version=2 Download a specific version (default: current) ut=... Unlock token, required for a full-locked file
DELETE /api/files/:id
No request body required.
Note: the first DELETE soft-deletes the file to trash
("permanent": false). A second DELETE on an already-trashed file
permanently erases it and its versions ("permanent": true). PUT /api/files/:id/rename
{
"name": "final-cut-v2.mp4"
} PUT /api/files/:id/move
{
"folder_id": "fld_2jd7m4qr"
}
Note: pass "folder_id": null to move the file to the workspace root. GET /api/files/:id/versions
No request body required.
POST /api/workspaces
{
"name": "Marketing Assets",
"default_region": "eu-central-2"
}
Note: "name" is the only required field. "default_region" is a label
- fetch valid codes from GET /api/regions - and defaults to
ap-southeast-2. Storage placement is effectively single-region per
workspace. You can also pass "icon_initials", "icon_color", and an
optional "max_total_storage_gb" cap. GET /api/workspaces
No request body required. Note: returns the complete list of workspaces you belong to - no paging.
GET /api/workspaces/:id
No request body required.
GET /api/files
Query parameters: workspace_id=ws_abc123 Required page=1 Page number (default 1) per_page=100 Results per page (default 100, clamped 10-500) sort=newest newest (default), oldest, name_asc, name_desc, largest, smallest filter=all all (default), documents, videos, images q=... Search files by name folder_id=... List a specific folder (omit for the workspace root) deleted=1 List trashed files instead of live ones hidden=1 List only hidden files (requires the hide-files permission)
DELETE /api/workspaces/:id
No request body required. Note: you must first remove all other members, and you can't delete your last workspace - either case returns 400.
POST /api/files/:id/share
{
"expires_in_days": 30,
"password": null,
"lock_mode": "none"
} GET /api/shares
Query parameters (required): workspace_id=ws_abc123 Note: workspace_id is required (omitting it returns 400). Returns at most 100 links, most recent first - no further paging.
POST /api/shares/:id/revoke
No request body required. Note: ":id" is the share link id (the "link_id" from the shares list).
POST /api/team/invite
{
"email": "alice@example.com",
"role": "role_member",
"workspace_id": "ws_abc123"
}
Note: "role" is optional (defaults to role_member). It accepts a
built-in role id - role_admin, role_member, role_viewer - or a custom
workspace role id. role_owner cannot be assigned via invite. GET /api/team
Query parameters (required): workspace_id=ws_abc123 Note: returns the complete member list for the workspace - no paging.
DELETE /api/team/members/:id
No request body required. Note: ":id" is the membership id (the "membership_id" from the members list), not the user id.
Webhooks
Get an HTTP callback the moment a file is uploaded, a file is deleted, or a share link is accessed - no polling required.
Registering an endpoint
Add a webhook from your workspace's Webhooks integration page, or register one directly with POST /api/webhooks (see below). The signing secret is returned once, on creation - store it right away, since it can't be fetched again later (only rotated via roll-secret).
POST /api/webhooks
{
"workspace_id": "ws_abc123",
"url": "https://example.com/hooks/dosya",
"events": ["file.uploaded", "file.deleted", "share.accessed"],
"description": "Slack notifier"
} GET /api/webhooks
Query parameters (required): workspace_id=ws_abc123
PATCH /api/webhooks/:id
{
"events": ["file.uploaded"],
"active": false,
"description": "Paused for maintenance"
} DELETE /api/webhooks/:id
No request body required.
POST /api/webhooks/:id/roll-secret
No request body required.
POST /api/webhooks/:id/test
No request body required.
GET /api/webhooks/:id/deliveries
Query parameters (optional): page=1 Page number per_page=25 Results per page (10-100)
POST /api/webhooks/:id/deliveries/:deliveryId/redeliver
No request body required.
file.uploaded
{
"id": "evt_9xk2m4p7",
"type": "file.uploaded",
"created": 1745312400,
"workspace_id": "ws_abc123",
"data": {
"file_id": "file_8xk2m9p4",
"name": "project-final.mp4",
"size": 214748364800,
"folder_id": "fld_2jd7m4qr",
"version": 2,
"is_new_version": true
}
} file.deleted
{
"id": "evt_3kp7n2xm",
"type": "file.deleted",
"created": 1745312450,
"workspace_id": "ws_abc123",
"data": {
"file_id": "file_8xk2m9p4",
"name": "project-final.mp4",
"permanent": false
}
} share.accessed
{
"id": "evt_7n2xm4kp",
"type": "share.accessed",
"created": 1745312500,
"workspace_id": "ws_abc123",
"data": {
"share_id": "share_9xm2k4p7",
"token": "9xm2k4p7",
"file_id": "file_8xk2m9p4",
"access_type": "download"
}
}
Every event is delivered as a single POST with a JSON body (the payload shown above) and these headers:
POST <your endpoint URL> Content-Type: application/json User-Agent: dosya-webhooks/1 X-Dosya-Signature: t=1745312400,v1=3f9c1a7e2b8d4f6a9c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a X-Dosya-Event-Id: evt_9xk2m4p7 X-Dosya-Event-Type: file.uploaded X-Dosya-Delivery-Id: whd_3kp7n2xm <JSON payload - see the event catalog above>
X-Dosya-Signature carries t=<unix-seconds>,v1=<hex-hmac>. Recompute HMAC_SHA256(secret, `${t}.${rawBody}`) over the raw, unparsed request body and compare it to v1 using a constant-time comparison.
Reject the request if the timestamp is more than 300 seconds (5 minutes) away from the current time - this blocks replayed deliveries.
import crypto from "node:crypto";
function verifyDosyaSignature(secret, rawBody, header) {
// header looks like: "t=1745312400,v1=3f9c1a..."
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
// Replay protection: reject signatures older than 5 minutes
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const message = t + "." + rawBody;
const expected = crypto.createHmac("sha256", secret).update(message).digest("hex");
// Constant-time compare
const a = Buffer.from(parts.v1, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
} Retries & auto-disable
A delivery counts as a success on any 2xx response. Otherwise it's retried up to 6 attempts total, with increasing backoff:
An endpoint that racks up too many consecutive failed attempts across events is automatically deactivated so it stops burning retries against a dead URL - re-enable it any time from the app or via PATCH /api/webhooks/:id. View delivery history per endpoint on the app's Webhooks integration page, or via GET /api/webhooks/:id/deliveries - failed deliveries can be redelivered from either place.
Storage limits
Two independent caps apply: your plan's total storage, and (optionally) a per-workspace max file size.
Numbers above are each plan's total storage. An upload that would push you over it is rejected with 400 Bad Request. Separately, a workspace can set its own max_file_size_gb cap on individual files (uncapped by default, configurable in workspace settings) - a file over that cap is rejected with 400 on the REST upload API, or 413 over S3 or WebDAV.
Rate limits
Most API responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers (some exempt paths, e.g. desktop sync, webhooks, and upload data transfer, omit them). When a limit is exceeded, the response is 429 Too Many Requests with a Retry-After header (seconds) and body { "ok": false, "error": "Too many requests" }.
API versioning
This API is unversioned - the base path is /api, with no version number in the URL or in request headers. Breaking changes are announced in the changelog ahead of time; non-breaking additions (new fields, new endpoints) may ship without notice.