File Upload Chunked upload flow, web upload (URL import), status polling, and upload management.
Overview
Fastio supports four upload flows:
- Small files (< 4 MB) — Single-request upload. Send the file as a multipart chunk in the session creation request. Optionally auto-add to storage in the same call.
- Large files (≥ 4 MB) — Chunked upload. Create a session, upload chunks (up to 3 in parallel), trigger assembly, poll until complete.
- Stream upload — Upload a file of unknown size in a single request. Create a session with
stream=true, then POST the raw file body to the stream endpoint. - Web upload (URL import) — Import files from a public HTTPS URL. The server downloads and uploads the file asynchronously.
All direct upload flows produce an upload session with a unique id (OpaqueId). Once the upload reaches complete status, the file is ready to use.
Upload Constraints
| Constraint | Value |
|---|---|
| Single-call upload max size | 4 MB (4,194,304 bytes) |
| Chunk size | Varies by organization (query /upload/limits/ for exact values) |
| Last chunk | May be smaller than the chunk size |
| Max parallel chunk uploads per session | 3 |
| Max undersized chunks per session | 1 (a chunk below the minimum chunk size; make it the final chunk) |
| Chunk ordering | 1-based (first chunk is order=1) |
| Supported hash algorithms | md5, sha1, sha256, sha384, crc32c (recommended — see Integrity Checksums (CRC-32C)) |
relative_path max length | 8192 characters |
relative_path max length per segment | 255 characters, counted as characters not bytes — each path segment is itself capped, and a segment over the limit is rejected (never shortened); the error names the offending segment, its length, and the limit |
relative_path | Omit entirely if empty — do NOT send as empty string |
creator format | 1–150 chars, alphanumeric and hyphens only (/^[a-zA-Z0-9\-]+$/) |
| Max file size | Varies by organization (up to 100 GB) |
| Max concurrent sessions | Varies by organization (query /upload/limits/) |
| Long-poll max wait | 590 seconds |
| Stream upload | Exactly one stream upload allowed per stream-mode session |
Org access policy. When an organization has restricted access by location or network (see Access Policy (Geo / IP Restrictions) in the Organizations reference), every upload endpoint for a target owned by that org refuses a blocked caller with 403 geo_restricted — session create, chunk, stream, complete, and both the per-session upload/{id}/details and web_upload/{id}/details reads. An MCP-classified caller blocked by the org’s mcp_access policy gets 403 mcp_access_denied instead. The session itself is not deleted or cancelled by the refusal. A session that is dropped from an upload list for this reason still returns 403 geo_restricted from its own details call, never 404 — so a 404 there still means the session itself is gone, not that it was hidden by policy. If a session’s own target cannot itself be read to resolve the policy, the call fails 503 access_policy_unavailable (retryable) rather than passing through unchecked — this applies to the two details reads above and to the upload/web_upload session lists: a list request whose row-level policy check hits an unreadable target fails 503 for the whole request rather than silently including or excluding that row.
Upload Status Values
| Status | Meaning | Action |
|---|---|---|
ready | Session created, awaiting chunks | Upload chunks |
uploading | Chunks being received | Continue uploading |
assemble | Assembly queued | Keep polling |
assembling | Assembly in progress | Keep polling |
complete | Terminal success state for every path. With a target, the file is in storage and new_file_id is set; with no target, the file is held for a later addfile call. | Done (use addfile if there was no target) |
store | Reserved — not emitted by current uploads | Treat like storing if seen |
storing | Being imported to storage | Keep polling |
stored | Reserved — not emitted by current uploads | Treat like complete if seen |
assembly_failed | Assembly failed — including a whole-file CRC-32C mismatch, or a session whose uploaded chunks are no longer available (see session.integrity_failure) | Handle error |
store_failed | Storage import failed | Handle error |
Terminal states: complete, assembly_failed, store_failed. Stop polling when you reach one of these. Every successful path ends at complete.
State machine branches
A session takes one of two branches based on how it was created:
- Target path — request supplies a storage target (
action+instance_id). Path:uploading → assemble → assembling → storing → complete. Terminal:complete, withnew_file_idset; the file is accessible for download and preview. May instead end atassembly_failedorstore_failed. - No-target path — no
instance_idprovided. Path:uploading → complete(it may pass throughassemble/assembling). Terminal:complete. File is held for a lateraddfilecall.
Transition-Tracking Fields
Each session object carries three fields for distinguishing "still moving" from "stuck" when polling:
| Field | Type | Meaning |
|---|---|---|
updated_ms | integer | Millisecond-precision unix epoch of the most recent session update. Orders snapshots that share the second-precision updated value. |
state_epoch | integer | Monotonic counter that increments on every status change. Compare across polls to detect transitions that happen inside a single wall-clock second. |
assembling_deadline_ms | integer | Present only while status=assembling. Unix-ms deadline; treat now > assembling_deadline_ms && state_epoch unchanged as stuck. Before either condition holds, the session is still legitimately in flight. |
A short-lived assemble → assembling → storing → complete sequence can finish in well under a second for small chunk-based uploads, so a single poll can catch a session mid-transition. Do not treat a non-terminal status on the first poll as a failure.
Integrity Checksums (CRC-32C)
CRC-32C (Castagnoli) is the recommended integrity checksum for uploads. It is fast to compute, and per-chunk values can be combined into the whole-file value without reading the file a second time. md5, sha1, sha256 and sha384 keep working exactly as before.
- Format.
hash_algo=crc32cwithhashset to exactly 8 lowercase hex digits, in the standard big-endian rendering: the CRC-32C of the ASCII bytes123456789ise3069283, and of empty input00000000. Uppercase digits, a0xprefix, or a decimal value are rejected as an invalid hash. - Per chunk. Send
hash_algo=crc32c&hash={crc32c_hex}with eachPOST /current/upload/{upload_id}/chunk/. A chunk whose bytes do not match is rejected the same way as with any other algorithm ("The chunk failed to hash properly..."); re-send that chunk. - Whole body. On a single-call upload, a stream body, or a batch entry,
hash_algo=crc32c+hashis the CRC-32C of the whole body and is checked against the bytes received. - Whole file (chunked). Declare the CRC-32C of the entire file either at session creation (
hash_algo=crc32c+hash) or with the optionalfile_crc32cparameter onPOST /current/upload/{upload_id}/chunk/(only there — single-call, stream and batch uploads already carry a whole-body hash). Compute it by combining your per-chunk CRCs in chunk order with the zlibcrc32_combinemethod, using the CRC-32C polynomial (reflected0x82F63B78); no second pass over the file is needed. - Which chunk carries
file_crc32c. Send it on the first send of the chunk whose CRC you finish computing last — for a client that uploads chunks one after another, the last chunk — and on retries of that same chunk only. Never move it to a different chunk. The server must hold the value before the session has received every byte, so afile_crc32cthat arrives after that is refused. Resending the same value is harmless; a different value is refused with10779(HTTP 409). If you know the whole-file CRC-32C before you start, you can instead declare it at session creation. - Validation. When the upload is finalized, the server combines the CRC-32Cs of the chunks it stored and compares the result with your value, without re-reading the file. On a match the file is stored as normal. On a mismatch nothing is stored and the session ends
assembly_failed. The mismatch is reported in one of three shapes: (a) the completing chunk, the stream body, the single-call upload, or/completefails with an HTTP 406 error, code10778— as does any later chunk, stream or/completecall on that session; (b) a batch entry fails inline: the batch still returns HTTP 200, and that entry carriesstatus: "error"witherror_code10778; (c) session details still succeed, showingstatus: "assembly_failed"andintegrity_failure.error_code10778. The session can still be deleted. A mismatch is terminal — do not retry the session; create a new one and upload the file again. - A legacy whole-file hash still works alongside it. A session created with a
sha256(or other legacy) whole-filehashcan also receive afile_crc32c; each is checked on its own, and a legacy mismatch behaves as it always has. - Stored on the file. Every newly stored file records its whole-file CRC-32C, returned as
crc32con node and version details next to the unchangedhash(see the Storage reference). Compare it with your local value to confirm what was stored. Files stored before the platform began recording it reportnull. An upload that was already in progress when the server was updated may complete without the whole-file check, and its storedcrc32cmay benull.
Workflow: Small File Upload (< 4 MB)
A single request creates the session and uploads the file. Optionally auto-adds to storage.
Step 1: Upload in one request
Create upload session and send file data in a single request.
Content-Type: multipart/form-data
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | File name including extension (1–255 chars). |
| size | integer | Yes | File size in bytes (must match actual file) |
| chunk | file | Yes | The file binary data (multipart field) |
| action | string | No | "create" for new file, "update" for file replacement |
| instance_id | string | Required if action=create or update | Workspace or share profile ID (19-digit numeric) |
| file_id | string | Required if action=update | OpaqueId of the existing file to replace |
| if_version_id | string | No | Only with action=update. Compare-and-swap precondition: the version id (the file’s version field) you expect to be current. The replace lands only if it still matches; on a mismatch the session ends assembly_failed with status_message = CONFLICT_VERSION_MISMATCH:{current_version_id} — not a 409. A malformed id is 1605 (Invalid Input). See Optional Compare-and-Swap. |
| folder_id | string | No | Target folder OpaqueId or "root" for storage root |
| hash | string | No | Hash of the full file. Must be provided with hash_algo. |
| hash_algo | string | No | Hash algorithm: "md5", "sha1", "sha256", "sha384", or "crc32c" (recommended; 8 lowercase hex digits — see Integrity Checksums (CRC-32C)) |
| relative_path | string | No | For folder uploads, relative path for auto folder creation (max 8192 chars). Each path segment is itself capped at 255 characters, counted as characters not bytes; a segment over the limit is rejected (never shortened), with an error naming the offending segment, its length, and the limit. Omit entirely if not applicable. |
| org | string | No | Organization ID for billing limit resolution (only used when no action is specified) |
| creator | string | No | Client identifier string (1–150 chars, alphanumeric and hyphens only) |
Example (small file with auto-add to workspace)
curl -X POST "https://api.fast.io/current/upload/" \
-H "Authorization: Bearer {jwt_token}" \
-F "name=notes.txt" \
-F "size=1024" \
-F "action=create" \
-F "instance_id=1234567890123456789" \
-F "folder_id=root" \
-F "creator=my-web-client" \
-F "chunk=@notes.txt"
Response (201 Created)
{
"result": true,
"id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda",
"creator": "my-web-client",
"new_file_id": null
}
Response fields
| Field | Type | Description |
|---|---|---|
| result | boolean | true on success |
| id | string | Upload session OpaqueId |
| creator | string | Echoed back only if creator was provided in the request |
| new_file_id | string or null | Only present for single-call uploads with an upload target (instance_id). Usually null: the file is added to storage asynchronously after this response, so the node id is not yet known. When it is an OpaqueId, storage already completed within the request. |
When instance_id and folder_id are provided, the file is automatically added to storage — no addfile step is needed. Because storage completes asynchronously, get the new node id by long-polling GET /current/upload/{id}/details/?wait=60 until the session reaches a terminal status, then read session.new_file_id. Skip the poll only when the create response already carried a non-null new_file_id.
Response (without instance_id, 201 Created)
{
"result": true,
"id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda"
}
Without a target, only the upload session id is returned. Use addfile to place the file in storage after the upload completes.
Error responses
Reading the error tables: the four-digit 16xx/17xx values below are HTTP-status classes, not error.code. The error.code a client actually receives is assigned per endpoint, so use the HTTP status as the gate and a documented error.code — five or six digits, plus the 9661-9669 family — only as a refinement. A 16xx value identifies the status class — useful for telling which kind of failure occurred — but comparing one against error.code will never match. Five- and six-digit codes (and the 9661-9669 family) are real error.code values. If you widen a check from a specific code to a status, widen what you assert with it — a status covers failures the narrower code did not, so a message written for that one code becomes a confident falsehood on the rest.
| Error Code | HTTP Status | Message |
|---|---|---|
1605 (Invalid Input) | 406 | "The size was not supplied." |
1605 (Invalid Input) | 406 | "The file name is not valid." |
1605 (Invalid Input) | 406 | "The size is not valid." |
1605 (Invalid Input) | 406 | "Invalid share, workspace, or sign envelope instance ID." (action=update also accepts a File Share id: "Invalid share, workspace, sign envelope, or file share instance ID.") |
1609 (Not Found) | 404 | "No such folder exists in this share." — folder_id is missing, is not a folder, or lies outside the share (action=create on a share). |
1609 (Not Found) | 404 | "No such file exists in this share." — file_id is missing or lies outside the share (action=update on a share). |
1605 (Invalid Input) | 406 | "A note cannot be replaced with a binary upload." (action=update on a note) |
1658 (Not Acceptable) | 406 | The file type is not allowed for upload in this organization — only while extension enforcement is on (see enforcement_enabled on GET /current/upload/limits/extensions/; currently off). |
1685 (Feature Limit) | 412 | "The file size exceeds (4194304) single call upload, use chunks." |
1685 (Feature Limit) | 412 | "You have created too many upload sessions..." — params.reason = plan_limit. |
1685 (Feature Limit) | 412 | The size exceeds the organization’s maximum file size. params.reason = plan_limit. |
1685 (Feature Limit) | 412 | "The total size of all active upload sessions exceeds the limit." — params.reason = plan_limit. |
1605 (Invalid Input) | 406 | "The hash algorithm provided is not valid." |
1605 (Invalid Input) | 406 | "The hash provided is not valid." |
1605 (Invalid Input) | 406 | "The hash algorithm was provided but not the hash." |
1605 (Invalid Input) | 406 | "The chunk failed to hash properly, check the chunk hash and retry." — the uploaded bytes do not match hash. |
10778 | 406 | "The uploaded file did not match its whole-file CRC-32C, so it was not saved. Upload the file again in a new session." — terminal; see Integrity Checksums (CRC-32C). |
162098 | 406 | "The uploaded chunks of this session are no longer available, so the file was not saved. Upload the file again in a new session." — terminal; the session normally ends assembly_failed with integrity_failure.reason "chunk_references_released". Not retryable; upload the file again. |
1658 (Not Acceptable) | 406 | "We were unable to create the upload session..." |
1683 (Resource Missing) | 404 | "The upload session was deleted before the file could be stored." — the new session was deleted while the file was being processed (e.g. a concurrent DELETE /current/upload/{upload_id}/). Not retryable as-is; upload the file again. |
1693 (Temporarily Unavailable) | 503 | "This upload is busy. Please retry the upload shortly." — the file’s data is still being processed by another request; nothing was stored by this request. Retryable: retry after a short delay, backing off if it repeats (the other request usually finishes quickly, but a stalled one can hold the upload for several minutes). This and a store failure carry the same error.code, so tell them apart by HTTP status. |
Workflow: Large File Upload (Chunked)
Step 1: Create upload session
Create a new upload session for a large file.
Content-Type: application/x-www-form-urlencoded
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | File name including extension (1–255 chars). |
| size | integer | Yes | Total file size in bytes |
| action | string | No | "create" for new file, "update" for file replacement. Omit it to upload with no storage target (add the file later with addfile). |
| instance_id | string | Required if action=create or update | Workspace or share profile ID (19-digit numeric) for auto-add to storage after assembly |
| file_id | string | Required if action=update | OpaqueId of existing file to replace |
| if_version_id | string | No | Only with action=update. Compare-and-swap precondition: the version id (the file’s version field) you expect to be current. The replace lands only if it still matches; on a mismatch the session ends assembly_failed with status_message = CONFLICT_VERSION_MISMATCH:{current_version_id} — not a 409. A malformed id is 1605 (Invalid Input). See Optional Compare-and-Swap. |
| folder_id | string | No | Target folder OpaqueId or "root" for storage root |
| hash | string | No | Hex hash of the full file, computed with hash_algo, for integrity verification. Must be provided with hash_algo. |
| hash_algo | string | No | Hash algorithm: "md5", "sha1", "sha256", "sha384", or "crc32c". A crc32c value is the whole-file CRC-32C, checked when the upload is finalized against the CRCs of the stored chunks; session details report it as file_crc32c (not hash/hash_algo). You can instead send it later as file_crc32c on a chunk — see Integrity Checksums (CRC-32C). |
| relative_path | string | No | For folder uploads, relative path for auto folder creation (max 8192 chars). Each path segment is itself capped at 255 characters, counted as characters not bytes; a segment over the limit is rejected (never shortened), with an error naming the offending segment, its length, and the limit. Omit entirely if not applicable. |
| org | string | No | Organization ID for billing limit resolution (only used when no action is specified) |
| creator | string | No | Client identifier string (1–150 chars, alphanumeric and hyphens only) |
Example
curl -X POST "https://api.fast.io/current/upload/" \
-H "Authorization: Bearer {jwt_token}" \
-d "name=annual-report.pdf" \
-d "size=52428800" \
-d "action=create" \
-d "instance_id=1234567890123456789" \
-d "hash_algo=sha256" \
-d "hash=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
Response (201 Created)
{
"result": true,
"id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda"
}
When instance_id names a workspace or share owned by an organization restricting access by location or network, session creation refuses a blocked caller with 403 geo_restricted — see Org access policy above.
Step 2: Upload chunks
Upload a single chunk of file data.
Content-Type: multipart/form-data
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| {upload_id} | string | Yes | The upload session ID from Step 1 |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| order | integer | Yes | 1-based chunk number (first chunk = 1). Must not exceed the chunk limit. |
| size | integer | Yes | Size of this chunk in bytes. Must match the actual uploaded file size. |
| hash | string | No | Hash of this chunk. Must be provided with hash_algo. |
| hash_algo | string | No | Hash algorithm: "md5", "sha1", "sha256", "sha384", or "crc32c" (recommended) |
| file_crc32c | string | No | CRC-32C of the entire file (8 lowercase hex digits), checked when the upload is finalized. Send it on the first send of the chunk whose CRC you finish computing last (sequential clients: the last chunk) and on retries of that same chunk only — see Integrity Checksums (CRC-32C). Independent of this chunk's own hash/hash_algo. |
Request body (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
| chunk | file | Yes | Binary chunk data |
Upload up to 3 chunks in parallel. The last chunk may be smaller. Only 1 undersized chunk is allowed per session.
When all chunks have been uploaded (total bytes equal the declared file size), auto-finalization triggers automatically. You can still call the complete endpoint explicitly.
Example
curl -X POST "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/chunk/?order=1&size=5242880&hash_algo=sha256&hash=abc123def456..." \
-H "Authorization: Bearer {jwt_token}" \
-F "chunk=@chunk_001.bin"
Example (last chunk with CRC-32C, carrying the whole-file file_crc32c)
curl -X POST "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/chunk/?order=5&size=5242880&hash_algo=crc32c&hash={chunk_crc32c_hex}&file_crc32c={file_crc32c_hex}" \
-H "Authorization: Bearer {jwt_token}" \
-F "chunk=@chunk_005.bin"
Response (202 Accepted)
{
"result": true
}
Error responses
| Error Code | HTTP Status | Message |
|---|---|---|
1605 (Invalid Input) | 406 | "The session id provided is not valid." |
1658 (Not Acceptable) | 406 | "The session id provided is not in a valid state to accept a chunk." |
1658 (Not Acceptable) | 406 | "This session uses stream mode. Use the /param/stream/ endpoint instead of uploading chunks." — send stream-mode sessions to POST /current/upload/{upload_id}/stream/. |
1605 (Invalid Input) | 406 | "No order supplied" |
1605 (Invalid Input) | 406 | "Invalid order supplied" |
1605 (Invalid Input) | 406 | "The order provided for this chunk is not valid..." — when the order is beyond the organization’s maximum chunk count, params.reason = plan_limit. |
1605 (Invalid Input) | 406 | "The size was not supplied." |
1685 (Feature Limit) | 412 | The chunk size exceeds the organization’s maximum chunk size. params.reason = plan_limit. |
1685 (Feature Limit) | 412 | The order exceeds the organization’s maximum chunk count. params.reason = plan_limit. Not reached in practice: an order beyond the chunk count is refused first with the 406 above. |
1685 (Feature Limit) | 412 | Too many chunks in this session are smaller than the organization’s minimum chunk size — only one chunk per session (normally the last) may be undersized. |
1685 (Feature Limit) | 412 | "You have exceeded the maximum number of chunks..." — params.reason = plan_limit. |
1685 (Feature Limit) | 412 | "The combined chunk size exceeds the size for this session." |
1605 (Invalid Input) | 406 | "The upload chunk failed or was the wrong size..." |
1605 (Invalid Input) | 406 | "The chunk failed to hash properly..." |
1605 (Invalid Input) | 406 | "The file_crc32c provided is not valid; it must be 8 lowercase hex digits." |
1658 (Not Acceptable) | 406 | "The session id provided is not in a valid state to accept a whole-file CRC-32C." — file_crc32c arrived after the session had received every byte or left ready/uploading. Send it on the first send of the carrier chunk. |
10778 | 406 | "The uploaded file did not match its whole-file CRC-32C, so it was not saved. Upload the file again in a new session." — the whole-file CRC-32C did not match the stored chunks; the session is assembly_failed. Returned by the chunk that completed the file and by every later chunk on the session. Not retryable; start a new session. |
162098 | 406 | "The uploaded chunks of this session are no longer available, so the file was not saved. Upload the file again in a new session." — the session’s uploaded chunks were no longer available when the file was finalized; the session normally ends assembly_failed with integrity_failure.reason "chunk_references_released". Returned by the chunk that completed the file and normally by every later chunk on the session. Not retryable; start a new session and upload the whole file again. |
10779 | 409 | "A different whole-file CRC-32C is already set for this upload session. Resend the value sent first." — a retry must resend the original file_crc32c. |
1683 (Resource Missing) | 404 | "The id provided is not found..." — the upload session was deleted while this chunk was being processed (e.g. a concurrent DELETE /current/upload/{upload_id}/), including when a chunk already stored is re-sent. Not retryable; create a new session. |
1693 (Temporarily Unavailable) | 503 | "This chunk is busy. Please retry the chunk shortly." — the chunk order of this session is still being processed by another request (often an earlier attempt); nothing was stored by this request. Retryable: re-send the chunk after a short delay, backing off if it repeats (the other request usually finishes quickly, but a stalled one can hold the chunk for several minutes). This and the 500 below carry the same error.code, so tell them apart by HTTP status. |
1654 (Internal Error) | 500 | "The chunk failed to be stored..." |
| (generated per call site) | 403 | "Access to this organization is not permitted from your current location or network." — params.reason: "geo_restricted". See Access Policy in the Organizations reference. |
Step 3: Trigger assembly
Trigger asynchronous assembly of all uploaded chunks.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| {upload_id} | string | Yes | The upload session ID |
Query parameters (optional)
| Parameter | Type | Required | Description |
|---|---|---|---|
| hash | string | No | Final file hash. Checked for format only and not stored — to record a whole-file hash on the session, supply hash and hash_algo at session creation (or, for CRC-32C, file_crc32c on a chunk). Must be provided with hash_algo. |
| hash_algo | string | No | Hash algorithm: "md5", "sha1", "sha256", "sha384", or "crc32c" |
No body parameters required. If the session is already in a completed or processing state (complete, assemble, assembling, store, storing, stored), the endpoint returns 200 OK immediately without error.
Example
curl -X POST "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/complete/" \
-H "Authorization: Bearer {jwt_token}"
Response (202 Accepted)
{
"result": true
}
Error responses
| Error Code | HTTP Status | Message |
|---|---|---|
1683 (Resource Missing) | 404 | "The id provided is not found..." |
1658 (Not Acceptable) | 406 | "The session id provided is not in a valid state to assemble." |
1658 (Not Acceptable) | 406 | "No chunks have been uploaded to this session." |
1658 (Not Acceptable) | 406 | "The chunks provided do not match the size of the file." |
1685 (Feature Limit) | 412 | "You have created too many upload sessions..." — params.reason = plan_limit. |
10778 | 406 | "The uploaded file did not match its whole-file CRC-32C, so it was not saved. Upload the file again in a new session." — returned when this call’s finalization finds the mismatch, and on every later /complete call for a session that already failed it (session details still succeed). Not retryable; start a new session. |
162098 | 406 | "The uploaded chunks of this session are no longer available, so the file was not saved. Upload the file again in a new session." — returned when this call’s finalization finds the session’s uploaded chunks are no longer available, and normally on every later /complete call for that session (session details still succeed). Not retryable; start a new session and upload the whole file again. |
1678 (Enqueue Failed) | 500 | "Your request was valid but could not be processed." |
| (generated per call site) | 403 | "Access to this organization is not permitted from your current location or network." — params.reason: "geo_restricted". See Access Policy in the Organizations reference. |
Step 4: Poll for completion
Get upload session status with optional long-poll.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| {upload_id} | string | Yes | The upload session ID |
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| wait | integer | No | — | Long-poll wait time in seconds (1 to 590; larger values are capped at 590). Server holds the connection while the upload is still processing (assemble, assembling, or storing) — a session still in ready or uploading returns immediately — and returns as soon as it reaches a terminal status: complete on success (for uploads with a target, session.new_file_id then carries the new file's node ID) or assembly_failed/store_failed on failure. |
The server detects status changes efficiently during long-poll. Maximum wait is 590 seconds. A single wait call replaces polling: when it returns with status complete, the node ID is already in session.new_file_id.
Example
curl -X GET "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/details/?wait=60" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true,
"session": {
"status": "complete",
"id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda",
"updated": "2025-01-20 10:35:00 UTC",
"created": "2025-01-20 10:30:00 UTC",
"updated_ms": 1737369300412,
"state_epoch": 4,
"size": 52428800,
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"hash_algo": "sha256",
"filename": "annual-report.pdf",
"org": "1234567890123456789",
"target": {
"action": "create",
"instance_id": "1234567890123456789",
"folder_id": "root",
"relative_path": null,
"relative_id": null
},
"new_file_id": "2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4",
"chunks": {
"1": 10485760,
"2": 10485760,
"3": 10485760,
"4": 10485760,
"5": 10485760
}
}
}
Response fields
| Field | Type | Description |
|---|---|---|
| session.id | string | Upload session OpaqueId |
| session.filename | string | Filename |
| session.size | integer | Declared file size in bytes |
| session.status | string | Current status (see status table above) |
| session.hash | string | File hash (if provided; a crc32c whole-file hash is reported as file_crc32c instead) |
| session.hash_algo | string | Hash algorithm (if provided) |
| session.file_crc32c | string | The whole-file CRC-32C the server will check, from session creation (hash_algo=crc32c) or file_crc32c on a chunk. Absent until one is set. |
| session.integrity_failure | object | Present only on an assembly_failed session that failed for one of these reasons; branch on reason, and expect new reasons to be added. "file_crc32c_mismatch": the whole-file CRC-32C check failed; also carries expected_crc32c (your value), computed_crc32c (the CRC-32C of the bytes the server received), and error_code (10778, the same code the chunk and /complete calls return). "chunk_references_released": the session’s uploaded chunks were no longer available when it was finalized; carries no CRC fields, and error_code is 162098, the same code the chunk, stream and /complete calls on the session return. Both are terminal: upload the file again in a new session. |
| session.created | string | Session creation timestamp |
| session.updated | string | Last update timestamp |
| session.chunks | object | Map of chunk order (string key) to chunk size (integer value) |
| session.new_file_id | string | OpaqueId of created storage node (only when complete with a target) |
| session.status_message | string/null | Present on some terminal failures. For a File Share external-edit conflict it carries CONFLICT_VERSION_MISMATCH:{current_version_id} (see File Share External Edit below). |
| session.target | object/null | Upload-target descriptor; null when the session was created without a target. For a create it carries {action:"create", instance_id, folder_id, relative_path, relative_id} (folder_id is "root" or a raw folder id); for an update {action:"update", instance_id, file_id}. |
| session.org | string/null | Organization ID used for billing-limit resolution (from the target, or the org parameter); null when none applies. |
| session.updated_ms, session.state_epoch, session.assembling_deadline_ms | integer | Transition-tracking fields — see Transition-Tracking Fields above. |
| session.creator | string | Echoed client identifier; present only when creator was supplied at creation. |
| session.stream_mode | boolean | true for stream-mode sessions. Always present (and true) on a stream upload; omitted for non-stream sessions. |
| session.max_size | integer | Maximum size ceiling in bytes for a stream-mode session. Always present on a stream upload; omitted when no ceiling applies. |
Exit condition: Stop polling when status is a terminal state — complete, assembly_failed, or store_failed. See "State machine branches" above.
Whole-file CRC-32C mismatch. A session whose stored bytes did not match its file_crc32c ends like this; nothing was stored, so upload the file again in a new session:
{
"result": true,
"session": {
"status": "assembly_failed",
"id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda",
"size": 26214400,
"file_crc32c": "7a3c19e4",
"filename": "presentation.pptx",
"status_message": "The uploaded file did not match its whole-file CRC-32C. Please upload it again.",
"integrity_failure": {
"reason": "file_crc32c_mismatch",
"expected_crc32c": "7a3c19e4",
"computed_crc32c": "1c2f9a07",
"error_code": 10778
}
}
}
Uploaded chunks no longer available. A session whose uploaded chunks were no longer available when it was finalized normally ends like this; nothing was stored, and the chunk, stream and /complete calls on it normally return the same 162098. Do not retry the session or re-send its chunks; upload the whole file again in a new session:
{
"result": true,
"session": {
"status": "assembly_failed",
"status_message": "The uploaded chunks of this session are no longer available. Please upload the file again.",
"integrity_failure": {
"reason": "chunk_references_released",
"error_code": 162098
}
}
}
If the target org restricts access by location or network and the caller is currently blocked, this read refuses with 403 geo_restricted instead of returning session — the session itself is not deleted or cancelled by the refusal. A genuine 404 (Not Found) on this route still means the session is gone, never that it was hidden by policy. If the session’s own target cannot itself be read to resolve the policy, this read fails 503 access_policy_unavailable (retryable) instead of passing through unchecked.
Step 5 (if no instance_id): Add file to storage manually
Add a completed upload to workspace storage.
Or for shares:
Add a completed upload to share storage.
Path parameters
{workspace_id}or{share_id}— Profile ID (19-digit numeric string){folder_id}— OpaqueId of the target folder, or"root"for the storage root
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Filename for the new file. 1-255 characters (counted as characters, not bytes). |
| from | string (JSON) | Yes | Source specification as JSON-encoded string |
from format
from={"type":"upload","upload":{"id":"{upload_id}"}}
Example
curl -X POST "https://api.fast.io/current/workspace/1234567890123456789/storage/root/addfile/" \
-H "Authorization: Bearer {jwt_token}" \
-d 'name=annual-report.pdf' \
-d 'from={"type":"upload","upload":{"id":"5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda"}}'
Step 6 (optional): Clean up session
Delete an upload session and clean up temporary files.
Complete Chunked Upload Example
1. Create session (25 MB file, 5 chunks)
curl -X POST "https://api.fast.io/current/upload/" \
-H "Authorization: Bearer {jwt_token}" \
-d "name=presentation.pptx" \
-d "size=26214400" \
-d "action=create" \
-d "instance_id=1234567890123456789" \
-d "folder_id=root"
Response:
{"result": true, "id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda"}
2. Upload 5 chunks (3 in parallel, then 2 more)
# Chunks 1-3 in parallel
curl -X POST "https://api.fast.io/current/upload/5uhlp-.../chunk/?order=1&size=5242880" \
-H "Authorization: Bearer {jwt_token}" -F "chunk=@chunk1.bin" &
curl -X POST "https://api.fast.io/current/upload/5uhlp-.../chunk/?order=2&size=5242880" \
-H "Authorization: Bearer {jwt_token}" -F "chunk=@chunk2.bin" &
curl -X POST "https://api.fast.io/current/upload/5uhlp-.../chunk/?order=3&size=5242880" \
-H "Authorization: Bearer {jwt_token}" -F "chunk=@chunk3.bin" &
wait
# Chunks 4-5
curl -X POST ".../chunk/?order=4&size=5242880" \
-H "Authorization: Bearer {jwt_token}" -F "chunk=@chunk4.bin" &
curl -X POST ".../chunk/?order=5&size=5242880" \
-H "Authorization: Bearer {jwt_token}" -F "chunk=@chunk5.bin" &
wait
3. Trigger assembly
curl -X POST "https://api.fast.io/current/upload/5uhlp-.../complete/" \
-H "Authorization: Bearer {jwt_token}"
4. Poll until complete
curl -X GET "https://api.fast.io/current/upload/5uhlp-.../details/?wait=60" \
-H "Authorization: Bearer {jwt_token}"
Response: {"result": true, "session": {"status": "assembling", ...}} — keep polling
curl -X GET "https://api.fast.io/current/upload/5uhlp-.../details/?wait=60" \
-H "Authorization: Bearer {jwt_token}"
Response: {"result": true, "session": {"status": "complete", "new_file_id": "2ltsu-...", ...}} — done
5. Clean up session
curl -X DELETE "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda" \
-H "Authorization: Bearer {jwt_token}"
Resume a Disconnected Upload
If an upload is interrupted (network failure, client crash), resume it without re-uploading completed chunks.
Steps
-
Get session status:
curl -X GET "https://api.fast.io/current/upload/{upload_id}/details/" \ -H "Authorization: Bearer {jwt_token}" - Read the
chunksmap in the response. Keys are chunk numbers already uploaded, values are byte sizes. - Upload only missing chunks. Compare the
chunksmap against the expected chunk list. Upload any chunks not present. Ifsession.file_crc32cis already set, do not send a different value; if it is not set and you use one, sendfile_crc32cwith the missing chunk whose CRC you finish last. -
Trigger assembly:
curl -X POST "https://api.fast.io/current/upload/{upload_id}/complete/" \ -H "Authorization: Bearer {jwt_token}" - Poll for completion as normal.
File Update (New Version)
To upload a new version of an existing file:
- Create session with
action=update,instance_id, andfile_id(OpaqueId of the file to replace). - Upload chunks and complete as normal. The existing file receives a new version.
curl -X POST "https://api.fast.io/current/upload/" \
-H "Authorization: Bearer {jwt_token}" \
-F "name=report-v2.pdf" \
-F "size=1024" \
-F "action=update" \
-F "instance_id=1234567890123456789" \
-F "file_id=2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4" \
-F "chunk=@report-v2.pdf"
The instance_id for an update target may be a workspace, a share, or a File Share (see below).
Optional Compare-and-Swap (if_version_id)
To avoid clobbering a concurrent edit, supply an optional if_version_id precondition on the update session. The replace is applied only if the target file's current version equals that id. This is accepted and enforced on every update target — workspace, share, and File Share alike.
| Name | Type | Required | Description |
|---|---|---|---|
| if_version_id | string | No | OpaqueId of the version you expect to be current. The replace lands only if it still matches. |
Pass if_version_id at session creation (action=update) — the complete/ finalize call does not read it.
Version Conflict Surfacing
This is an asynchronous upload session, not the synchronous storage-update endpoints (POST .../storage/{node_id}/update/, which answer a 409 with error.params[] — see the Storage reference). Here, when if_version_id does not match the current version, the session ends terminally instead: status becomes assembly_failed and session.status_message carries CONFLICT_VERSION_MISMATCH:{current_version_id}. There is no 409 on this path. A polling client can parse the current version id off that prefix, re-read the file, and retry with the fresh if_version_id. This terminal state is not retried by the server — the client decides whether to re-attempt.
Read the file before concluding nothing was written. This status means the write was refused on the attempt that reported it. It is not a guarantee that the session as a whole wrote nothing: an internal retry can re-run the write against the base you supplied after an earlier attempt in the same session already applied it, and the conflict is then reported against your own committed version. Rebasing onto the returned id and re-uploading in that situation stores your content a second time. So on a conflict, fetch the file and compare it against what you intended to write; re-upload only if the content is not already there.
{
"result": true,
"session": {
"id": "57m2t-rba7h-bjv5m-yxi6q-lhb24-jexy",
"status": "assembly_failed",
"status_message": "CONFLICT_VERSION_MISMATCH:3dcdvao5jdflim47gxa5lygwotenm"
}
}
⚠️ STATUS (2026-08-26): the persistence defect is FIXED in code and covered by tests that re-read the stored row, but the fix is NOT yet confirmed on a live conflict. Until it is, treat status_message as the reliable conflict signal and the fields below as the intended contract rather than a measured one. This note is removed the moment a real conflict on a deployed build is observed to carry them.
Read session.conflict, not the status string. The same terminal conflict is also persisted as a structured object, and that is what a new client should consume:
"conflict": {
"type": "CONFLICT_VERSION_MISMATCH",
"base_version_id": "33qhg-tbqyj-wrmr2-ssdmv-ht4lr-hal5",
"current_version_id": "3dcdv-ao5jd-flim4-7gxa5-lygwo-tenm",
"candidate_id": "4itaflutlgy3jnwfck7v5lf62cujg",
"expires_at": "2026-08-27 04:00:00 UTC",
"permitted_actions": ["rebase", "discard"]
}
base_version_id is what you asserted; current_version_id is what the node actually moved to. You need both to decide whether to rebase or surface the conflict, and the colon-delimited status_message carries only the second. status_message is kept for clients already parsing it and is not going away, but it is a string with a separator, not a protocol — prefer conflict.
current_version_id is optional. When the store could not supply the current version — the target row was gone by the time the write was refused — the key is absent entirely rather than empty, because an empty string is not "unknown", it is an id you might send back. Test for the key before reading it; when it is missing, re-read the file to find out what state it is actually in. The same applies to candidate_id and expires_at, which are present only when content was retained.
conflict.candidate_download_url is deliberately absent. The only way to address content with no node attached is internal-network-only and unsupported for chunked uploads — which is exactly the large file you would most want back — so a key that could not be fetched is not emitted. candidate_id plus expires_at is what is real today.
Your upload MAY be recoverable — branch on the fields, never assume them. A conflicted session CAN carry candidate_phy_id and candidate_retained_until, meaning the bytes you sent are retained rather than discarded. They are frequently absent, and absent is the ordinary case. When the server can tell your if_version_id is stale before it has written anything, it refuses early and there is nothing to retain — no content was ever stored, so there is nothing lost either. The fields carry a value only when the write had already progressed far enough to store your content before losing the race.
⚠ permitted_actions is the signal to branch on, not the presence of the keys. It contains "discard" only when there is genuinely something to discard; a conflict with ["rebase"] alone means re-read and resend. candidate_retained_until is a real deadline when present — after it the content is reclaimed — but do not build a recovery flow that assumes it will be there.
The candidate is exposed on the SESSION, which belongs to whoever created the upload. That ownership is the authorization: nobody else can read these fields, including users with write access to the target file. It is your unaccepted draft, not the file's.
⚠ Ids appear in TWO renderings in this payload, and the hyphenated set is an allowlist. session.id, session.new_file_id, and the two version ids inside session.conflict (base_version_id, current_version_id) are emitted in the canonical hyphenated form (29 characters grouped in fives, 34 characters total). Every other OpaqueId in the session payload is emitted RAW — 29 lowercase alphanumeric characters, no hyphens — including session.target.file_id, session.candidate_phy_id, conflict.candidate_id, and any id carried inside status_message.
The two version ids are hyphenated because they are compare-and-swap tokens: current_version_id is what you hand back as the next if_version_id, and it reads exactly like the version field you took it from. conflict.candidate_id stays raw because it is the same value as session.candidate_phy_id, and one id must not read two ways within one response.
So the same node can appear both ways in one response (new_file_id hyphenated, target.file_id raw), and the same version appears hyphenated in conflict.current_version_id but raw in the status_message prefix.
⇒ Do not write a parser that requires either shape. Strip non-alphanumerics and lowercase before comparing or storing an id; the API accepts either form on input and canonicalises it, so an id read in one rendering can be sent back in the other. A new field added to the session payload defaults to RAW unless it is listed above.
⚠️ Migrating from status_message to conflict changes the rendering you display. The same version is raw in the status_message prefix and hyphenated in conflict.current_version_id, so a client that switches sources starts showing a different-looking id for the same version — and a user comparing today's output to yesterday's will reasonably conclude something moved. Two consequences worth planning for: normalise before you compare across the two sources (a direct string compare of the parsed status_message id against conflict.current_version_id returns false on a match, which is exactly the signal a compare-and-swap client acts on), and decide deliberately which rendering you show a human, rather than inheriting whichever source you happen to read. Both are correct ids for the same version.
File Share External Edit (Write-Back)
A holder of an edit grant on a durable File Share can replace the shared file's content — even without being a member of the owning workspace — by using the File Share id as the instance_id of an action=update upload session. The session targets the File Share's single bound file node; the bytes land in (and are metered to) the owning workspace's storage.
- Create session with
action=update,instance_id={fileshare_id}(the File Share's numeric id or its opaqueid_alt), andfile_id={bound_node_id}(the File Share's bound file, available asbound_node_idfrom the management list, orfile.idfrom the public details endpoint). - Upload chunks and complete as normal. The bound file receives a new version, and a
file_share_content_updatedevent is emitted.
curl -X POST "https://api.fast.io/current/upload/" \
-H "Authorization: Bearer {jwt_token}" \
-F "name=report-v2.pdf" \
-F "size=1024" \
-F "action=update" \
-F "instance_id=1234567890123456789" \
-F "file_id=2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4" \
-F "chunk=@report-v2.pdf"
Authorization. The write requires a named edit grant on the File Share (or an equivalent scope token) — workspace/org membership is not required, and no access tier alone confers write. In addition to the edit grant, the same access gate that governs reads still applies to the write: if the File Share is password-protected, the editor must present the link password via the x-ve-password request header, and the File Share's access tier (anyone_with_link / any_registered / named_people) is enforced. The file_id you supply must canonicalize to the File Share's bound node; any other node id resolves to 404 (siblings cannot be probed). The optional if_version_id compare-and-swap precondition documented above applies here the same as on any other update target.
Sign Envelope Target (Draft Documents)
A Sign Envelope in draft status is also an accepted upload target: pass the Sign Envelope id as instance_id on POST /current/upload/ — with action=create to add a draft document, or action=update to replace one. Uploads are permitted only while the envelope is a draft and require membership in the envelope's owning workspace with view access to the envelope; once the envelope leaves draft it no longer accepts document uploads. See the Signing reference for the envelope lifecycle and its document-management endpoints — the details here are limited to noting that the upload API accepts this target.
Stream Upload (Unknown File Size)
For clients that don't know the exact file size upfront (piped output, generated content, compressed streams). The client declares a maximum size ceiling, streams the file in a single request, and the system records actual bytes.
Step 1: Create stream session
Create a stream-mode upload session.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Filename including extension |
| stream | string | Yes | Must be "true" |
| max_size | integer | No | Maximum file size in bytes (defaults to the organization’s maximum file size) |
| action | string | No | "create" or "update" (same as standard upload) |
| instance_id | string | Conditional | Target workspace/share ID (required if action=create or update) |
| file_id | string | Conditional | File to update (required if action=update) |
| folder_id, relative_path, if_version_id, org | string | No | Same as the standard upload session parameters |
| hash | string | No | Expected whole-file hash |
| hash_algo | string | No | Hash algorithm: "md5", "sha1", "sha256", "sha384", or "crc32c" (recommended — a crc32c value is checked when the stream finalizes; see Integrity Checksums (CRC-32C)) |
| creator | string | No | Client identifier |
Example
curl -X POST "https://api.fast.io/current/upload/" \
-H "Authorization: Bearer {jwt_token}" \
-d "name=output.tar.gz" \
-d "stream=true" \
-d "max_size=52428800" \
-d "action=create" \
-d "instance_id=1234567890123456789"
Response (201 Created)
{
"result": true,
"id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda"
}
Step 2: Stream file body
Upload the entire file as a raw binary stream.
Send the raw file bytes as the request body with Content-Type: application/octet-stream. No size or order parameters needed.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| {upload_id} | string | Yes | The upload session ID from Step 1 |
Query parameters (optional)
| Parameter | Type | Required | Description |
|---|---|---|---|
| hash | string | No | Whole-file hash for validation. Must be provided with hash_algo. |
| hash_algo | string | No | Hash algorithm: "md5", "sha1", "sha256", "sha384", or "crc32c" |
Example
curl -X POST "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/stream/" \
-H "Authorization: Bearer {jwt_token}" \
-H "Content-Type: application/octet-stream" \
--data-binary @myfile.tar.gz
Response (201 Created)
{
"result": true
}
The session auto-finalizes after the stream completes. The session's size field is updated to reflect the actual bytes received.
Error responses
| Error Code | HTTP Status | Message |
|---|---|---|
1605 (Invalid Input) | 406 | "The session id provided is not valid." |
1658 (Not Acceptable) | 406 | "This session was not created with stream mode enabled. Use the chunk endpoint instead." |
1658 (Not Acceptable) | 406 | "The session id provided is not in a valid state to accept a stream upload." |
1658 (Not Acceptable) | 406 | "A stream upload is already in progress for this session." |
1658 (Not Acceptable) | 406 | "A stream has already been uploaded for this session." |
1605 (Invalid Input) | 406 | "The stream upload was interrupted or contained no data." |
1605 (Invalid Input) | 406 | "The uploaded file is smaller than the minimum allowed size." |
1685 (Feature Limit) | 412 | "The uploaded file exceeds the maximum size for this session." |
1605 (Invalid Input) | 406 | "The file failed to hash properly, check the file hash and retry." |
10778 | 406 | "The uploaded file did not match its whole-file CRC-32C, so it was not saved. Upload the file again in a new session." — the body did not match the crc32c hash declared at session creation; the session is assembly_failed. Not retryable; start a new session. |
162098 | 406 | "The uploaded chunks of this session are no longer available, so the file was not saved. Upload the file again in a new session." — the session’s uploaded data was no longer available when the stream was finalized; the session normally ends assembly_failed, and later stream calls on it normally return this code. Not retryable; start a new session. |
1683 (Resource Missing) | 404 | "The id provided is not found..." — the upload session was deleted while the stream was being processed (e.g. a concurrent DELETE /current/upload/{upload_id}/). Not retryable; create a new session. |
1693 (Temporarily Unavailable) | 503 | "This upload is busy. Please retry the stream upload shortly." — the session’s data is still being processed by another request (often an earlier attempt); this request stored no chunk data. Retryable: retry the stream after a short delay, backing off if it repeats (the other request usually finishes quickly, but a stalled one can hold the upload for several minutes). If a retry after this 503 answers 406 "A stream has already been uploaded for this session", the earlier request stored the data; check the session’s status instead of re-sending. This and the 500 below carry the same error.code, so tell them apart by HTTP status. |
1654 (Internal Error) | 500 | "The stream upload failed to be stored, please retry your upload or contact us if this persists." |
Notes
- The
max_sizeparameter is used for quota validation at session creation. If omitted, defaults to the organization's maximum file size. - The actual uploaded bytes must not exceed
max_size. - Stream mode sessions produce exactly one chunk and finalize automatically — no
/complete/call is needed. With a target, the session still moves throughassemble/assembling/storingtocomplete; long-pollGET /current/upload/{upload_id}/details/?wait=60fornew_file_id. - If you know the exact file size, you can still provide
sizeinstead ofmax_size(or both). - Stream upload is a single-shot operation: you cannot stream to the same session twice.
- Stream mode sessions cannot use the chunk endpoint — attempting to upload chunks to a stream session will return an error. Use the
/stream/endpoint instead. - Only one concurrent stream upload is allowed per session; concurrent requests to the same session are rejected.
Batch Upload (Many Small Files)
Submit 1–200 small files in a single request. Returns a per-file result array, so partial success is legible and does not abort the batch. Use this when you have many small files destined for the same workspace or share — one rate-limit cost instead of one per file. For files over 4 MB, keep using the standard chunked POST /current/upload/ flow.
Batch Limits
| Limit | Value |
|---|---|
| Files per batch | 1–200 |
| Max per-file size | 4 MB (4,194,304 bytes) |
| Max request body | 100 MB (applied post-base64-decode on the JSON path) |
| Supported hash algorithms | md5, sha1, sha256, sha384, crc32c |
| Status record TTL | 1 hour from POST |
Create batch (multipart)
Submit a batch of up to 200 small files via multipart/form-data.
Content-Type: multipart/form-data
Authentication: Required. Anonymous callers are not supported on this endpoint — use POST /current/upload/ for anonymous public-receive / public-exchange share uploads.
Batch-level fields
| Field | Type | Required | Description |
|---|---|---|---|
| instance_id | string | Yes | Target workspace or share profile ID (19-digit numeric). Every file in the batch lands in this target. |
| folder_id | string | No | Destination folder for the whole batch: OpaqueId of a folder under instance_id, or the literal "root". Omit (or pass "root") to land files at the target's storage root. For workspace-folder shares, an omitted folder_id resolves to the share's configured storage-root folder; a supplied folder_id always overrides that default. |
| creator | string | No | Optional echo-back correlation tag (1–150 chars, alphanumeric and hyphens only). |
| manifest | string (JSON) | Yes | JSON-encoded array of per-file manifest entries (see below). |
| file_{index} | file | Yes (one per manifest entry) | Binary file body. The suffix matches the index in the manifest entry. |
Manifest entry schema
| Field | Type | Required | Description |
|---|---|---|---|
| index | integer | Yes | 0-based position. Indices must be contiguous from 0 to N−1 with no duplicates. |
| filename | string | Yes | File name (1–255 chars). |
| relative_path | string | No | Optional per-entry sub-path applied under folder_id. 1–8192 characters, counted as characters not bytes; a path over the limit is rejected (never shortened). Segments are separated by / or \; leading, trailing, and repeated separators are ignored, so a trailing slash is optional and a leading slash does not make the path absolute — the path always resolves under folder_id. Every segment becomes a literal folder name, so a .. segment creates a folder named .. rather than moving up a level. Each path segment is itself capped at 255 characters, counted as characters not bytes; a segment over the limit is rejected as a per-entry error (only that file fails; the rest of the batch still uploads), with an error naming the offending segment, its length, and the limit. Matches the validation rules on POST /current/upload/ relative_path. |
| hash_algo | string | No | "md5", "sha1", "sha256", "sha384", or "crc32c" (recommended) for optional integrity validation. |
| hash | string | No | Hex digest of the uploaded bytes (for crc32c, exactly 8 lowercase hex digits). |
Hash validation is opt-in per entry: supply both hash_algo and hash, or neither. A mismatch errors only that entry; the batch still returns 200.
Example
curl -X POST "https://api.fast.io/current/upload/batch/" \
-H "Authorization: Bearer {jwt_token}" \
-F "instance_id=1234567890123456789" \
-F "creator=my-importer" \
-F 'manifest=[{"index":0,"filename":"doc-001.txt"},{"index":1,"filename":"doc-002.txt"}]' \
-F "file_0=@doc-001.txt" \
-F "file_1=@doc-002.txt"
Create batch (JSON)
Fallback body shape for clients that cannot compose multipart.
Content-Type: application/json
Base64 inflation adds ~33% to the wire size and forces the server to hold decoded bytes in memory while parsing; prefer multipart (which streams directly to disk) for non-trivial payloads.
Body schema
{
"instance_id": "1234567890123456789",
"folder_id": "{folder_opaque_id}",
"creator": "my-importer",
"files": [
{"filename": "doc-001.txt", "content_b64": "SGVsbG8sIHdvcmxkIQ=="},
{"filename": "doc-002.txt", "relative_path": "2026/q1/", "content_b64": "U2Vjb25kIGZpbGU="}
]
}
Each files entry accepts the same optional relative_path, hash_algo, and hash fields as the multipart manifest. The array position is the entry's logical index. folder_id (batch-level) and relative_path (per-entry) follow the same semantics as on POST /current/upload/.
Example
curl -X POST "https://api.fast.io/current/upload/batch/" \
-H "Authorization: Bearer {jwt_token}" \
-H "Content-Type: application/json" \
-d '{
"instance_id": "1234567890123456789",
"creator": "my-importer",
"files": [
{"filename": "doc-001.txt", "content_b64": "SGVsbG8sIHdvcmxkIQ=="},
{"filename": "doc-002.txt", "content_b64": "U2Vjb25kIGZpbGU="}
]
}'
Response (200 OK)
Always 200 OK on a well-formed batch. Inspect count_errored to detect per-file failures.
{
"result": true,
"batch_id": "{batch_id}",
"count_total": 2,
"count_succeeded": 1,
"count_errored": 1,
"creator": "my-importer",
"results": [
{
"index": 0,
"filename": "doc-001.txt",
"status": "ok",
"upload_id": "{upload_id}",
"node_id": "{node_id}"
},
{
"index": 1,
"filename": "doc-002.txt",
"status": "error",
"error_code": 196420,
"error_message": "hash_algo and hash must both be provided for a manifest entry."
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| batch_id | string | Opaque batch identifier for the GET status lookup (valid for 1 hour). |
| count_total | integer | Files submitted. |
| count_succeeded | integer | Files with status: "ok". |
| count_errored | integer | Files with status: "error". |
| creator | string | Echoed back only if creator was supplied. |
| results[].index | integer | Matches the submitted manifest index (or array position for the JSON path). |
| results[].filename | string | Submitted filename. |
| results[].status | string | "ok" or "error". |
| results[].upload_id | string | Present on ok. Upload session OpaqueId. |
| results[].node_id | string or null | Present on ok. OpaqueId when finalize completed inline; null when storage is async (node id assigned later when async finalization completes — matches single-file /upload/'s new_file_id: null). |
| results[].error_code | integer | Present on error. A unique 6-digit diagnostic code identifying the specific per-file failure (e.g. 196420 for a manifest entry missing one of hash/hash_algo). These are batch-specific inline codes, distinct from the platform error codes returned by POST /current/upload/ — except 10778 (whole-file CRC-32C mismatch) and 162098 (the entry’s uploaded data no longer available), which are the same codes those endpoints return. |
| results[].error_message | string | Present on error. Human-readable description. |
Per-file errors are reported inline on each entry — the batch itself returns HTTP 200 even if every file errored; check count_errored. The per-file shape is {error_code, error_message} and is intentionally flat (one error per file). It does not carry the envelope-level params array used by whole-batch validation rejections; the per-file error_code is a unique 6-digit diagnostic code specific to the batch endpoint, not one of the platform error-reference codes — except 10778 (whole-file CRC-32C mismatch) and 162098 (uploaded data no longer available), which are the same codes the other upload endpoints return.
Whole-batch error responses
These are returned in the standard error envelope (no results[]).
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | Unsupported Content-Type (must be multipart/form-data or application/json). |
1605 (Invalid Input) | 406 | instance_id missing, not numeric, or not a valid workspace/share ID. |
1605 (Invalid Input) | 406 | manifest not valid JSON, empty, or entries malformed. |
1605 (Invalid Input) | 406 | Manifest index values not contiguous from 0, or contain duplicates. |
1605 (Invalid Input) | 406 | Manifest filename fails validation. |
1605 (Invalid Input) | 406 | JSON body missing files array or files empty. |
1605 (Invalid Input) | 406 | creator fails length or character-set validation. |
1685 (Feature Limit) | 412 | Batch contains more than 200 files. |
1685 (Feature Limit) | 412 | Request body exceeds 100 MB (pre-parse Content-Length, or post-decode total for JSON). |
1685 (Feature Limit) | 412 | The organization’s maximum file size is below the 4 MB per-file bound. params.reason = plan_limit. |
1685 (Feature Limit) | 412 | The batch would exceed the organization’s active upload-session count or aggregate in-flight session size. params.reason = plan_limit. |
1680 (Access Denied) | 401 | Caller not authorized to upload to the target (anonymous callers are rejected here — use POST /current/upload/ for anonymous public-receive / public-exchange share uploads). |
1605 (Invalid Input) | 406 | folder_id malformed (not "root" and not a valid OpaqueId), or a per-entry relative_path malformed at validation time. Rejects the whole batch. |
1605 (Invalid Input) | 406 | folder_id does not resolve to a folder under instance_id, or caller lacks write permission on it. |
Per-file error causes
Recorded in results[] with status: "error"; the batch returns 200 OK.
- Missing / empty
file_{index}part (multipart) or missing / emptycontent_b64(JSON). - Base64 decode failure (JSON path).
- File over the 4 MB per-file bound — use
POST /current/upload/instead. - File type (extension or MIME) restricted for the organization (only while extension enforcement is on).
hash_algonot supported orhashlength wrong for the declared algorithm.hash_algosupplied withouthash(or vice versa).- Uploaded bytes do not match the declared hash.
- The stored file did not match the entry’s
crc32chash when it was finalized. This entry carrieserror_code10778(the same code as the single-file and chunked endpoints) and nothing was stored for it; resubmit the file. - The entry’s uploaded data was no longer available when it was finalized. This entry carries
error_code162098(the same code as the single-file and chunked endpoints) and nothing was stored for it; resubmit the file. - Internal storage / finalization failure for the single entry.
- The request reached the server’s per-request processing budget before this entry was started (large batches only). These entries carry
error_code190079, and theerror_messageasks you to resubmit the file; it was not stored.
Fetch batch status
Re-fetch the stored result record for a prior batch.
Authentication is not required for this endpoint — the opaque batch_id is the only credential. Treat batch_id as bearer-equivalent; transport over HTTPS and do not log alongside identifiers.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| {batch_id} | string | Yes | The opaque batch identifier returned by a previous POST. |
Example
curl -X GET "https://api.fast.io/current/upload/batch/{batch_id}/"
Response (200 OK)
{
"result": true,
"batch_id": "{batch_id}",
"creator": "my-importer",
"count_total": 2,
"count_succeeded": 1,
"count_errored": 1,
"results": [
{ "index": 0, "filename": "doc-001.txt", "status": "ok", "upload_id": "{upload_id}", "node_id": "{node_id}" },
{ "index": 1, "filename": "doc-002.txt", "status": "error", "error_code": 196420, "error_message": "hash_algo and hash must both be provided for a manifest entry." }
],
"created_ts": 1745000000
}
Error responses
| Error Code | HTTP Status | Cause |
|---|---|---|
1605 (Invalid Input) | 406 | batch_id missing or not a valid opaque identifier. |
1609 (Not Found) | 404 | No record found, or the 1-hour TTL elapsed. 404 is returned for both cases; callers cannot distinguish "never existed" from "expired". |
Notes
- The batch endpoint is rate-limited in an independent bucket from
POST /current/upload/. A client doing bulk uploads and then a chunked upload is not double-charged. - Each successful file produces an
upload_session_createdevent — downstream consumers (search indexing, AI pipelines) see the same event stream as N independentPOST /current/upload/calls. - Partial success is the documented contract. A failure on file 37 does not roll back files 0–36; they are already persisted.
- Files over 4 MB in the batch return per-file errors pointing back to
POST /current/upload/— the rest of the batch still processes. - A large batch can stop early when the request reaches the server’s per-request processing budget. Entries already processed keep their results; every entry that was not started comes back with
status: "error"and a retryable message asking you to resubmit that file. Match onerror_code190079(not the message text) to find them, and resubmit only those entries in a new batch — do not resend the whole batch, since entries reportedokare already stored. Re-number the resubmitted entries’indexfrom 0 in the new manifest, with matchingfile_{index}parts, because indices must be contiguous from 0.
Web Upload (URL Import)
Import files from a public HTTPS URL. Supports OAuth-protected URLs (Google Drive, OneDrive, Dropbox, Box, iCloud). The server downloads the file in the background and streams it through the standard upload pipeline.
Create web upload job
Create a new web upload job to import a file from a URL.
Content-Type: application/x-www-form-urlencoded
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| source_url | string | Yes | URL to download the file from. Max 2048 characters. Must be https:// on the default port (443) with a fully qualified domain name; plain HTTP, IP-address hosts, credentials embedded in the URL, and hosts that do not resolve to public addresses are refused. |
| file_name | string | Yes | Filename to save as (1–255 chars). |
| profile_id | string | Yes | Target workspace or share profile ID (19-digit numeric) |
| profile_type | string | Yes | "workspace" or "share" |
| folder_id | string | No | Target folder OpaqueId or "root" for storage root |
| relative_path | string | No | Relative path for automatic folder creation (1–8192 chars). Each path segment is itself capped at 255 characters, counted as characters not bytes; a segment over the limit is rejected (never shortened), with an error naming the offending segment, its length, and the limit. |
| options | integer | No | Non-negative integer bitfield (default: 0). Stored and echoed back on the job, but currently has no effect on how the import is processed. |
| creator | string | No | Client identifier string (1–150 chars, alphanumeric and hyphens only) |
Example
curl -X POST "https://api.fast.io/current/web_upload/" \
-H "Authorization: Bearer {jwt_token}" \
-d "source_url=https://example.com/files/document.pdf" \
-d "file_name=document.pdf" \
-d "profile_id=1234567890123456789" \
-d "profile_type=workspace" \
-d "folder_id=root"
Response (201 Created)
{
"result": true,
"web_upload": {
"id": "5dpct-ajic7-kyc2q-fpism-fg4xc-2ehf",
"user_id": "1234567890123456789",
"profile_id": "1234567890123456789",
"profile_type": "workspace",
"source_url": "[redacted]",
"file_name": "document.pdf",
"folder_id": null,
"relative_path": null,
"status": "queued",
"status_description": "Queued for processing",
"bytes_downloaded": 0,
"expected_size": null,
"upload_session_id": null,
"async_job_id": null,
"creator": null,
"error_message": null,
"options": 0,
"created": "2025-01-26 15:00:00 UTC",
"updated": "2025-01-26 15:00:00 UTC"
}
}
Response fields
| Field | Type | Description |
|---|---|---|
| web_upload.id | string | Web upload job OpaqueId |
| web_upload.user_id | string | ID of the user who created the job |
| web_upload.profile_id | string | Target workspace or share ID |
| web_upload.profile_type | string | "workspace" or "share" |
| web_upload.source_url | string | Always the literal [redacted]. The submitted URL is never returned on any response, because it routinely carries a credential (a provider OAuth token from the cloud picker, or a presigned signature). The field is retained so the response shape is stable. |
| web_upload.file_name | string | Destination filename |
| web_upload.folder_id | string/null | Target folder OpaqueId |
| web_upload.relative_path | string/null | Relative path for folder creation |
| web_upload.status | string | Status string (e.g., "queued", "downloading", "complete") |
| web_upload.bytes_downloaded | integer | Bytes downloaded so far (0 initially) |
| web_upload.expected_size | integer/null | Expected file size (from HEAD request, if known) |
| web_upload.upload_session_id | string/null | Linked upload session ID (populated during uploading phase) |
| web_upload.async_job_id | string/null | The async job ID processing this upload |
| web_upload.status_description | string | Human-readable status description |
| web_upload.creator | string/null | Echoed-back client identifier supplied at creation, or null if none was supplied. |
| web_upload.error_message | string/null | Error details if failed |
| web_upload.options | integer | Options bitfield |
| web_upload.created | string | Creation timestamp |
| web_upload.updated | string | Last update timestamp |
Error responses
| Error Code | HTTP Status | Message |
|---|---|---|
1605 (Invalid Input) | 406 | "Only HTTPS URLs are supported." |
1605 (Invalid Input) | 406 | "IP addresses are not allowed; a fully qualified domain name is required." |
1605 (Invalid Input) | 406 | "A fully qualified domain name is required." |
1605 (Invalid Input) | 406 | "URL is not supported." — non-443 port, credentials in the URL, a blocked host, or a host that does not resolve to public addresses. |
1605 (Invalid Input) | 406 | "Invalid profile_type. Must be \"workspace\" or \"share\"." |
1680 (Access Denied) | 401 | "You do not have permission to upload to this workspace." |
1680 (Access Denied) | 401 | "You do not have permission to upload to this share." |
1658 (Not Acceptable) | 406 | "You have too many active web uploads..." |
1654 (Internal Error) | 500 | "Failed to create web upload job." |
OAuth for protected URLs
For Google Drive, OneDrive, and other OAuth-protected files, include the access token as a query parameter in the source URL:
https://www.googleapis.com/drive/v3/files/{fileId}?alt=media&access_token={oauth_token}
The server extracts the token from the URL, removes it from the query string, and sends it as an Authorization: Bearer header on all HTTP requests. Tokens are never logged or returned in API responses.
List web upload jobs
List all web upload jobs for the current user.
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| limit | integer | No | 50 | Maximum number of results (1–100) |
| offset | integer | No | 0 | Pagination offset |
| status | string | No | — | Filter by status: "pending", "queued", "downloading", "uploading", "complete", "failed", "canceled" |
Example
curl -X GET "https://api.fast.io/current/web_upload/?status=downloading&limit=20" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true,
"web_uploads": [
{
"id": "5dpct-ajic7-kyc2q-fpism-fg4xc-2ehf",
"user_id": "1234567890123456789",
"profile_id": "1234567890123456789",
"profile_type": "workspace",
"source_url": "[redacted]",
"file_name": "document.pdf",
"folder_id": null,
"relative_path": null,
"status": "downloading",
"status_description": "Downloading file from URL",
"bytes_downloaded": 5242880,
"expected_size": 52428800,
"progress_percent": 10,
"upload_session_id": null,
"error_message": null,
"created": "2025-01-26 15:00:00 UTC",
"updated": "2025-01-26 15:01:00 UTC"
}
],
"total": 1,
"limit": 20,
"offset": 0
}
Response fields
| Field | Type | Description |
|---|---|---|
| web_uploads | array | Array of web upload job objects |
| web_uploads[].id | string | Web upload job OpaqueId |
| web_uploads[].profile_type | string | "workspace" or "share" |
| web_uploads[].status | string | Status string (e.g., "downloading", "complete") |
| web_uploads[].status_description | string | Human-readable status description |
| web_uploads[].bytes_downloaded | integer | Bytes downloaded so far |
| web_uploads[].expected_size | integer/null | Expected file size (null if unknown) |
| web_uploads[].progress_percent | integer | Download progress percentage (0–100) |
| web_uploads[].upload_session_id | string/null | Linked upload session ID |
| web_uploads[].error_message | string/null | Error details if failed |
| total | integer | Total count of matching records |
| limit | integer | Applied limit |
| offset | integer | Applied offset |
Get web upload job details
Get details for a specific web upload job.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| {upload_id} | string | Yes | The web upload job OpaqueId |
Example
curl -X GET "https://api.fast.io/current/web_upload/5dpct-ajic7-kyc2q-fpism-fg4xc-2ehf/details/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true,
"web_upload": {
"id": "5dpct-ajic7-kyc2q-fpism-fg4xc-2ehf",
"user_id": "1234567890123456789",
"profile_id": "1234567890123456789",
"profile_type": "workspace",
"source_url": "[redacted]",
"file_name": "document.pdf",
"folder_id": null,
"relative_path": null,
"status": "complete",
"status_description": "Upload complete",
"bytes_downloaded": 52428800,
"expected_size": 52428800,
"upload_session_id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda",
"async_job_id": "apkti-6i76h-6o4xr-5aqkj-egxgu-zmrc",
"creator": null,
"error_message": null,
"options": 0,
"created": "2025-01-26 15:00:00 UTC",
"updated": "2025-01-26 15:02:00 UTC"
}
}
Note: Both the details and list endpoints return status as string values (e.g., "pending", "queued", "downloading", "uploading", "complete", "failed", "canceled").
Error responses
| Error Code | HTTP Status | Message |
|---|---|---|
1609 (Not Found) | 404 | "Web upload job not found." |
1680 (Access Denied) | 401 | "You do not have permission to view this web upload job." |
| (generated per call site) | 403 | "Access to this organization is not permitted from your current location or network." — The job is not cancelled by the refusal; a genuine 404 still means the job is gone. See Access Policy in the Organizations reference. |
Cancel web upload job
Cancel an active web upload job.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | The web upload job OpaqueId to cancel |
Example
curl -X DELETE "https://api.fast.io/current/web_upload/?id=5dpct-ajic7-kyc2q-fpism-fg4xc-2ehf" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true,
"canceled": true,
"id": "5dpct-ajic7-kyc2q-fpism-fg4xc-2ehf"
}
Error responses
| Error Code | HTTP Status | Message |
|---|---|---|
1609 (Not Found) | 404 | "Web upload job not found." |
1680 (Access Denied) | 401 | "You do not have permission to cancel this web upload job." |
1658 (Not Acceptable) | 406 | "This web upload job cannot be canceled because it is already in a terminal state." |
1654 (Internal Error) | 500 | "Failed to cancel web upload job." |
Web Upload Status Values
| Status | Value | Description | Terminal |
|---|---|---|---|
pending | 1 | Job created, waiting for async job pickup | No |
queued | 2 | Async job has been queued for processing | No |
downloading | 3 | Actively downloading from the source URL | No |
uploading | 4 | Feeding downloaded chunks to upload system | No |
complete | 5 | Upload successfully completed | Yes |
failed | 6 | Download or upload failed | Yes |
canceled | 7 | User canceled the web upload | Yes |
Web Upload Limits
| Limit | Value |
|---|---|
| Max active per user | 50 (non-terminal jobs) |
| Max file size | Up to 100 GB (subject to the organization’s limits) |
| Max retries | 3 (automatic on transient failures) |
| Retry delay | 60 seconds between attempts |
Upload Management Endpoints
List all upload sessions
Returns all upload sessions for the current user in any state.
Example
curl -X GET "https://api.fast.io/current/upload/details/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true,
"sessions": [
{
"id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda",
"filename": "document.pdf",
"size": 52428800,
"status": "uploading",
"hash": "e3b0c44298fc1c14...",
"hash_algo": "sha256",
"created": "2025-01-20 10:30:00 UTC",
"updated": "2025-01-20 10:35:00 UTC"
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| results | integer | Total number of sessions (only present when > 1) |
| sessions | array | Array of upload session objects — the same fields as session on GET /current/upload/{upload_id}/details/, without chunks. |
Delete/cancel an upload session
Cancel and delete an active upload session.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| {upload_id} | string | Yes | The upload session ID to delete (appended to URL path) |
Cleans up temporary chunk files and releases session quota. If the session has an associated web upload job, that job is automatically canceled.
Sessions can be deleted in states: ready, uploading, assembly_failed, store_failed, complete. Sessions in assemble, assembling, store, storing, or stored states cannot be deleted.
Example
curl -X DELETE "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true
}
Error responses
| Error Code | HTTP Status | Message |
|---|---|---|
1605 (Invalid Input) | 406 | "The id provided is not found or is not associated with your account." |
1658 (Not Acceptable) | 406 | "The session id provided is not in a valid state to delete." |
1693 (Temporarily Unavailable) | 503 | "The upload session is finalizing. Please retry the delete shortly." (retryable) |
1654 (Internal Error) | 500 | "We were unable to delete the requested upload session." |
Get upload limits
Returns the upload limits that apply to the caller and the target context.
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| action | string | No | — | "create" or "update" to get limits in context of a target |
| org | string | No | — | Organization ID for limit resolution (used when no action specified) |
| instance_id | string | Required if action=create or update | — | Target workspace or share ID |
| folder_id | string | No | — | Target folder OpaqueId or "root" |
| file_id | string | Required if action=update | — | File ID for update context |
Example
# General limits (with org context)
curl -X GET "https://api.fast.io/current/upload/limits/?org=1234567890123456789" \
-H "Authorization: Bearer {jwt_token}"
# Limits for creating a file in a workspace
curl -X GET "https://api.fast.io/current/upload/limits/?action=create&instance_id=1234567890123456789" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true,
"limits": {
"chunk_size": 262144000,
"size": 107374182400,
"chunks": 1000,
"sessions": 10000,
"sessions_size_max": 1099511627776
}
}
Response fields
| Field | Type | Description |
|---|---|---|
| limits.chunk_size | integer | Maximum size of a single chunk in bytes |
| limits.size | integer | Maximum total file size in bytes |
| limits.chunks | integer | Maximum number of chunks per upload session |
| limits.sessions | integer | Maximum concurrent active upload sessions |
| limits.sessions_size_max | integer | Maximum aggregate size of all active sessions in bytes |
Get restricted file extensions
Returns restricted and archive file extensions. Authentication is optional — unauthenticated requests fall back to the most restrictive (unpaid) extension policy baseline. This baseline does not make an organization usable — a new organization must have billing set up to operate.
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| plan | string | No | User's plan or "unpaid" | Override the billing plan to check restrictions for. Takes a plan id from GET /current/org/billing/plan/list/; an unknown id is rejected with an input error. The legacy value "free" is still accepted and resolves to "unpaid", which is what the response echoes back. |
Example
curl -X GET "https://api.fast.io/current/upload/limits/extensions/"
# With specific plan
curl -X GET "https://api.fast.io/current/upload/limits/extensions/?plan={plan_id}"
Response (200 OK)
{
"result": true,
"restricted_extensions": [".exe", ".apk", ".jar", ".php"],
"archive_extensions": [".7z", ".zip", ".rar", ".tar.gz", ".bz2"],
"enforcement_enabled": false,
"plan": "unpaid",
"cache_ttl": 86400
}
Response fields
| Field | Type | Description |
|---|---|---|
| restricted_extensions | string[] | Extensions blocked for the plan |
| archive_extensions | string[] | Archive extensions (only populated if the plan restricts archives) |
| enforcement_enabled | boolean | Whether extension restriction enforcement is currently active. While false the lists are informational only: uploads are not refused by extension or file type. |
| plan | string | The plan used for this response |
| cache_ttl | integer | Suggested client-side cache TTL in seconds (86400 = 24 hours) |
Clients should call this once on startup and cache the results for 24 hours.
List supported hash algorithms
Returns the list of supported hash algorithms for upload integrity verification.
Example
curl -X GET "https://api.fast.io/current/upload/algos/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true,
"algos": ["md5", "sha1", "sha256", "sha384", "crc32c"]
}
crc32c is the recommended client integrity checksum — see Integrity Checksums (CRC-32C).
Get chunk information
Returns information about uploaded chunks for a session.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| order | integer | No | Specific chunk number to retrieve. If omitted, returns all chunks. |
Example (all chunks)
curl -X GET "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/chunk/" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK, all chunks)
{
"result": true,
"chunks": {
"1": 5242880,
"2": 5242880,
"3": 2097152
}
}
To retrieve a specific chunk, you can also append the chunk number to the path:
Returns information about a specific uploaded chunk by path parameter.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| {upload_id} | string | Yes | The upload session ID |
| {order} | integer | Yes | Specific chunk number to retrieve |
Example (single chunk by path)
curl -X GET "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/chunk/1" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK, single chunk)
{
"result": true,
"chunk": {
"1": 5242880
}
}
Error responses
| Error Code | HTTP Status | Message |
|---|---|---|
1609 (Not Found) | 404 | "The supplied chunk not valid or found." |
Delete a chunk
Delete a specific chunk from an upload session. Session must be in uploading state.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| order | integer | Yes | The chunk number to delete |
Example
curl -X DELETE "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/chunk/?order=3" \
-H "Authorization: Bearer {jwt_token}"
Response (200 OK)
{
"result": true
}
Error responses
| Error Code | HTTP Status | Message |
|---|---|---|
1658 (Not Acceptable) | 406 | "The session id provided is not in a valid state to delete a chunk." |
1693 (Temporarily Unavailable) | 503 | "The chunk could not be deleted due to a temporary failure. Please retry the delete." (retryable; this and the 500 below carry the same error.code, so tell them apart by HTTP status) |
1654 (Internal Error) | 500 | "We were unable to delete the requested upload session chunk." |
Quick Reference
Small file (one request, auto-add)
POST /current/upload/
multipart: name, size, chunk, action=create, instance_id, folder_id
-> 201: {id, new_file_id} # new_file_id usually null (finalization is asynchronous)
GET /current/upload/{id}/details/?wait=60 # When new_file_id was null
-> 200: {session: {status, new_file_id}}
Large file (chunked)
POST /current/upload/ # Create session
form: name, size, action=create, instance_id, folder_id
-> 201: {id}
POST /current/upload/{id}/chunk/?order=N&size=N # Upload chunks (up to 3 parallel)
multipart: chunk
query (recommended): hash_algo=crc32c, hash; file_crc32c on the carrier chunk
-> 202
POST /current/upload/{id}/complete/ # Trigger assembly
-> 202
GET /current/upload/{id}/details/?wait=60 # Poll until complete
-> 200: {session: {status, new_file_id}}
DELETE /current/upload/{id} # Clean up session
-> 200
Stream upload (unknown file size)
POST /current/upload/ # Create stream session
form: name, stream=true, max_size, action=create, instance_id
-> 201: {id}
POST /current/upload/{id}/stream/ # Stream file body
body: raw binary (application/octet-stream)
-> 201 (auto-finalizes)
Manual add to storage (if no instance_id)
POST /current/workspace/{id}/storage/{folder}/addfile/
form: name, from={"type":"upload","upload":{"id":"{upload_id}"}}
-> 200
Batch upload (many small files)
POST /current/upload/batch/ # Submit up to 200 small files
multipart: instance_id, creator?, manifest (JSON), file_0..file_N
(or) application/json: {instance_id, creator?, files: [{filename, content_b64, hash_algo?, hash?}]}
-> 200: {batch_id, count_total, count_succeeded, count_errored, results[]}
GET /current/upload/batch/{batch_id}/ # Fetch status record (1-hour TTL)
-> 200: {batch_id, creator?, count_total, count_succeeded, count_errored, results[], created_ts}
Web upload (URL import)
POST /current/web_upload/ # Create job
form: source_url, file_name, profile_id, profile_type
-> 201: {web_upload}
GET /current/web_upload/ # List jobs
query: limit, offset, status
-> 200: {web_uploads, total}
GET /current/web_upload/{id}/details/ # Get job details
-> 200: {web_upload}
DELETE /current/web_upload/?id={id} # Cancel job
-> 200: {canceled, id}
Upload management
GET /current/upload/details/ # List all sessions
GET /current/upload/limits/ # Get upload limits
GET /current/upload/limits/extensions/ # Get restricted extensions
GET /current/upload/algos/ # List hash algorithms
GET /current/upload/{id}/chunk/ # Get all chunk info
GET /current/upload/{id}/chunk/{order} # Get single chunk info
DELETE /current/upload/{id}/chunk/?order=N # Delete a chunk
Best Practices
- Check limits first: Query
/upload/limits/and/upload/limits/extensions/before starting uploads. - Use hash validation: Always provide chunk and file hashes to detect corruption early.
- Default to CRC-32C: Send
hash_algo=crc32cwith each chunk’s CRC-32C, and send the whole-file value — your per-chunk CRCs combined in order, no second pass — asfile_crc32con the first send of the chunk whose CRC completes last (sequential clients: the last chunk), and on that chunk’s retries only. Treat10778as terminal (re-upload in a new session) and10779as a client bug (resend the original value). After completion, compare the file’scrc32cin node details with your local value. - Implement retry logic: Failed chunk uploads can be retried by re-uploading the same
order. - Track chunks locally: Maintain a local record of successfully uploaded chunks for resumability.
- Long-poll for completion: Use the
waitparameter on the details endpoint instead of frequent polling. - Clean up failures: DELETE failed sessions to free session quota.
- Cache extension restrictions: Call
/upload/limits/extensions/once and cache for 24 hours. - Use auto-finalization: When all chunks total the declared file size, assembly triggers automatically. Explicit
/complete/is optional but recommended for reliability. - Omit
relative_pathwhen unused: Do NOT send it as an empty string.