Skip to content

Upload protocol

The upload protocol, field by field, for a client written without a CCS library (the Node, web and Flutter clients implement it). Why it works this way is on Uploads.

POST P/uploads (or POST P/files/:file_id/versions for a new version of an existing file, with size, mime_type, content_sha256 and comment). Send an Idempotency-Key.

Field Required Meaning
name yes The file name; at most 255 characters
size yes Bytes, exactly what you will send
mime_type yes The declared type; checked against the policy now and against the bytes later
folder_id no Default root
on_name_conflict no fail (default: name_conflict) or rename
content_sha256 no Hex SHA-256 of the bytes
title, description, category, department, tags, custom, status, visibility, external_ref no Metadata, as on the file
policy no A named upload policy of the application
entries, inherit no Initial access entries, and whether to inherit the folder’s
notify_user_ids no Who gets file.ready besides the owner
owner_id, acting_user_id no, machine tokens only The user the file belongs to; the user recorded on the audit row

The answer, 201:

{
"upload_id": "01J9Z6S9Q5F5R6Y2W2GZ3B3K4E", "file_id": "01J9Z6S9Q5F5R6Y2W2GZ3B3K4D", "version_no": 1,
"status": "open", "policy_applied": "default", "method": "multipart",
"headers": { "Content-Type": "video/mp4" },
"part_size": 8388608, "parts_total": 25,
"parts": [ { "part_number": 1, "url": "https://…" }, { "part_number": 2, "url": "https://…" } ],
"expires_at": "2026-10-23T10:00:00.000Z"
}

A single PUT ticket has method: "put" and url instead of part_size, parts_total and parts.

method Send
put PUT url with the whole file as the body and exactly the ticket’s headers, nothing else: no Authorization, no other Content-Type
multipart For each part n from 1 to parts_total: PUT the bytes from (n − 1) × part_size (the last part is shorter) to that part’s URL, with no headers of your own. Keep the ETag response header of each
  • The URLs are opaque: depending on where the tenant’s files are stored they point at an object store or at the File Service’s own host, and the part URLs of one ticket may differ in form from another ticket’s. PUT to each URL exactly as given; never build, change or parse one (Where files are stored).
  • Parts can go in any order and in parallel. A failed PUT can be repeated.
  • The ticket holds the first 100 part URLs. POST P/uploads/:upload_id/parts with ?from=<part_number>&count=<at most 100> answers { parts, expires_at } for more, or fresh ones for expired URLs (each lives 1 hour).
  • A PUT to an expired URL answers 403 from the store; request new URLs and repeat the part.

GET P/uploads/:upload_id answers the ticket with status and parts_uploaded: [{ part_number, etag, size }] as the store has them. Upload the missing parts and complete. The ticket lives 1 hour for a single PUT and 24 hours for multipart.

POST P/uploads/:upload_id/complete:

{ "parts": [ { "part_number": 1, "etag": "\"9b2cf535f27731c974343645a3985328\"" }, { "part_number": 2, "etag": "\"…\"" } ] }

Send every part, with the ETag exactly as the store returned it (quotes included). A single PUT completes with an empty body. The answer is 200 with the file (state: "available", every processing step pending), or 202 with state: "pending_upload" and a job_id above 1,000 parts.

Answer Means
size_mismatch The stored size differs from the declared one. The object was deleted; create a new ticket
upload_incomplete A part is missing from the list or from the store
upload_expired The ticket’s lifetime ended
upload_not_open Already completed, being completed, or aborted

DELETE P/uploads/:upload_id answers 204 and releases the reserved quota at once. An expired ticket is cleaned up by the service on its own.

Step Caller
Create A machine token with files:manage, or a user with edit on the folder (on the file, for a version)
Status, parts, complete The ticket’s creator: the same user, or the same client
Abort The creator, or files.manage
The PUTs Anyone holding the URL: it is its own authorisation. Never forward it beyond the device that uploads