Files

Upload, inspect, download, rename, copy, move, and delete files inside your containers.

Endpoints

MethodPathDescriptionPermission
POST/api/v1/containers/{container}/filesUpload a filefiles.write
GET/api/v1/containers/{container}/files/{fileId}File information + metadatafiles.read
GET/api/v1/containers/{container}/files/{fileId}/contentDownload the file bodyfiles.read
PATCH/api/v1/containers/{container}/files/{fileId}Rename the filefiles.write
DELETE/api/v1/containers/{container}/files/{fileId}Delete the filefiles.delete
POST/api/v1/containers/{container}/files/{fileId}/copyCopy (async job)files.copy
POST/api/v1/containers/{container}/files/{fileId}/moveMove (job)files.move
POST/api/v1/containers/{container}/uploadsStart 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 sessionfiles.write
POST/api/v1/containers/{container}/direct-uploadsGet 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-urlGet a URL to download directly from storagefiles.read
GET/api/v1/containers/{container}/itemsList items (root or ?folderId/?path)files.read
GET/api/v1/jobs/{jobId}Poll a copy/move jobfiles.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 | desc

Responses include nextCursor when more results are available; pass it back unchanged. Note: type, name, and sort apply within each page in the MVP.