API

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.

Getting Started

Authentication

Base URL & API Key
# 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"
Conventions

Response & error format

Every endpoint on this page follows the same envelope, so you can handle errors generically.

Error format
# 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" }
Endpoints

API Reference

Click any endpoint to see its example request and response.

Files
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.
Workspaces
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.
Sharing
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).
Members
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

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).

Webhooks management API
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.
Event catalog
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"
  }
}
Delivery request

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>
Verifying signatures

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:

Immediate 1st attempt
30s 2nd attempt
2m 3rd attempt
10m 4th attempt
1h 5th attempt
6h 6th attempt

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.

Limits

Storage limits

Two independent caps apply: your plan's total storage, and (optionally) a per-workspace max file size.

5 GB Free
100 GB Starter
500 GB Plus
1 TB Pro
2 TB Business

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.

Limits

Rate limits

300 / min General API requests
20 / 60s Auth endpoints (login, signup, etc.)
300 / min Upload-init calls, per user
600 / min Sync-poll calls
600 / 60s Per-IP guard, all endpoints

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" }.

Versioning

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.

Start integrating today

Full API access on every plan, including the free tier. Get your key and start building.

Free tier includes 5 GB storage and full API access.