Files and uploads

The v3 API never receives file content, and no operation accepts multipart/form-data. Your client sends the bytes directly to the storage service through presigned URLs that the API issues. The API manages the upload session and returns a file_id when the upload is complete.

Upload a file

  1. Open an upload session with the purpose, file name, size, and, if available, the checksum:
POST /api/v3/uploads
Idempotency-Key: 5f7d9f0a-3f8f-4a14-8b5c-6f9d2e0c1a77

{"purpose": "pdf", "filename": "handbook.pdf", "size_bytes": 10485760, "content_type": "application/pdf", "sha256": "9f86d0..."}

The response includes the session id (upl_...), part_size, parts_total, and expires_at. Use one of these purposes: image, pdf, import, editor, document.

  1. Request URLs for the parts you are about to send (POST /uploads/{upload_id}/parts with part_numbers). Each URL accepts a PUT request sent directly to the storage service and is valid for 15 minutes. Keep the ETag that the storage service returns for each part.

  2. Complete the upload with the list of parts and their ETags (POST /uploads/{upload_id}/completions). The response is the upload session with status completed and a file object containing the file id (file_...), extension, size, content_type, and checksum.

  3. Use the file_id in the resource that needs it, such as a lesson attachment, a showcase image, or an import spreadsheet.

While the session is open, GET /uploads/{upload_id} returns its current status and GET /uploads/{upload_id}/parts lists the parts the storage service has already received. To abandon an upload, send PATCH /uploads/{upload_id} with status: "cancelled".

You can send large files in parts, in parallel, and resume an interrupted upload: the session survives network failures, and sending the same part again is safe. An open session expires after 24 hours.

Download a file

Downloads always go through a short-lived link, never through a URL stored in a resource:

POST /api/v3/files/{file_id}/download-links

{"ttl_seconds": 300, "inline": false, "download_name": "handbook.pdf"}

The response includes url, method, and expires_at. A link is valid for at most 900 seconds, which is also the default when you omit ttl_seconds.

GET /files lists the files in your account and can be filtered by purpose and by creation date (created_after, created_before). Results are always sorted from newest to oldest; the sort parameter has no effect. DELETE /files/{file_id} permanently deletes the file from storage and from the account's file inventory. Deleted files are not moved to the trash and cannot be restored.

For every field and parameter, see the Uploads and Files references.

Limits and errors

LimitValue
File size5 GiB
Part URL lifetime15 minutes
Download link lifetime900 seconds
Open session lifetime24 hours
CodeWhen
upload_integrity_mismatchThe received bytes do not match the declared size, checksum, or content type. The exact cause is in details[].reason. The session is marked as failed, and you must open a new one.
state_conflictThe session was already completed, cancelled, failed, or expired.
storage_unavailableThe storage service did not respond. Retry with backoff.
validation_failedThe purpose is not supported, the size exceeds the limit, or a part number is out of range.

Presigned URLs are never written to logs, audit entries, or events. They are kept only in the idempotency receipt, which lasts 48 hours, so a replay can return a URL that has already expired. To get a valid URL, send the request again with a new Idempotency-Key.

On this page