REST API

Every SDK sits on these endpoints. Use them directly from any language. All requests take JSON bodies and return JSON; authenticate with your project API key in the x-devstorage-key header.

Base URL & auth

https://api.devstorage.io

# every request:
#   x-devstorage-key: dsk_live_...

Keys carry scopes (upload, read, delete) set when you create them in the dashboard. Errors return { "error": "message" } with a matching HTTP status; upload endpoints are rate-limited and answer 429 when exceeded.

Upload a file

POST/v1/upload/presign

Ask for a presigned PUT URL. The session expires if unused.

curl https://api.devstorage.io/v1/upload/presign \
  -H "x-devstorage-key: $DEVSTORAGE_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "fileName": "photo.jpg",
    "fileSize": 482113,
    "mimeType": "image/jpeg",
    "metadata": { "userId": "u42" }
  }'
response
{
  "sessionId": "file_01J...",
  "presignedUrl": "https://...r2.cloudflarestorage.com/...",
  "key": "proj_x/file_01J.../photo.jpg",
  "expiresAt": "2026-07-02T12:34:56.000Z"
}

Then PUT the raw bytes to presignedUrl — this call goes to Cloudflare R2, not the DevStorage API, and needs no API key:

curl -X PUT "$PRESIGNED_URL" \
  -H "content-type: image/jpeg" \
  --data-binary @photo.jpg
POST/v1/upload/complete

Confirm the upload. DevStorage verifies the object exists and matches the declared size, records the file, and fires your webhook. Returns the file record:

curl https://api.devstorage.io/v1/upload/complete \
  -H "x-devstorage-key: $DEVSTORAGE_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "sessionId": "file_01J..." }'
response
{
  "id": "file_01J...",
  "name": "photo.jpg",
  "mimeType": "image/jpeg",
  "sizeBytes": 482113,
  "r2Key": "proj_x/file_01J.../photo.jpg",
  "url": "https://...",
  "metadata": { "userId": "u42" },
  "uploadedAt": "2026-07-02T12:35:10.000Z"
}
POST/v1/upload/abort

Cancel a session you won't complete ({ "sessionId" }). Abandoned sessions are also cleaned up automatically after 24 hours.

Multipart uploads (large files)

For files that are too large for a single PUT (the SDKs switch at 100 MiB), upload in chunks. Every part except the last must be exactly partSize bytes:

# 1. Start — returns sessionId, partSize, partCount
curl https://api.devstorage.io/v1/upload/multipart/start \
  -H "x-devstorage-key: $DEVSTORAGE_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "fileName": "video.mp4", "fileSize": 734003200, "mimeType": "video/mp4" }'

# 2. For each part 1..partCount: get a URL, PUT the chunk, keep the ETag
curl https://api.devstorage.io/v1/upload/multipart/part \
  -H "x-devstorage-key: $DEVSTORAGE_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "sessionId": "file_01J...", "partNumber": 1 }'

# 3. Complete with every part's ETag
curl https://api.devstorage.io/v1/upload/multipart/complete \
  -H "x-devstorage-key: $DEVSTORAGE_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "sessionId": "file_01J...",
    "parts": [
      { "partNumber": 1, "etag": "\"9a0364b9...\"" },
      { "partNumber": 2, "etag": "\"e3b0c442...\"" }
    ]
  }'

POST /v1/upload/multipart/abort cancels an in-progress multipart session.

Uploading parts from a browser? The bucket's CORS policy must expose the ETagresponse header, or the client can't collect part ETags for the complete call.

Files

GET/v1/files

List completed files, newest first. Query params: limit (1–100, default 20) and cursor (from a previous page's nextCursor). Requires the read scope.

curl "https://api.devstorage.io/v1/files?limit=50" \
  -H "x-devstorage-key: $DEVSTORAGE_API_KEY"
response
{ "files": [ { "id": "file_01J...", ... } ], "nextCursor": "file_01H...", "hasMore": true }
GET/v1/files/:fileId

Fetch one file's record with a fresh download url.

DELETE/v1/files/:fileId

Delete the record and the stored object. Requires the delete scope. Returns { "deleted": true }.

Endpoint summary

EndpointPurpose
POST /v1/upload/presignGet a presigned PUT URL for a single-shot upload.
POST /v1/upload/completeVerify and finalize an upload; returns the file record.
POST /v1/upload/abortCancel an upload session.
POST /v1/upload/multipart/startBegin a chunked upload; returns partSize / partCount.
POST /v1/upload/multipart/partGet a presigned URL for one part.
POST /v1/upload/multipart/completeFinalize a chunked upload from the part ETags.
POST /v1/upload/multipart/abortCancel a chunked upload.
GET /v1/filesList files (cursor-paginated).
GET /v1/files/:idGet one file + download URL.
DELETE /v1/files/:idDelete a file.