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

# Ingest

> Upload documents and track ingestion status

The Ingest endpoints handle document uploads and status polling. Parsing
runs asynchronously — upload returns a `task_id` immediately, which you
then poll until ingestion completes.

<Tip>
  **Use an official SDK if you can** — the [Python](/products/python-sdk)
  and [JavaScript](/products/js-sdk) SDKs wrap the direct-upload flow into
  a single `client.ingest.file(...)` / `client.ingest.batch(...)` call
  that works for any file size. The endpoint-level details below are only
  needed if you're calling the REST API directly.
</Tip>

## Upload a Document

The default upload flow streams your file bytes straight from your client
to Polyvia's storage backend, then asks our API to register and parse it.
This is a **three-step** flow that works for any file size — there is no
practical upper limit.

### Step 1 — Get an upload URL

`POST /api/v1/ingest/upload-url`

No request body.

<ResponseField name="upload_url" type="string" required>
  Short-lived signed URL for the storage backend. Expires in \~1 hour and
  is single-use.
</ResponseField>

<ResponseExample>
  ```json Response theme={null}
  { "upload_url": "https://<your-deployment>.convex.cloud/api/storage/upload?token=..." }
  ```
</ResponseExample>

### Step 2 — PUT the file to that URL

Stream the raw file bytes to the returned `upload_url`. Set `Content-Type`
to the file's MIME type.

<Warning>
  Do **not** include your Polyvia `Authorization: Bearer poly_<key>`
  header on this request. The URL is already signed, and forwarding the
  key to a different origin would leak it unnecessarily.
</Warning>

Storage responds with the new object's identifier:

```json theme={null}
{ "storageId": "kg2abc...", "size": 12345678 }
```

### Step 3 — Finalize the upload

`POST /api/v1/ingest/finalize`

`application/json`

<ParamField body="storage_id" type="string" required>
  The `storageId` returned by storage in Step 2.
</ParamField>

<ParamField body="file_type" type="string" required>
  MIME type of the uploaded file.
</ParamField>

<ParamField body="name" type="string">
  Display name. Defaults to "Untitled" if omitted.
</ParamField>

<ParamField body="group_id" type="string">
  Assign the document to a group on creation.
</ParamField>

### Response

<ResponseField name="document_id" type="string" required>
  Unique identifier for the uploaded document
</ResponseField>

<ResponseField name="task_id" type="string" required>
  Ingestion task identifier — use this to poll for status
</ResponseField>

<ResponseField name="status" type="string">
  Initial status: always `pending`
</ResponseField>

### Example

<CodeGroup>
  ```bash cURL + jq theme={null}
  # 1. Get a signed upload URL
  UPLOAD_URL=$(curl -s -X POST https://app.polyvia.ai/api/v1/ingest/upload-url \
    -H "Authorization: Bearer poly_<your-key>" | jq -r '.upload_url')

  # 2. PUT the file bytes directly to storage (no auth header!)
  STORAGE_ID=$(curl -s -X PUT "$UPLOAD_URL" \
    -H "Content-Type: application/pdf" \
    --data-binary @report.pdf | jq -r '.storageId')

  # 3. Finalize — register the document and queue parsing
  curl -X POST https://app.polyvia.ai/api/v1/ingest/finalize \
    -H "Authorization: Bearer poly_<your-key>" \
    -H "Content-Type: application/json" \
    -d "{\"storage_id\": \"$STORAGE_ID\", \"file_type\": \"application/pdf\", \"name\": \"Q4 Report\", \"group_id\": \"g_...\"}"
  ```

  ```python httpx theme={null}
  import httpx

  API_KEY = "poly_<your-key>"
  BASE    = "https://app.polyvia.ai"
  HEADERS = {"Authorization": f"Bearer {API_KEY}"}

  # 1. Get an upload URL
  upload_url = httpx.post(
      f"{BASE}/api/v1/ingest/upload-url", headers=HEADERS
  ).json()["upload_url"]

  # 2. PUT the file directly to storage (no auth header here!)
  with open("report.pdf", "rb") as f:
      storage_id = httpx.put(
          upload_url,
          content=f.read(),
          headers={"Content-Type": "application/pdf"},
          timeout=300,
      ).json()["storageId"]

  # 3. Finalize
  resp = httpx.post(
      f"{BASE}/api/v1/ingest/finalize",
      headers=HEADERS,
      json={
          "storage_id": storage_id,
          "file_type":  "application/pdf",
          "name":       "Q4 Report",
          "group_id":   "g_...",
      },
  ).json()
  # {"document_id": "...", "task_id": "...", "status": "pending"}
  ```

  ```python Python SDK theme={null}
  from polyvia import Polyvia

  client = Polyvia(api_key="poly_...")

  # The SDK runs the three-step direct-upload flow internally.
  result = client.ingest.file("report.pdf", name="Q4 Report", group_id="g_...")
  client.ingest.wait(result.task_id)
  ```
</CodeGroup>

<ResponseExample>
  ```json Finalize response theme={null}
  {
    "document_id": "k57abc123...",
    "task_id":     "3f2e1d0c-...",
    "status":      "pending"
  }
  ```
</ResponseExample>

***

## Upload Multiple Documents

For batches, run the direct-upload flow once per file. The official SDKs
do this in `client.ingest.batch(...)` — each file is uploaded and
finalized independently, so a failure on one file is isolated to that
entry instead of failing the whole batch.

<CodeGroup>
  ```python Python SDK theme={null}
  from polyvia import Polyvia

  client = Polyvia(api_key="poly_...")

  batch = client.ingest.batch(
      ["q3.pdf", "q4.pdf"],
      names=["Q3 Report", "Q4 Report"],
      group_id="g_...",
  )
  for item in batch.results:
      if item.error:
          print(f"failed: {item.file}: {item.error}")
      else:
          client.ingest.wait(item.task_id)
  ```

  ```ts JavaScript SDK theme={null}
  import { Polyvia } from "polyvia";

  const client = new Polyvia({ apiKey: "poly_..." });

  const items = await client.ingest.batch(["q3.pdf", "q4.pdf"], {
    names: ["Q3 Report", "Q4 Report"],
    groupId: "g_...",
  });
  for (const item of items) {
    if (item.error) {
      console.error(`failed: ${item.error}`);
    } else if (item.task_id) {
      await client.ingest.wait(item.task_id);
    }
  }
  ```
</CodeGroup>

***

## Quick Multipart Upload (small files)

<Note>
  These multipart endpoints proxy file bytes through the API server,
  which has a **4.5 MB total request-body limit**. They exist as a
  one-call convenience for small uploads. For any file size, prefer the
  direct-upload flow above (or use an SDK, which does it for you).
</Note>

### `POST /api/v1/ingest`

`multipart/form-data`

<ParamField body="file" type="file" required>
  The document to upload. See [Supported File Formats](#supported-file-formats) below.
</ParamField>

<ParamField body="name" type="string">
  Display name in your workspace. Defaults to the filename.
</ParamField>

<ParamField body="group_id" type="string">
  Assign the document to a group on upload.
</ParamField>

Returns the same `{document_id, task_id, status}` shape as `/finalize`.

```bash theme={null}
curl -X POST https://app.polyvia.ai/api/v1/ingest \
  -H "Authorization: Bearer poly_<your-key>" \
  -F "file=@/path/to/report.pdf" \
  -F "name=Q4 2024 Report" \
  -F "group_id=g_..."
```

### `POST /api/v1/ingest/batch`

`multipart/form-data`. Same fields as `/ingest` but `files` is repeated
per file and `names` is a comma-separated string aligned to `files`.
Returns `{results: [...], errors: [...] | null}`.

```bash theme={null}
curl -X POST https://app.polyvia.ai/api/v1/ingest/batch \
  -H "Authorization: Bearer poly_<your-key>" \
  -F "files=@q3.pdf" \
  -F "files=@q4.pdf" \
  -F "names=Q3 Report,Q4 Report" \
  -F "group_id=g_..."
```

***

## Check Ingestion Status

Poll a parse task started by either upload flow.

### Endpoint

`GET /api/v1/ingest/{task_id}`

### Path Parameters

<ParamField path="task_id" type="string" required>
  The task identifier returned by `/ingest/finalize` (or the legacy multipart endpoints)
</ParamField>

### Response

<ResponseField name="task_id" type="string">
  Task identifier
</ResponseField>

<ResponseField name="document_id" type="string">
  Document identifier
</ResponseField>

<ResponseField name="status" type="string">
  Processing status (see table below)
</ResponseField>

<ResponseField name="error" type="string | null">
  Error message if status is `failed`, otherwise `null`
</ResponseField>

| `status` value | Meaning                           |
| -------------- | --------------------------------- |
| `pending`      | Queued, not yet started           |
| `parsing`      | Being parsed and indexed          |
| `completed`    | Ready to query                    |
| `failed`       | Parsing failed; see `error` field |

### Example

```bash cURL theme={null}
curl https://app.polyvia.ai/api/v1/ingest/3f2e1d0c-... \
  -H "Authorization: Bearer poly_<your-key>"
```

<CodeGroup>
  ```bash Poll until completed theme={null}
  while true; do
    STATUS=$(curl -s \
      -H "Authorization: Bearer poly_<your-key>" \
      "https://app.polyvia.ai/api/v1/ingest/3f2e1d0c-..." \
      | jq -r '.status')
    echo "Status: $STATUS"
    [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break
    sleep 5
  done
  ```

  ```python Python SDK theme={null}
  from polyvia import Polyvia

  client = Polyvia(api_key="poly_...")

  # Blocks until done — raises IngestionError on failure
  done = client.ingest.wait(task_id, poll_interval=5, timeout=300)
  ```
</CodeGroup>

<ResponseExample>
  ```json Success theme={null}
  {
    "task_id":     "3f2e1d0c-...",
    "document_id": "k57abc123...",
    "status":      "completed",
    "error":       null
  }
  ```
</ResponseExample>

***

## Supported File Formats

| Category  | Extensions                                |
| --------- | ----------------------------------------- |
| Documents | `.pdf`, `.docx`, `.pptx`                  |
| Text      | `.txt`, `.md` (Markdown)                  |
| Images    | `.png`, `.jpg` / `.jpeg`, `.webp`, `.gif` |
| Audio     | `.wav`, `.mp3`, `.m4a`                    |

See [Supported Formats](/products/supported-formats) for parser-by-parser details on what gets extracted from each file type.

<Tip>
  Documents are typically processed within 1–2 minutes. Audio files take longer — roughly real-time playback for transcription. Poll the status endpoint every few seconds to check progress.
</Tip>
