Skip to content
Core concepts

Files and large uploads

One request for a file up to 64 MiB; a larger one in parts that resume after a dropped connection.

A file up to 64 MiB (67,108,864 bytes) is one multipart request, POST /api/files, with spaceId (and channelId to post it in a channel) beside the file. Anything larger — up to your plan’s per-file limit — is sent in parts: start it, send each part, then finish it. Every request then stays well inside the most one request to Kemble may carry: 100 MB, Cloudflare’s limit, given here in Cloudflare’s own unit. A part that did not arrive is sent again on its own; nothing that arrived is sent twice.

curl https://kemble.io/api/files/uploads \
  -H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
  -d '{"spaceId":"'$SPACE'","fileName":"keynote.mov","mimeType":"video/quicktime","sizeBytes":157286400}'
# → 201 {"upload":{"id":"…","partBytes":16777216,"partCount":10,"receivedParts":[],"state":"open","expiresAt":"…"}}

curl -X PUT https://kemble.io/api/files/uploads/$UPLOAD/parts/1 \
  -H "authorization: Bearer $TOKEN" -H "content-type: application/octet-stream" \
  --data-binary @part-1

curl -X POST https://kemble.io/api/files/uploads/$UPLOAD/complete -H "authorization: Bearer $TOKEN"

Start

POST /api/files/uploads takes spaceId, an optional channelId, fileName, mimeType and sizeBytes. Every check a one-request upload makes is made here, before a byte is sent: the per-file limit, your permission to upload where it lands, a channel’s rules for files, and blocked file types. The size you declare is held against the workspace’s storage from this moment until the upload is finished, cancelled or expires, so two uploads cannot both take the last of it; past the storage, the start is refused with 413 file.quota_exceeded.

Parts

  • PUT /api/files/uploads/:upload/parts/:part with the raw bytes and content-type application/octet-stream. Parts are numbered from 1 to partCount and may be sent in any order, or several at once.
  • Every part but the last is exactly partBytes long (16 MiB, 16,777,216 bytes); the last is exactly what remains. A wrong length is refused with 422 upload.part_size, and a body longer than a part with 413, before it is stored.
  • Sending a part again replaces it. A request without a valid token is answered 401, and an upload id that is not yours 404, before its body is read.
  • GET /api/files/uploads/:upload answers which parts are in (receivedParts): after a dropped connection, send only the others.

Finish, cancel, expiry

  • POST /api/files/uploads/:upload/complete joins the parts once all are in (409 upload.parts_missing names the missing ones otherwise). It answers 201 with the file, which you then attach to a message by its id as with POST /api/files.
  • A picture or a video answers 202 while the details its camera recorded (where it was taken, the device) are removed, exactly as for a one-request upload. Ask GET /api/files/uploads/:upload until its state is done (the file is in upload.file) or failed (upload.failure says why). Finishing again answers what it already became.
  • DELETE /api/files/uploads/:upload cancels it and discards its parts (204). Once finished it is a file: delete the file instead.
  • An upload has 24 hours to finish. After that a part or a finish is answered 410 upload.expired, and the upload and its parts are removed.
  • In a channel shared into your workspace from another organization, name the channel in the address (?channelId=) on every step, as on POST /api/files.

When file storage is busy

Now and then the storage behind Kemble is busy for a moment. Kemble waits and tries again itself first; if it is still busy, the request is answered 503 storage.busy with a Retry-After header (and details.retryAfterSeconds, the same number of seconds). Nothing was refused, and it is safe to send the same request again: wait that long, then send it — the same part, the same finish, the same one-request upload. The same answer can come from any request that reads or writes a file, a picture or an emoji, and it also comes when the storage is still working 85 seconds after the request arrived, so every request is answered before the 100-second limit of the network in front of Kemble. An app, a bot or an integration should retry it a few times, waiting longer each time; an upload in parts keeps every part that arrived, so only the one that was answered 503 is sent again.

HTTP/1.1 503 Service Unavailable
Retry-After: 5
content-type: application/json

{"error":{"code":"storage.busy","message":"File storage is busy for a moment. Try again shortly.","details":{"retryAfterSeconds":5}}}

Uploads count against the workspace’s storage like any file. There is no limit on how many uploads one person may have going at once; the storage is the only bound.

In a Kemble Work channel whose managers turned off “Members can download files here”, GET /api/files/:file answers downloadable: false; a picture, a video or audio keeps a viewing link, and any other file answers url: null. Apps are never handed such a file in a command’s file option.