> Part of the Fastio API Reference. Overview: https://api.fast.io/current/llms/ # File Upload Base URL: `https://api.fast.io/current/` All upload endpoints require authentication unless noted: `Authorization: Bearer {jwt_token}` Request format: `multipart/form-data` for file data, `application/x-www-form-urlencoded` for non-file requests. --- ## Overview Fastio supports four upload flows: 1. **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. 2. **Large files (>= 4 MB)** -- Chunked upload. Create a session, upload chunks (up to 3 in parallel), trigger assembly, poll until complete. 3. **Stream upload** -- Upload a file of unknown size in a single request (stream mode). Create a session with `stream=true`, then POST the raw file body to the stream endpoint. 4. **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 | Plan-dependent (query `/upload/limits/` for exact values) | | Last chunk | May be smaller than the plan chunk size | | Max parallel chunk uploads per session | 3 | | Max undersized chunks per session | 1 (a chunk below the plan's 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 | Plan-dependent (up to 100 GB) | | Max concurrent sessions | Plan-dependent (Starter and Business 5,000; Enterprise 10,000) | | Long-poll max wait | 590 seconds | | Stream upload | Exactly one stream upload allowed per stream-mode session | **Org access policy.** When an Enterprise org has restricted access by location or network (see *Access Policy (Geo / IP Restrictions)* in `llms/orgs.txt`), 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 (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`, with `new_file_id` set; the file is accessible for download and preview. May instead end at `assembly_failed` or `store_failed`. - **No-target path** -- no `instance_id` provided. Path: `uploading -> complete` (it may pass through `assemble` / `assembling`). Terminal: `complete`. File is held for a later `addfile` call. ### 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=crc32c` with `hash` set to exactly 8 lowercase hex digits, in the standard big-endian rendering: the CRC-32C of the ASCII bytes `123456789` is `e3069283`, and of empty input `00000000`. Uppercase digits, a `0x` prefix, or a decimal value are rejected as an invalid hash. - **Per chunk.** Send `hash_algo=crc32c&hash={crc32c_hex}` with each `POST /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` + `hash` is 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 optional `file_crc32c` parameter on `POST /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 zlib `crc32_combine` method, using the CRC-32C polynomial (reflected `0x82F63B78`); 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 a `file_crc32c` that arrives after that is refused. Resending the same value is harmless; a different value is refused with `10779` (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 `/complete` fails with an HTTP 406 error, code `10778` -- as does any later chunk, stream or `/complete` call on that session; (b) a batch entry fails inline: the batch still returns HTTP 200, and that entry carries `status: "error"` with `error_code` `10778`; (c) session details still succeed, showing `status: "assembly_failed"` and `integrity_failure.error_code` `10778`. 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-file `hash` can also receive a `file_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 `crc32c` on node and version details next to the unchanged `hash` (see the Storage reference). Compare it with your local value to confirm what was stored. Files stored before the platform began recording it report `null`. An upload that was already in progress when the server was updated may complete without the whole-file check, and its stored `crc32c` may be `null`. --- ## 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 ``` POST /current/upload/ Content-Type: multipart/form-data Authorization: Bearer {jwt_token} ``` **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 | | `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) | **curl example (small file with auto-add to workspace):** ```bash 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):** ```json { "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):** ```json { "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. Codes shown as five or six digits (and the `9661`-`9669` family) ARE `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 | "This file type is not allowed for upload on your plan." -- 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..." | | `1685 (Feature Limit)` | 412 | "The size is too large for the account plan." | | `1685 (Feature Limit)` | 412 | "The total size of all active upload sessions exceeds the 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)*. | | `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. | --- ## Workflow: Large File Upload (Chunked) ### Step 1: Create upload session ``` POST /current/upload/ Content-Type: application/x-www-form-urlencoded Authorization: Bearer {jwt_token} ``` **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 | | `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) | **curl example:** ```bash 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):** ```json { "result": true, "id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda" } ``` When `instance_id` names a workspace or share owned by an Enterprise org 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 ``` POST /current/upload/{upload_id}/chunk/?order={n}&size={bytes} Content-Type: multipart/form-data Authorization: Bearer {jwt_token} ``` **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 plan's 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. **curl example:** ```bash 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" ``` **curl example (last chunk with CRC-32C, carrying the whole-file `file_crc32c`):** ```bash 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):** ```json { "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..." | | `1605 (Invalid Input)` | 406 | "The size was not supplied." | | `1685 (Feature Limit)` | 412 | "The size is too large for the account plan." | | `1685 (Feature Limit)` | 412 | "The `order` specified exceeds the maximum chunk limit for the account plan." | | `1685 (Feature Limit)` | 412 | "The size is too small for the account plan." | | `1685 (Feature Limit)` | 412 | "You have exceeded the maximum number of chunks..." | | `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. | | `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. | | `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 `llms/orgs.txt`. | ### Step 3: Trigger assembly ``` POST /current/upload/{upload_id}/complete/ Authorization: Bearer {jwt_token} ``` **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. Triggers asynchronous assembly of all uploaded chunks. 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. **curl example:** ```bash curl -X POST "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/complete/" \ -H "Authorization: Bearer {jwt_token}" ``` **Response (202 Accepted):** ```json { "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..." | | `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. | | `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 `llms/orgs.txt`. | ### Step 4: Poll for completion ``` GET /current/upload/{upload_id}/details/?wait=60 Authorization: Bearer {jwt_token} ``` **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`. **curl example:** ```bash 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):** ```json { "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 its whole-file CRC-32C check: `reason` (`"file_crc32c_mismatch"`), `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). | | `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: ```json { "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 } } } ``` 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 ``` POST /current/workspace/{workspace_id}/storage/{folder_id}/addfile/ ``` or ``` POST /current/share/{share_id}/storage/{folder_id}/addfile/ ``` **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}"}} ``` The value must be a JSON string sent as a form field: ```bash 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 /current/upload/{upload_id} Authorization: Bearer {jwt_token} ``` --- ## Complete Chunked Upload Example **1. Create session (25 MB file, 5 chunks):** ```bash 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: ```json {"result": true, "id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda"} ``` **2. Upload 5 chunks (3 in parallel, then 2 more):** ```bash # Chunks 1-3 in parallel curl -X POST "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/chunk/?order=1&size=5242880" \ -H "Authorization: Bearer {jwt_token}" -F "chunk=@chunk1.bin" & curl -X POST "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/chunk/?order=2&size=5242880" \ -H "Authorization: Bearer {jwt_token}" -F "chunk=@chunk2.bin" & curl -X POST "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/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:** ```bash curl -X POST "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/complete/" \ -H "Authorization: Bearer {jwt_token}" ``` **4. Poll until complete:** ```bash curl -X GET "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/details/?wait=60" \ -H "Authorization: Bearer {jwt_token}" ``` Response: `{"result": true, "session": {"status": "assembling", ...}}` -- keep polling ```bash curl -X GET "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda/details/?wait=60" \ -H "Authorization: Bearer {jwt_token}" ``` Response: `{"result": true, "session": {"status": "complete", "new_file_id": "2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4", ...}}` -- done **5. Clean up session:** ```bash curl -X DELETE "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda" \ -H "Authorization: Bearer {jwt_token}" ``` --- ## Workflow: 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 ```bash POST /current/upload/ ``` | Parameter | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Filename (1-255 chars). | | `stream` | string | Yes | Must be `"true"` | | `max_size` | integer | No | Maximum file size in bytes (defaults to plan limit) | | `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 (`crc32c` recommended -- a `crc32c` value is checked when the stream finalizes; see *Integrity Checksums (CRC-32C)*) | | `creator` | string | No | Client identifier | **Example:** ```bash curl -X POST "https://api.fast.io/current/upload/" \ -H "Authorization: Bearer $TOKEN" \ -d "name=output.tar.gz" \ -d "stream=true" \ -d "max_size=52428800" \ -d "action=create" \ -d "instance_id=1234567890123456789" ``` **Response:** `201 Created` -- returns session `id` for use in step 2. ### Step 2: Stream File Body ```bash POST /current/upload/{upload_id}/stream/ Content-Type: application/octet-stream ``` Send the raw file bytes as the request body. No `size` or `order` parameters needed. Pass the optional hash parameters in the query string. | Parameter | Type | Required | Description | |---|---|---|---| | `hash` | string | No | Whole-file hash for validation | | `hash_algo` | string | No | Hash algorithm (`md5`, `sha1`, `sha256`, `sha384`, or `crc32c`) | **Example:** ```bash curl -X POST "https://api.fast.io/current/upload/$SESSION_ID/stream/" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary @myfile.tar.gz ``` **Response:** `201 Created` (`{"result": true}`) -- the session auto-finalizes. The session's `size` is updated to 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. | | `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. | | `1654 (Internal Error)` | 500 | "The stream upload failed to be stored, please retry your upload or contact us if this persists." | ### Notes - The `max_size` parameter is used for quota validation at session creation. If omitted, defaults to the plan'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 through `assemble` / `assembling` / `storing` to `complete`; long-poll `GET /current/upload/{upload_id}/details/?wait=60` for `new_file_id`. - If you know the exact file size, you can still provide `size` instead of `max_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. - Only one concurrent stream upload is allowed per session; concurrent requests to the same session are rejected. --- ## Resume a Disconnected Upload If an upload is interrupted (network failure, client crash), resume it without re-uploading completed chunks. ### Steps: 1. **Get session status:** ```bash curl -X GET "https://api.fast.io/current/upload/{upload_id}/details/" \ -H "Authorization: Bearer {jwt_token}" ``` 2. **Read the `chunks` map** in the response. Keys are chunk numbers already uploaded, values are byte sizes. 3. **Upload only missing chunks.** Compare the `chunks` map against the expected chunk list. Upload any chunks not present. If `session.file_crc32c` is already set, do not send a different value; if it is not set and you use one, send `file_crc32c` with the missing chunk whose CRC you finish last. 4. **Trigger assembly:** ```bash curl -X POST "https://api.fast.io/current/upload/{upload_id}/complete/" \ -H "Authorization: Bearer {jwt_token}" ``` 5. **Poll for completion** as normal. --- ## File Update (New Version) To upload a new version of an existing file: 1. **Create session** with `action=update`, `instance_id`, and `file_id` (OpaqueId of the file to replace). 2. Upload chunks and complete as normal. The existing file receives a new version. ```bash 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. ```json { "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: ```json "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. 1. **Create session** with `action=update`, `instance_id={fileshare_id}` (the File Share's numeric id or its opaque `id_alt`), and `file_id={bound_node_id}` (the File Share's bound file, available as `bound_node_id` from the management list, or `file.id` from the public details endpoint). 2. Upload chunks and complete as normal. The bound file receives a new version, and a `file_share_content_updated` event is emitted. ```bash 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. --- ## 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) ``` POST /current/upload/batch/ Content-Type: multipart/form-data ``` Authentication is **required** on `POST /current/upload/batch/`. Anonymous callers are not supported on batch — use single-file `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. | | `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. **curl example:** ```bash 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) A fallback for clients that cannot compose multipart. 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. ``` POST /current/upload/batch/ Content-Type: application/json ``` **Body schema:** ```json { "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/`. **Response (200 OK):** Always `200 OK` on a well-formed batch. Inspect `count_errored` to detect per-file failures. ```json { "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 by the assemble worker — 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`, a whole-file CRC-32C mismatch, which is the same code 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), which is the same code the other upload endpoints return. **Whole-batch error responses** (no `results[]` — standard error envelope; validation rejections include the structured `error.params` array described in the platform error reference): | 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 | Account plan does not allow files at the 4 MB per-file bound. | | `1685 (Feature Limit)` | 412 | The batch would exceed the plan's active upload-session count or aggregate in-flight session size. | | `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"`; batch returns 200): - Missing / empty `file_{index}` part (multipart) or missing / empty `content_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 account plan (only while extension enforcement is on). - `hash_algo` not supported or `hash` length wrong for the declared algorithm. - `hash_algo` supplied without `hash` (or vice versa). - Uploaded bytes do not match the declared hash. - The stored file did not match the entry's `crc32c` hash when it was finalized. This entry carries `error_code` `10778` (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_code` `190079`, and the `error_message` asks you to resubmit the file; it was not stored. ### Fetch batch status Re-fetches the stored result record for a prior batch. Useful if the POST response was lost in transit, or for polling the batch outcome from a background worker. ``` GET /current/upload/batch/{batch_id}/ ``` 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. **curl example:** ```bash curl -X GET "https://api.fast.io/current/upload/batch/{batch_id}/" ``` **Response (200 OK):** ```json { "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 } ``` **Response fields:** | Field | Type | Description | |---|---|---| | `batch_id` | string | Same identifier from the path. | | `creator` | string or null | Echo-back tag, or null if none was supplied. | | `count_total`, `count_succeeded`, `count_errored` | integer | Aggregate counts. | | `results` | array | Per-file outcomes, same shape as the POST response. | | `created_ts` | integer | Unix timestamp when the batch was recorded. | **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_created` event -- downstream consumers (search indexing, AI pipelines) see the same event stream as N independent `POST /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 on `error_code` `190079` (not the message text) to find them, and resubmit **only** those entries in a new batch -- do not resend the whole batch, since entries reported `ok` are already stored. Re-number the resubmitted entries' `index` from 0 in the new manifest, with matching `file_{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 ``` POST /current/web_upload/ Content-Type: application/x-www-form-urlencoded Authorization: Bearer {jwt_token} ``` **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) | **curl example:** ```bash 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):** ```json { "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", "bytes_downloaded": 0, "expected_size": null, "upload_session_id": null, "async_job_id": null, "status_description": "Queued for processing", "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 ``` GET /current/web_upload/ Authorization: Bearer {jwt_token} ``` **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"` | **curl example:** ```bash curl -X GET "https://api.fast.io/current/web_upload/?status=downloading&limit=20" \ -H "Authorization: Bearer {jwt_token}" ``` **Response (200 OK):** ```json { "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 /current/web_upload/{upload_id}/details/ Authorization: Bearer {jwt_token} ``` **Path parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `{upload_id}` | string | Yes | The web upload job OpaqueId | **curl example:** ```bash 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):** ```json { "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", "bytes_downloaded": 52428800, "expected_size": 52428800, "upload_session_id": "5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda", "async_job_id": "apkti-6i76h-6o4xr-5aqkj-egxgu-zmrc", "status_description": "Upload complete", "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." -- `params.reason: "geo_restricted"`. The job is not cancelled by the refusal; a genuine 404 still means the job is gone. See *Access Policy* in `llms/orgs.txt`. | ### Cancel web upload job ``` DELETE /current/web_upload/?id={web_upload_id} Authorization: Bearer {jwt_token} ``` **Query parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `id` | string | Yes | The web upload job OpaqueId to cancel | **curl example:** ```bash 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):** ```json { "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 plan limits) | | Max retries | 3 (automatic on transient failures) | | Retry delay | 60 seconds between attempts | --- ## Upload Management Endpoints ### List all upload sessions ``` GET /current/upload/details/ Authorization: Bearer {jwt_token} ``` Returns all upload sessions for the current user in any state. **curl example:** ```bash curl -X GET "https://api.fast.io/current/upload/details/" \ -H "Authorization: Bearer {jwt_token}" ``` **Response (200 OK):** ```json { "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 ``` DELETE /current/upload/{upload_id} Authorization: Bearer {jwt_token} ``` **Path parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `{upload_id}` | string | Yes | The upload session ID to delete (appended to URL path) | Cancel and delete an active upload session. 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. **curl example:** ```bash curl -X DELETE "https://api.fast.io/current/upload/5uhlp-zzcds-g2ba5-rqnbo-s5ytn-geda" \ -H "Authorization: Bearer {jwt_token}" ``` **Response (200 OK):** ```json { "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 ``` GET /current/upload/limits/ Authorization: Bearer {jwt_token} ``` Returns upload limits based on the user's billing plan 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 | **curl example:** ```bash # 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):** ```json { "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 ``` GET /current/upload/limits/extensions/ ``` Returns restricted and archive file extensions. **Authentication is optional** -- unauthenticated requests fall back to the most restrictive (unpaid) extension policy. This is only the default upload-restriction baseline; it does not grant a usable plan, and new organizations require a paid plan 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 (for example `starter_monthly`); 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. | **curl example:** ```bash 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=starter_monthly" ``` **Response (200 OK):** ```json { "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 ``` GET /current/upload/algos/ Authorization: Bearer {jwt_token} ``` **curl example:** ```bash curl -X GET "https://api.fast.io/current/upload/algos/" \ -H "Authorization: Bearer {jwt_token}" ``` **Response (200 OK):** ```json { "result": true, "algos": ["md5", "sha1", "sha256", "sha384", "crc32c"] } ``` `crc32c` is the recommended client integrity checksum -- see *Integrity Checksums (CRC-32C)*. ### Get chunk information ``` GET /current/upload/{upload_id}/chunk/ Authorization: Bearer {jwt_token} ``` Returns information about all uploaded chunks for a session. **Path parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `{upload_id}` | string | Yes | The upload session ID | To retrieve a specific chunk, append the chunk number to the path: ``` GET /current/upload/{upload_id}/chunk/{order} ``` | Parameter | Type | Required | Description | |---|---|---|---| | `{order}` | integer | No | Specific chunk number to retrieve. If omitted, returns all chunks. | **curl example (all chunks):** ```bash 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):** ```json { "result": true, "chunks": { "1": 5242880, "2": 5242880, "3": 2097152 } } ``` **curl example (single chunk):** ```bash 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):** ```json { "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 /current/upload/{upload_id}/chunk/?order={n} Authorization: Bearer {jwt_token} ``` 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 | **curl example:** ```bash 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):** ```json { "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." | | `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 plan 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=crc32c` with each chunk's CRC-32C, and send the whole-file value -- your per-chunk CRCs combined in order, no second pass -- as `file_crc32c` on the first send of the chunk whose CRC completes last (sequential clients: the last chunk), and on that chunk's retries only. Treat `10778` as terminal (re-upload in a new session) and `10779` as a client bug (resend the original value). After completion, compare the file's `crc32c` in 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 `wait` parameter 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_path` when unused**: Do NOT send it as an empty string.