> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Large and resumable uploads

> Chunked upload sessions for files that exceed a single POST

Workspace `files.upload` is a single multipart POST. The Python and TypeScript
SDKs switch to a resumable **upload session** automatically at **32 MiB**.
Call the session routes yourself when you need to resume after a drop or
stream from a source that is not in memory.

<Tabs>
  <Tab title="Python">
    ```python theme={"dark"}
    from pathlib import Path

    from ndi_sdk import NdiClient

    path = Path("ledger.xlsx")
    data = path.read_bytes()

    with NdiClient() as client:
        workspace = client.workspaces.create(name="large-uploads")
        job = client.jobs.wait(
            client.files.upload_chunked(
                workspace.workspace_id,
                data,
                path="books/ledger.xlsx",
            ).job_id
        )
        print(job.result.file.path, job.result.file.size_bytes)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```ts theme={"dark"}
    import { NdiClient } from "ndi-sdk";

    const client = new NdiClient();
    const workspace = await client.workspaces.create({ name: "large-uploads" });
    const job = await client.jobs.wait(
      (
        await client.files.upload(workspace.workspace_id, "ledger.xlsx", {
          path: "books/ledger.xlsx",
        })
      ).job_id,
    );
    console.log(job.result);
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={"dark"}
    SIZE=$(wc -c < ledger.xlsx | tr -d ' ')
    SESSION=$(curl -s -X POST "$NDI_BASE_URL/v1/workspaces/$WS/upload-sessions" \
      -H "X-API-Key: $NDI_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{\"path\":\"books/ledger.xlsx\",\"total_size_bytes\":$SIZE}")

    SESSION_ID=$(echo "$SESSION" | jq -r '.session_id')
    TOKEN=$(echo "$SESSION" | jq -r '.session_token')
    CHUNK=$(echo "$SESSION" | jq -r '.chunk_size')

    # PUT each part (0-indexed). Resume with GET .../upload-sessions/$SESSION_ID.
    PART=0
    curl -s -X PUT "$NDI_BASE_URL/v1/workspaces/$WS/upload-sessions/$SESSION_ID/parts/$PART" \
      -H "X-API-Key: $NDI_API_KEY" \
      -H "X-Upload-Token: $TOKEN" \
      --data-binary @part0.bin

    curl -s -X POST "$NDI_BASE_URL/v1/workspaces/$WS/upload-sessions/$SESSION_ID/complete?wait_seconds=60" \
      -H "X-API-Key: $NDI_API_KEY" \
      -H "X-Upload-Token: $TOKEN"
    ```
  </Tab>
</Tabs>

## Session lifecycle

1. `POST .../upload-sessions` — returns `session_id`, `session_token`, `chunk_size`, `total_parts`
2. `PUT .../parts/{part_number}` — send each chunk with `X-Upload-Token`
3. `GET .../upload-sessions/{session_id}` — list landed parts after a drop
4. `POST .../complete` — concatenates parts and writes the ledger row (a `Job`)
5. `DELETE .../upload-sessions/{session_id}` — abort and reclaim staged parts

The session token is not your API key. Send it on every part and on complete.
Default session TTL is a server default; `ttl_seconds` is optional on create.

## When to use which

| Size / client                 | Call                                       |
| ----------------------------- | ------------------------------------------ |
| Under 32 MiB from the SDK     | `files.upload` (single POST)               |
| 32 MiB or larger from the SDK | automatic session under `files.upload`     |
| Resume or stream yourself     | `create_upload_session` + parts + complete |
| Browser without your API key  | [Upload grants](/guides/upload-grants)     |

Uploading still does not ingest. Call [ingest](/guides/ingest-reconcile) after
the complete job succeeds.
