Files
Upload, inspect, download, rename, copy, move, and delete files inside your containers.
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
| POST | /api/v1/containers/{container}/files | Upload a file | files.write |
| GET | /api/v1/containers/{container}/files/{fileId} | File information + metadata | files.read |
| GET | /api/v1/containers/{container}/files/{fileId}/content | Download the file body | files.read |
| PATCH | /api/v1/containers/{container}/files/{fileId} | Rename the file | files.write |
| DELETE | /api/v1/containers/{container}/files/{fileId} | Delete the file | files.delete |
| POST | /api/v1/containers/{container}/files/{fileId}/copy | Copy (async job) | files.copy |
| POST | /api/v1/containers/{container}/files/{fileId}/move | Move (job) | files.move |
| POST | /api/v1/containers/{container}/uploads | Start a resumable upload (large files) | files.write |
| PUT | /api/v1/containers/{container}/uploads/{uploadId} | Upload one chunk (Content-Range) | files.write |
| POST | /api/v1/containers/{container}/uploads/{uploadId} | Finalise a byte-proxy-uploaded session | files.write |
| POST | /api/v1/containers/{container}/direct-uploads | Get a URL to upload directly to storage (bypass the platform) | files.write |
| POST | /api/v1/containers/{container}/direct-uploads/{uploadId} | Complete a direct-URL upload (register the file) | files.write |
| POST | /api/v1/containers/{container}/files/{fileId}/download-url | Get a URL to download directly from storage | files.read |
| GET | /api/v1/containers/{container}/items | List items (root or ?folderId/?path) | files.read |
| GET | /api/v1/jobs/{jobId} | Poll a copy/move job | files.read |
Uploading
Two upload styles are supported:
# Multipart form (simplest) curl -X POST "https://storage.devd.org/api/v1/containers/contracts/files" \ -H "X-API-Key: sk_live_YOUR_KEY" \ -F "file=@agreement.pdf" \ -F "folderId=folder_XXXX" # optional destination folder # Raw body (streams straight to storage) curl -X POST "https://storage.devd.org/api/v1/containers/contracts/files?name=agreement.pdf" \ -H "X-API-Key: sk_live_YOUR_KEY" \ -H "Content-Type: application/pdf" \ --data-binary @agreement.pdf
The synchronous upload limit is configured per deployment (default 4 MB on Vercel). Files above the limit are rejected with FILE_TOO_LARGE and the limit in error.details.maxUploadBytes. Use conflictBehavior = fail, replace (default), or rename to control name collisions. Pass an Idempotency-Key header to make retries safe.
Large files: resumable chunked uploads
Files above the synchronous limit are uploaded in chunks through a resumable session. Quota is reserved when the session is created. Each chunk request stays under the platform body limit, and non-final chunks must be a multiple of 327680 bytes (320 KiB) — use the chunkSizeBytes the API recommends.
When the byte-proxy Worker is wired up on this deployment, the session-create response also carries an uploadUrl field. Clients SHOULD prefer that URL: chunks flow through the stateless byte-proxy Worker instead of the Pages API request pipeline, which avoids the per-chunk auth + rate-limit + audit overhead that otherwise trips Cloudflare's per-request CPU and subrequest ceilings on long uploads. When chunks go through uploadUrl, follow up with POST /uploads/{uploadId} to register the file. When uploadUrl is absent, PUT chunks to /uploads/{uploadId} — the final chunk auto-finalises the file and no extra call is needed.
# 1. Create a session (reserves quota, validates the name)
curl -X POST "$BASE/containers/contracts/uploads" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"fileName":"video.mp4","totalSizeBytes":10485760}'
# → { "upload": { "id": "upload_XXXX", "chunkSizeBytes": 10485760, ... } }
# 2. Upload chunks in order (bash example, 10 MiB chunks)
split -b 10485760 video.mp4 /tmp/chunk-
START=0; TOTAL=10485760
for PART in /tmp/chunk-*; do
SIZE=$(stat -c%s "$PART"); END=$((START + SIZE - 1))
curl -X PUT "$BASE/containers/contracts/uploads/upload_XXXX" \
-H "X-API-Key: $KEY" \
-H "Content-Range: bytes $START-$END/$TOTAL" \
--data-binary @"$PART"
START=$((END + 1))
done
# The final chunk returns 201 with the completed file (upload.file.id)
# Resume after an interruption: ask where to continue
curl "$BASE/containers/contracts/uploads/upload_XXXX" -H "X-API-Key: $KEY"
# → "bytesReceived" is the next expected offset
# Abort
curl -X DELETE "$BASE/containers/contracts/uploads/upload_XXXX" -H "X-API-Key: $KEY"Direct-URL uploads and downloads (fastest, no relay)
For large-scale traffic, the platform can hand you the storage provider's own pre-authenticated URL so bytes flow directly between your client and Microsoft SharePoint — the platform only mints the URL. This is 10-100× cheaper on server-side bandwidth and typically faster for the end user.
URLs are short-lived secrets. They embed a bearer token and expire in roughly one hour. Do not log them; do not reuse them across users. Request a fresh URL on demand.
Direct upload. Same protocol as the resumable chunked flow, but you PUT chunks to the provider URL yourself instead of relaying through the platform.
# 1. Open a direct-URL upload session — quota is reserved and bandwidth
# is charged against your daily upload cap for totalSizeBytes.
curl -X POST "$BASE/containers/contracts/direct-uploads" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"fileName":"video.mp4","totalSizeBytes":10485760}'
# → { "upload": { "id": "upload_XXXX", "uploadUrl": "https://...",
# "chunkSizeBytes": 10485760, "chunkMultipleBytes": 327680, ... } }
# 2. PUT chunks DIRECTLY to upload.uploadUrl (bytes never touch our server)
START=0; TOTAL=10485760
for PART in /tmp/chunk-*; do
SIZE=$(stat -c%s "$PART"); END=$((START + SIZE - 1))
curl -X PUT "$UPLOAD_URL" \
-H "Content-Range: bytes $START-$END/$TOTAL" \
-H "Content-Length: $SIZE" \
--data-binary @"$PART"
# The final chunk's response is a driveItem JSON — capture item.id for step 3.
START=$((END + 1))
done
# 3. Tell the platform we're done so it registers the file. providerItemId
# is optional — if omitted, the platform looks up the file by name.
curl -X POST "$BASE/containers/contracts/direct-uploads/upload_XXXX" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"providerItemId":"01ABCDEF..."}'Direct download. Ask for a URL and hit it from the client. The response also includes sizeBytes, contentType, and expiresAt.
curl -X POST \
"$BASE/containers/contracts/files/file_XXXX/download-url" \
-H "X-API-Key: $KEY"
# → { "downloadUrl": "https://...", "expiresAt": "...", "sizeBytes": 12345 }
# Then, from the client:
curl -o report.pdf "$DOWNLOAD_URL"Bandwidth is charged at URL issuance: opening a direct upload reserves the declared totalSizeBytes against your daily upload cap; asking for a download URL charges the file's actual size against your daily download cap. Cancel an unused upload session with DELETE to release the quota reservation.
Backend hostname visibility. Today the URLs returned here are on the storage backend's own domain (*.sharepoint.com). Use the relayed flow (POST /files, POST /uploads) if backend opacity matters to your application; or ask your administrator to disable direct URLs on a specific container by setting directUrlsAllowed=false, which causes these three endpoints to return 403 DIRECT_URLS_DISABLED for that container.
Copy and move jobs
Copy (and move) return HTTP 202 with a job. Copies may run asynchronously in the storage backend — poll the job until it completes:
curl -X POST "https://storage.devd.org/api/v1/containers/contracts/files/file_XXXX/copy" \
-H "X-API-Key: sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"targetContainer": "archive", "newName": "agreement-2026.pdf"}'
{ "success": true, "data": { "job": {
"id": "job_YYYY", "operation": "copy", "status": "running",
"sourceItemId": "file_XXXX",
"destination": { "container": "archive", "name": "agreement-2026.pdf" },
"resultItemId": null, "error": null } }, ... }
# Poll
curl "https://storage.devd.org/api/v1/jobs/job_YYYY" -H "X-API-Key: sk_live_YOUR_KEY"
# → status becomes "completed" with "resultItemId": "file_ZZZZ"Job statuses: queued, running, completed, failed. Moves within the same storage backend complete immediately; moving between containers on different backends is not supported in the MVP (copy + delete instead).
Listing and pagination
GET /api/v1/containers/{container}/items
?folderId=folder_XXXX # or ?path=projects/2026
&limit=50 # 1-200
&cursor=... # from previous response's nextCursor
&type=file # file | folder
&name=report # case-insensitive substring match
&sort=name # name | size | modified
&order=asc # asc | descResponses include nextCursor when more results are available; pass it back unchanged. Note: type, name, and sort apply within each page in the MVP.