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

# Documents

> Upload files into a space so their content becomes searchable memory.

A document is parsed, split into chunks, and stored as memory, so the same
[Retrieve](https://docs.memory.anonalabs.com/api-reference/retrieve.md) and [Reason](https://docs.memory.anonalabs.com/api-reference/reason.md) calls surface
file content alongside anything you recorded conversationally.

| Category | Formats | How it is extracted |
| - | - | - |
| Documents | PDF, DOCX | Text extraction |
| Office | PPTX, XLSX, XLS | Text and table extraction |
| Web and plain text | HTML, HTM, TXT, Markdown, CSV | Direct parsing |
| Images | JPG, JPEG, PNG | Text in the image |
| Video | MP4, MOV, WEBM, MKV | On-screen text and a description of what happens |
| Audio | MP3, WAV | Transcription |

> [!WARNING]
>
> **Legacy `.doc` and `.ppt` are accepted and then fail.** The upload returns
> `202`, and the job ends `failed` because there is no reader for the old binary
> formats yet. Save as `.docx` / `.pptx` first. Audio from voice-memo apps is
> usually `.m4a`, which is refused outright — convert it to MP3.

## Upload files

```http theme={null}
POST /v1/spaces/{space_id}/documents
Authorization: Bearer anona_live_YOUR_KEY
Content-Type: multipart/form-data
```

| Form field | Type | Required | Description |
| - | - | - | - |
| `files` | file(s) | Yes | One or more files to upload. |
| `tags` | string | No | Comma-separated tags applied to every file, so retrieval can scope to them later. Tags starting with `anona:` are reserved and rejected with `422`. |
| `user_id` | string | No | End user these documents belong to. See [Scoping](https://docs.memory.anonalabs.com/api-reference/retrieve.md#scoping-within-a-space). |
| `agent_id` | string | No | Owning agent. |
| `session_id` | string | No | Owning session. |
| `strategy` | string | No | How the file is retained. Defaults to storing it as retrieval chunks; pass a named strategy to override. |

> [!WARNING]
>
> If you use scoping, upload with the same scope you retrieve with. Scoped search
> is strict, so a document uploaded **without** `user_id` is not returned to a
> search **with** one: it is only reachable from an unscoped search.

| Limit | Value |
| - | - |
| Files per request | 20 |
| Size per file | 25 MB, or 64 MB for video |
| Total per request | 50 MB, or 100 MB for a request containing video |

Video is allowed more because 25 MB is a few seconds of it at any real bitrate.

Ingestion is always asynchronous. The call returns immediately with a `job_ids` list;
poll each with the [job status](https://docs.memory.anonalabs.com/api-reference/memories.md#job-status) endpoint. When a job
reaches `completed`, that file's content is searchable. For an upload the job's own
`memory_count` and `memory_ids` stay `null`; how many memories a file produced is on the
document — `memory_count` from [List documents](#list-documents).

> [!WARNING]
>
> **Known issue: files over about 1 MB currently fail.** Above roughly 1 MB per
> file, the request is refused with `400 bad_request` ("There was an error
> parsing the body") instead of being accepted up to the limits above. Several
> smaller files in one request work. We are fixing this; until then, split or
> compress large files.

Uploading to a space that does not exist yet creates it, exactly as a first
[record](https://docs.memory.anonalabs.com/api-reference/memories.md) does — an upload is a write, so there is no
space to pre-create.

**Response** `202 Accepted`

```json theme={null}
{
  "job_ids": ["job_conv_7f1a", "job_conv_7f1b"]
}
```

```bash cURL theme={null}
curl -X POST https://api.anonalabs.com/v1/spaces/customer-support-bot/documents \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -F "files=@handbook.pdf" \
  -F "tags=hr,policy" \
  -F "user_id=alice"
```

```python Python theme={null}
job = client.upload_file(
    space_id="customer-support-bot",
    file="handbook.pdf",
    tags=["hr", "policy"],
)
print(job["job_ids"])  # poll each with client.get_job(...)
```

```typescript TypeScript theme={null}
import { readFileSync } from "node:fs";

const job = await anona.uploadFiles({
  spaceId: "customer-support-bot",
  files: [{ data: readFileSync("handbook.pdf"), filename: "handbook.pdf" }],
  tags: ["hr", "policy"],
});
console.log(job.job_ids); // poll each with anona.getJob(...)
```

The TypeScript client takes 1–20 files in one call; the Python client takes one per
call. Both carry `user_id` / `agent_id` / `session_id` (`userId` / `agentId` /
`sessionId` in TypeScript), and passing them matters whenever the space is scoped at
all: a scoped `retrieve` is strict, so an upload written without scope is invisible to
every scoped query — silently, since the files are stored and the job completes.

> [!NOTE]
>
> Parsing and embedding a document is billed on completion, not at upload. Larger files
> cost more, because more content is processed. Images and audio are read into text
> first and then treated as text, so each costs what its reading costs.
>
> An image yields a verbatim transcription of any text in it, so a screenshot is
> worth uploading; a photograph with no text in it currently yields little, since the
> description of what it shows is not being produced yet. A video yields its on-screen
> text and a description of what happens. **Speech in a video is not transcribed
> yet** — for a recording where the words matter, upload the audio track as MP3.
>
> Very long recordings are rejected rather than truncated: a transcript that stopped
> early would be stored as though it were the whole thing. Split a long recording and
> upload the parts.

## List documents

```http theme={null}
GET /v1/spaces/{space_id}/documents?limit=100&offset=0
Authorization: Bearer anona_live_YOUR_KEY
```

| Query parameter | Type | Description |
| - | - | - |
| `limit` | integer | Maximum results. Defaults to 100. |
| `offset` | integer | Pagination offset. |
| `q` | string | Optional substring filter on document id. |
| `source` | string | Keep only documents of these kinds: `file`, `custom`, `memory`. Repeat the parameter or comma-separate. Omit for everything. |

**Response** `200 OK`

```json theme={null}
{
  "documents": [
    {
      "document_id": "file_9c2f1a",
      "source": "file",
      "created_at": "2026-07-22T10:30:00Z",
      "updated_at": "2026-07-22T10:30:04Z",
      "text_length": 18240,
      "memory_count": 37,
      "tags": ["hr", "policy"]
    }
  ],
  "total": 1,
  "limit": 100,
  "offset": 0
}
```

`memory_count` is how many searchable memories were extracted from the document.

### What counts as a document

This list contains everything ingested into the space, not only uploaded files. Storing
a memory with `POST /v1/record` also creates a document row, so `source` tells you which
is which.

| `source` | Meaning |
| - | - |
| `file` | Uploaded through this endpoint. |
| `custom` | Content grouped under a caller-chosen id. Reserved: the record endpoints do not currently accept a `document_id`, so new writes never produce this value. |
| `memory` | Created implicitly by `POST /v1/record`. It holds a single stored memory rather than an uploaded document. |

Newest rows come first, and a space with steady `record` traffic accumulates `memory`
rows fast, so uploads are often not on the first page. Ask for the kind you want rather
than paging until you find it:

```http theme={null}
GET /v1/spaces/{space_id}/documents?source=file
```

`limit` and `offset` then apply to the filtered set, and `total` counts only the
matching documents.

Every memory that was *ingested* belongs to a document — an uploaded file, or the
`memory` row `POST /v1/record` writes for it — and carries that `document_id` on
retrieve and in the listings, so you can walk from a search hit back to the document
that produced it, and deleting a document removes every memory extracted from it.

`document_id` is nullable, and the common null is a `note`: a consolidation is
*derived* from raw facts rather than ingested, so it belongs to no document. Walk
its `source_ids` to the facts it was built from, and read their `document_id`.

> [!NOTE]
>
> The dashboard's Documents tab hides `memory` rows by default for this reason, with a
> toggle to show everything ingested.

```bash cURL theme={null}
curl "https://api.anonalabs.com/v1/spaces/customer-support-bot/documents?source=file" \
  -H "Authorization: Bearer anona_live_YOUR_KEY"
```

```python Python theme={null}
docs = client.list_documents(space_id="customer-support-bot")
```

```typescript TypeScript theme={null}
const docs = await anona.listDocuments({ spaceId: "customer-support-bot" });
```

## Get a document

```http theme={null}
GET /v1/spaces/{space_id}/documents/{document_id}
Authorization: Bearer anona_live_YOUR_KEY
```

```bash cURL theme={null}
curl https://api.anonalabs.com/v1/spaces/customer-support-bot/documents/file_9c2f1a \
  -H "Authorization: Bearer anona_live_YOUR_KEY"
```

```python Python theme={null}
doc = client.get_document(space_id="customer-support-bot", document_id="file_9c2f1a")
```

```typescript TypeScript theme={null}
const doc = await anona.getDocument({
  spaceId: "customer-support-bot",
  documentId: "file_9c2f1a",
});
```

Returns the document's metadata and ingestion status, in the same shape as one item in
the list above.

## Delete a document

Removes the document and every memory extracted from it. On a space shared with
you, only the owning organization can delete a document, whatever your role:
deleting one removes memories other members wrote and rely on, the same reason
memory delete is owner-only.

```http theme={null}
DELETE /v1/spaces/{space_id}/documents/{document_id}
Authorization: Bearer anona_live_YOUR_KEY
```

```bash cURL theme={null}
curl -X DELETE \
  https://api.anonalabs.com/v1/spaces/customer-support-bot/documents/file_9c2f1a \
  -H "Authorization: Bearer anona_live_YOUR_KEY"
```

```python Python theme={null}
client.delete_document(space_id="customer-support-bot", document_id="file_9c2f1a")
```

```typescript TypeScript theme={null}
await anona.deleteDocument({
  spaceId: "customer-support-bot",
  documentId: "file_9c2f1a",
});
```

**Response** `204 No Content`.

## Error responses

| Status | Code | Cause |
| - | - | - |
| 400 | `too_many_files` | More than 20 files in one request. |
| 400 | `bad_request` | The body could not be parsed. Today this is what a file over about 1 MB gets — see the known issue above. |
| 401 | `missing_auth` / `invalid_api_key` | Missing or invalid API key. |
| 403 | `space_access_denied` | You are not a member of this space. |
| 403 | `space_owner_only` | Deleting a document from a space shared with you. Only the owning organization can. |
| 404 | `space_not_found` | The space does not exist. A listing of a space that exists but holds nothing is an empty page, not this. |
| 413 | `file_too_large` | A single file exceeds 25 MB, or 64 MB for video. |
| 413 | `upload_too_large` | The total upload exceeds 50 MB, or 100 MB with video. |
| 413 | `file_expands_too_much` | An Office file unpacks to far more than it weighs. Read from the archive's own index without decompressing, so it costs nothing to check. |
| 415 | `unsupported_file_type` | A file's extension is outside the supported list above. One bad file rejects the whole request. |
| 415 | `file_type_mismatch` | The file's contents do not match its extension — a zip named `.pdf`, or an archive named as text or audio. |
| 400 | `invalid_source` | The `source` filter named a kind that does not exist. Valid values are `file`, `custom` and `memory`. |
| 429 | `rate_limited` / `credits_exhausted` | Rate limit hit, or the organization is out of credits. |

See the full [error reference](https://docs.memory.anonalabs.com/api-reference/errors.md).

## Next steps

> **[Retrieve API](https://docs.memory.anonalabs.com/api-reference/retrieve.md)**
>
> Search across recorded memory and uploaded documents.

> **[Memories API](https://docs.memory.anonalabs.com/api-reference/memories.md)**
>
> Record memories and poll ingestion jobs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.