> ## 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.

# Upload a document

> Stage bytes for a one-shot parse, extract, split, classify, or ground call

`POST /v1/uploads` stages a file for a **granular** operation. It does not put
the file in a workspace. The handle is single-use: bytes are deleted when the
job that reads them finishes.

For a durable corpus, use [workspace uploads](/guides/workspace) instead.

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

    from ndi_sdk import NdiClient

    with NdiClient() as client:
        upload = client.documents.create_upload(Path("report.pdf"))
        print(upload.upload_id, upload.expires_at)
        job = client.jobs.wait(client.documents.parse(upload, wait_seconds=30).job_id)
        print(job.result.markdown)
    ```
  </Tab>

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

    const client = new NdiClient();
    const upload = await client.documents.createUpload("report.pdf");
    const job = await client.jobs.wait(
      (await client.documents.parse(upload, { wait_seconds: 30 })).job_id,
    );
    console.log(job.result && "markdown" in job.result ? job.result.markdown : job.result);
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={"dark"}
    UPLOAD=$(curl -s -X POST "$NDI_BASE_URL/v1/uploads" \
      -H "X-API-Key: $NDI_API_KEY" \
      -F "file=@report.pdf")
    echo "$UPLOAD" | jq .

    UPLOAD_ID=$(echo "$UPLOAD" | jq -r '.upload_id')
    curl -s -X POST "$NDI_BASE_URL/v1/parse?wait_seconds=60" \
      -H "X-API-Key: $NDI_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{\"source\":{\"type\":\"upload\",\"upload_id\":\"$UPLOAD_ID\"}}"
    ```
  </Tab>
</Tabs>

## Inputs

|              | Python                          | TypeScript                           | HTTP                               |
| ------------ | ------------------------------- | ------------------------------------ | ---------------------------------- |
| File         | `Path`, bytes, or a binary file | path string, `Blob`, or `Uint8Array` | `multipart/form-data` field `file` |
| Optional TTL | `ttl_seconds=`                  | `ttl_seconds`                        | form field `ttl_seconds`           |

Default TTL is 24 hours; the cap is 7 days.

## Result

```json theme={"dark"}
{
  "upload_id": "550e8400-e29b-41d4-a716-446655440002",
  "file_name": "report.pdf",
  "size_bytes": 2048000,
  "content_hash": "a3f2b1c0d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
  "expires_at": "2026-08-10T12:00:00Z"
}
```

The SDK `create_upload` / `createUpload` return value is itself a valid
`source` for document operations.

## Errors

* **`file_too_large`** (413) — over the upload size cap
* **`unsupported_file_type`** (422) — rejected by the next operation, not always at upload
* **`upload_expired`** (409) — handle expired or already consumed

## Next

* [Parse](/guides/parse)
* [Sources](/concepts/sources-and-uploads)
* [Generated upload route](/api-reference/uploads)
