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

# Documents

> Getting a file into the platform, reading its parsed representation, and handing out expiring links.

A document is a stored file plus the normalised representation the platform
parsed out of it. Parse once, read many times — extractions, reports and
knowledge ingestion all read the same representation rather than each re-parsing
the bytes.

## What is accepted

```
.pdf .docx .odt .epub .pptx .odp .xlsx .xlsm .ods .csv
.txt .md .markdown .html .htm .json
.png .jpg .jpeg .tif .tiff .bmp .webp
.mp3 .wav .m4a .webm .ogg
```

Audio is transcribed. `.doc`, `.ppt` and `.xls` — the pre-2007 formats — are not
accepted.

The ceiling is **20 MB** unless your deployment lowers it. An oversized upload is
**413**; an unreadable type is **415**, with a rejection sentence written for a
human.

<Note>
  Both are refused at `POST`, before the bytes are stored. The extraction worker
  would reject an unreadable file too — but by then a document row exists and you
  have a 201 followed by a failure you have to poll for. A 400-family answer with
  the reason in it is better.
</Note>

## `POST /documents` — one endpoint, three modes

The mode is chosen by what the body carries, and they are **mutually exclusive**.
Naming two is a 400 rather than a precedence rule nobody can remember: an
ambiguous request is a request whose author believed something that is not true.

<AccordionGroup>
  <Accordion title="Mode A — multipart, for a file you have in hand">
    ```bash theme={null}
    curl "$IMPEL_API/documents" \
      -H "Authorization: Bearer $IMPEL_KEY" \
      -H "Idempotency-Key: 3d0a…" \
      -F "file=@invoice.pdf" \
      -F "metadata={\"vendor\":\"Acme\"};type=application/json"
    ```

    Needs `documents:create`. **201** on a fresh key, **200** on a replay.
  </Accordion>

  <Accordion title="Mode B — a signed upload grant, for bytes you would rather not proxy">
    Three steps.

    ```bash theme={null}
    # 1. Ask for a target. Needs documents:create.
    curl -X POST "$IMPEL_API/documents/uploads" \
      -H "Authorization: Bearer $IMPEL_KEY" \
      -H "Content-Type: application/json" \
      -d '{"filename": "invoice.pdf"}'
    ```

    ```bash theme={null}
    # 2. Send the raw bytes to the returned URL. No API key.
    curl -X PUT "https://api.impellabs.tech/api/v2/documents/uploads/<token>" \
      --data-binary @invoice.pdf
    ```

    ```bash theme={null}
    # 3. Confirm. Needs documents:create. This is what creates the Document.
    curl -X POST "$IMPEL_API/documents" \
      -H "Authorization: Bearer $IMPEL_KEY" \
      -H "Content-Type: application/json" \
      -d '{"upload_id": "upl_…"}'
    ```

    Step 2 takes **the raw body as the file** — no multipart, no field name, no
    metadata. The endpoint exists so a client can stream bytes at a URL, and
    every additional thing it parses is another thing an unauthenticated caller
    can reach. `POST` is accepted as well as `PUT`, for clients that cannot PUT.

    Step 2 creates nothing. The upload becomes a Document only when an
    authorised caller confirms it in step 3.
  </Accordion>

  <Accordion title="Mode C — a document you already have">
    ```bash theme={null}
    curl -X POST "$IMPEL_API/documents" \
      -H "Authorization: Bearer $IMPEL_KEY" \
      -H "Content-Type: application/json" \
      -d '{"document_id": "doc_…"}'
    ```
  </Accordion>
</AccordionGroup>

<Warning>
  `POST /documents` sets **no replay header**. The status is the signal: a
  **200** where a create answers **201** *is* the replay. See
  [Idempotency](/v2/idempotency).
</Warning>

## Signed grants

Two endpoints take no API key, and that is deliberate. The signed token in the
path **is** the credential.

|                                        |                                                                                   |
| -------------------------------------- | --------------------------------------------------------------------------------- |
| `POST\|PUT /documents/uploads/{token}` | Upload grant. 15 minutes by default; the surrounding upload window is 30 minutes. |
| `GET /documents/downloads/{token}`     | Download grant. 5 minutes by default, 1 hour maximum.                             |

A grant names one upload or one document, for one purpose, inside one isolation
boundary, and it is minted only after a credentialed caller has passed both the
scope check and the credit gate.

Every grant is **re-checked against the row on redemption**, for something a
signature cannot cover. A valid grant for a document that has since been deleted
is a 404. A signature proves the link was issued; it does not prove the thing it
points at still exists and is still yours.

Mint a download link with:

```bash theme={null}
curl -X POST "$IMPEL_API/documents/doc_…/download-link" \
  -H "Authorization: Bearer $IMPEL_KEY"
```

Needs `documents:read`. It is a POST rather than a GET because it mints a
capability: it is not safe to repeat from a browser's history bar, and a link
handed out by a GET ends up cached by something.

## Reading a document

| Endpoint                             | Scope              | Returns              |
| ------------------------------------ | ------------------ | -------------------- |
| `GET /documents`                     | `documents:read`   | A page of documents  |
| `GET /documents/{id}`                | `documents:read`   | The document row     |
| `GET /documents/{id}/representation` | `documents:read`   | The parsed structure |
| `GET /documents/{id}/content`        | `documents:read`   | The original bytes   |
| `DELETE /documents/{id}`             | `documents:delete` | —                    |

### The representation

Parsed once, on ingest, and read from then on:

```json theme={null}
{
  "object": "document_representation",
  "document_id": "doc_…",
  "schema_version": 1,
  "page_count": 12,
  "character_count": 24310,
  "quality": "…",
  "text": "…",
  "title": "…",
  "pages": [], "headings": [], "tables": [], "lists": [],
  "images": [], "layout": [], "segments": [],
  "metadata": {},
  "warnings": [],
  "created_at": "…"
}
```

Asking for the representation of a document that has not been processed yet is a
**409**, and so is asking for one whose processing failed — with different
messages, so you can tell "wait" from "this will never arrive".

The parser is not named in the response. Which engine read your file is an
implementation detail that would become a contract the moment it was published.

### Content is always a download

```
Content-Disposition: attachment
X-Content-Type-Options: nosniff
```

Both are fixed and neither is caller-controllable. A tenant can upload an `.html`
or an `.svg`, and serving one inline from an origin that also serves the API is
stored XSS with a customer's own file as the payload. There is no legitimate
request for "render my upload in your origin".

## Paging

`GET /documents` returns `next_cursor` and reads it back as **`cursor`** — not
`after`, which is what the conversation-messages endpoint uses.

```
GET /documents?limit=50
  → { "data": [...], "next_cursor": "…" }
GET /documents?limit=50&cursor=…
```

Cursors are opaque base64url. Do not construct one.

## What happens next

<CardGroup cols={2}>
  <Card title="Extractions" icon="table" href="/v2/extractions">
    A document plus a JSON Schema becomes validated fields with evidence.
  </Card>

  <Card title="Knowledge ingestion" icon="book" href="/v2/knowledge-ingestion">
    Publish a document into the index so agents can retrieve from it.
  </Card>
</CardGroup>

Uploading a document does **not** make it retrievable by an agent. Publication is
a deliberate, separate act — see [Knowledge ingestion](/v2/knowledge-ingestion).
