Skip to main content
The File Upload Example posts a whole file in one request. That works until the file is bigger than what a single request can carry — a proxy, gateway, or serverless function in front of your client will usually cap a request body long before Oxen does. The chunked upload API removes that ceiling. It’s the same protocol oxen push uses for large files: you hash the file, upload it in pieces, and ask the server to put those pieces back together. Because each piece is its own request, the file’s size stops mattering — only the size of one chunk does. It also does something the single-request upload can’t: stage the assembled file directly into a workspace, so it’s ready to commit — or to read back — without ever committing it.

The three steps

All three take your API key as a bearer token:

The version id is the file’s hash

{version_id} is not an id you invent. It is the XXH3-128 hash of the file’s complete contents, written as lowercase hexadecimal with leading zeros stripped — the format Rust’s {:x} produces for a u128. This matters because step 3 re-hashes the bytes it assembled and compares them to {version_id}. Get the hash wrong and the upload is rejected; get it right and you have a guarantee that what the server stored is exactly what you sent.
For files large enough that you don’t want them in memory, hash incrementally — every XXH3 implementation supports feeding it the file in pieces. Reuse the same pieces you’re about to upload and you read the file only once.

Step 1: Announce the upload

Tell the server what’s coming. dst_dir is optional and only used in step 3.

Request

A 200 means go ahead and upload chunks. A rejection means this content already exists in the version store — file contents are addressed by their hash, so there is nothing left to upload and you can skip to step 3.

Step 2: Upload the chunks

Send the file in slices. The body is the raw bytes of that slice — not multipart, not JSON — and offset is the slice’s byte position in the complete file.

Request

Output:
Because every chunk carries its own offset, chunks are independent: send them in any order, in parallel, and retry any one of them on its own without restarting the upload.
Keep each chunk at or below 10 MiB, the server’s transfer segment size. Below that, pick a size that fits whatever limit sits between you and the server — a chunk still has to survive one ordinary HTTP request.

Step 3: Reassemble

Ask the server to join the chunks into the version file. It verifies the count, then verifies the hash.

Request

Staging without committing

workspace_id is what makes this more than an upload. Provide it and the assembled file is staged into that workspace at dst_dir/file_name — it shows up in GET /workspaces/{workspace_id}/changes, it can be read back through GET /workspaces/{workspace_id}/files/{path}, and it becomes part of the next commit you make from that workspace. Leave workspace_id out and the bytes simply live in the version store, addressed by their hash, for you to reference later. This means a large file can be uploaded, inspected, and even discarded without ever entering the repository’s history.

Errors

Complete example

Uploads a file in 4 MB chunks and stages it into a workspace.
Python
Output:
From here, commit the workspace to turn the staged file into history:

When to use this

Reach for chunked upload when any of these is true:
  • The file is larger than one request can carry. The most common reason. Serverless platforms in particular cap request bodies at a few megabytes.
  • You want the upload to be resumable. Chunks are independent, so a failure costs you one chunk, not the whole file.
  • You want parallelism. Offsets make the order irrelevant.
  • You want the file staged but not committed. workspace_id puts it in a workspace, where you can read it back or throw it away without touching history.
For small files where none of this applies, the single-request file upload is simpler.