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

# Upload and update documents

> Upload documents to an Anam knowledge group, check processing status, and replace outdated files with verified source material.

## Overview

Upload [prepared documents](https://anam.ai/docs/personas/knowledge/preparing-documents.md) to a [knowledge folder](https://anam.ai/docs/personas/knowledge/setup.md), then check that processing completes and the expected passages can be retrieved.

| Task | Go to |
| - | - |
| Upload a new document | [Upload process](#upload-process) |
| Check whether a file is searchable | [Monitor processing](#monitoring-upload-progress) |
| Upload several files | [Batch uploads](#batch-uploads) |
| Replace an outdated file | [Replace an existing document](#replace-an-existing-document) |
| Investigate a failed upload | [Troubleshooting](#troubleshooting) |

Use the [upload checklist](https://anam.ai/docs/personas/knowledge/upload-checklist.md) before and after making changes.

## Upload process

Uploading a document takes three requests: ask Anam for a presigned URL, send the file straight to storage, then confirm the upload so processing can start.

This is the recommended route for every file, because the second request goes directly to cloud storage and so is not bound by the 4.5MB request body limit that applies to the API itself. A single multipart request to [`POST /v1/knowledge/groups/{id}/documents`](https://anam.ai/docs/api-reference/knowledge/upload-knowledge-group-document) also works and is simpler for small files, but it is subject to that limit.

See [upload limits](https://anam.ai/docs/personas/knowledge/setup.md#upload-limits) for file size and monthly allowance.

**1. Request presigned URL**

Request an upload URL from Anam:

```bash cURL theme={"system"}
curl -X POST 'https://api.anam.ai/v1/knowledge/groups/FOLDER_ID/documents/presigned-upload' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "filename": "large-document.pdf",
    "contentType": "application/pdf",
    "fileSize": 10485760
  }'
```

```javascript JavaScript theme={"system"}
const response = await fetch(
  `https://api.anam.ai/v1/knowledge/groups/${folderId}/documents/presigned-upload`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      filename: "large-document.pdf",
      contentType: "application/pdf",
      fileSize: file.size,
    }),
  }
);

const { uploadUrl, documentId } = await response.json();
```

```python Python theme={"system"}
response = requests.post(
    f'https://api.anam.ai/v1/knowledge/groups/{folder_id}/documents/presigned-upload',
    headers={
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    },
    json={
        'filename': 'large-document.pdf',
        'contentType': 'application/pdf',
        'fileSize': file_size
    }
)

data = response.json()
upload_url = data['uploadUrl']
document_id = data['documentId']
```

**Response**:

```json theme={"system"}
{
  "uploadUrl": "https://storage.cloudflare.com/presigned-url-here",
  "documentId": "doc-uuid-123"
}
```

**2. Upload to presigned URL**

Upload the file directly to cloud storage:

```bash cURL theme={"system"}
curl -X PUT 'PRESIGNED_URL_FROM_STEP_1' \
  -H 'Content-Type: application/pdf' \
  --data-binary '@large-document.pdf'
```

```javascript JavaScript theme={"system"}
const uploadResponse = await fetch(uploadUrl, {
  method: "PUT",
  headers: {
    "Content-Type": "application/pdf",
  },
  body: file,
});

if (!uploadResponse.ok) {
  throw new Error("File upload to storage failed");
}
```

```python Python theme={"system"}
with open('large-document.pdf', 'rb') as f:
    upload_response = requests.put(
        upload_url,
        headers={'Content-Type': 'application/pdf'},
        data=f
    )

if upload_response.status_code != 200:
    raise Exception('File upload to storage failed')
```

> [!NOTE]
>
> This step uploads directly to cloud storage, bypassing Anam's API servers for better performance with large files.

**3. Confirm upload**

Notify Anam that the upload is complete:

```bash cURL theme={"system"}
curl -X POST 'https://api.anam.ai/v1/knowledge/documents/DOCUMENT_ID/confirm-upload' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "fileSize": 10485760
  }'
```

```javascript JavaScript theme={"system"}
const confirmResponse = await fetch(
  `https://api.anam.ai/v1/knowledge/documents/${documentId}/confirm-upload`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      fileSize: file.size,
    }),
  }
);

const document = await confirmResponse.json();
console.log("Upload confirmed, processing started");
```

```python Python theme={"system"}
confirm_response = requests.post(
    f'https://api.anam.ai/v1/knowledge/documents/{document_id}/confirm-upload',
    headers={
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    },
    json={'fileSize': file_size}
)

document = confirm_response.json()
print('Upload confirmed, processing started')
```

> [!TIP]
>
> The response returns the document with status `PROCESSING`. Indexing then runs in the background, so a successful confirmation does not mean the document is searchable yet — [poll its status](#monitoring-upload-progress) until it reaches `READY`.

### The three requests as one function

The batch example below reuses `uploadLargeDocument` from here.

> [accordion] Complete upload helper
>
> ```javascript JavaScript theme={"system"}
> async function uploadLargeDocument(file, folderId, apiKey) {
>   try {
>     // Step 1: Get presigned URL
>     const presignedResponse = await fetch(
>       `https://api.anam.ai/v1/knowledge/groups/${folderId}/documents/presigned-upload`,
>       {
>         method: 'POST',
>         headers: {
>           'Authorization': `Bearer ${apiKey}`,
>           'Content-Type': 'application/json'
>         },
>         body: JSON.stringify({
>           filename: file.name,
>           contentType: file.type,
>           fileSize: file.size
>         })
>       }
>     );
>
>     if (!presignedResponse.ok) {
>       throw new Error('Failed to get presigned URL');
>     }
>
>     const { uploadUrl, documentId } = await presignedResponse.json();
>     console.log('Got presigned URL for document:', documentId);
>
>     // Step 2: Upload to storage
>     const uploadResponse = await fetch(uploadUrl, {
>       method: 'PUT',
>       headers: {
>         'Content-Type': file.type
>       },
>       body: file
>     });
>
>     if (!uploadResponse.ok) {
>       throw new Error('Failed to upload file to storage');
>     }
>
>     console.log('File uploaded to storage');
>
>     // Step 3: Confirm upload
>     const confirmResponse = await fetch(
>       `https://api.anam.ai/v1/knowledge/documents/${documentId}/confirm-upload`,
>       {
>         method: 'POST',
>         headers: {
>           'Authorization': `Bearer ${apiKey}`,
>           'Content-Type': 'application/json'
>         },
>         body: JSON.stringify({
>           fileSize: file.size
>         })
>       }
>     );
>
>     if (!confirmResponse.ok) {
>       throw new Error('Failed to confirm upload');
>     }
>
>     const document = await confirmResponse.json();
>     console.log('Upload complete! Document ID:', document.id);
>     console.log('Status:', document.status);
>
>     return document;
>
>   } catch (error) {
>     console.error('Upload error:', error);
>     throw error;
>   }
> }
>
> ```
>
> ```python Python theme={"system"}
> import requests
> import os
>
> def upload_document(file_path, folder_id, api_key):
>     """Upload a document using presigned URL"""
>
>     filename = os.path.basename(file_path)
>     file_size = os.path.getsize(file_path)
>     content_type = 'application/pdf'  # Adjust based on file type
>
>     try:
>         # Step 1: Get presigned URL
>         presigned_response = requests.post(
>             f'https://api.anam.ai/v1/knowledge/groups/{folder_id}/documents/presigned-upload',
>             headers={
>                 'Authorization': f'Bearer {api_key}',
>                 'Content-Type': 'application/json'
>             },
>             json={
>                 'filename': filename,
>                 'contentType': content_type,
>                 'fileSize': file_size
>             }
>         )
>         presigned_response.raise_for_status()
>
>         data = presigned_response.json()
>         upload_url = data['uploadUrl']
>         document_id = data['documentId']
>
>         print(f'Got presigned URL for document: {document_id}')
>
>         # Step 2: Upload to storage
>         with open(file_path, 'rb') as f:
>             upload_response = requests.put(
>                 upload_url,
>                 headers={'Content-Type': content_type},
>                 data=f
>             )
>         upload_response.raise_for_status()
>
>         print('File uploaded to storage')
>
>         # Step 3: Confirm upload
>         confirm_response = requests.post(
>             f'https://api.anam.ai/v1/knowledge/documents/{document_id}/confirm-upload',
>             headers={
>                 'Authorization': f'Bearer {api_key}',
>                 'Content-Type': 'application/json'
>             },
>             json={'fileSize': file_size}
>         )
>         confirm_response.raise_for_status()
>
>         document = confirm_response.json()
>         print(f'Upload complete! Document ID: {document["id"]}')
>         print(f'Status: {document["status"]}')
>
>         return document
>
>     except requests.exceptions.RequestException as e:
>         print(f'Upload error: {e}')
>         raise
> ```

### Show upload progress

The direct-to-storage request is the slow part, so track progress there with `XMLHttpRequest` if you need a progress bar. This replaces step 2 only — request the presigned URL and confirm the upload as above.

> [accordion] Upload with progress tracking
>
> ```javascript theme={"system"}
> // Send the file to the presigned URL returned by step 1, reporting progress.
> function uploadToStorageWithProgress(file, uploadUrl, onProgress) {
>   return new Promise((resolve, reject) => {
>     const xhr = new XMLHttpRequest();
>
>     xhr.upload.addEventListener('progress', (e) => {
>       if (e.lengthComputable) {
>         onProgress((e.loaded / e.total) * 100);
>       }
>     });
>
>     xhr.addEventListener('load', () => {
>       if (xhr.status >= 200 && xhr.status < 300) {
>         resolve();
>       } else {
>         reject(new Error(`Upload failed with status ${xhr.status}`));
>       }
>     });
>
>     xhr.addEventListener('error', () => reject(new Error('Upload failed')));
>
>     xhr.open('PUT', uploadUrl);
>     xhr.setRequestHeader('Content-Type', file.type);
>     xhr.send(file);
>   });
> }
> ```

## Monitoring upload progress

After uploading, monitor the document's processing status:

```bash cURL theme={"system"}
curl -X GET 'https://api.anam.ai/v1/knowledge/documents/DOCUMENT_ID' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

```javascript JavaScript theme={"system"}
async function checkDocumentStatus(documentId, apiKey) {
  const response = await fetch(
    `https://api.anam.ai/v1/knowledge/documents/${documentId}`,
    {
      headers: {
        Authorization: `Bearer ${apiKey}`,
      },
    }
  );

  const document = await response.json();
  return document.status;
}

// Poll until READY
async function waitForProcessing(documentId, apiKey) {
  // 360 attempts x 5s = 30 minutes, past the server's own 15-25 minute window
  const maxAttempts = 360;
  let attempts = 0;

  while (attempts < maxAttempts) {
    const status = await checkDocumentStatus(documentId, apiKey);
    console.log(`Status: ${status}`);

    if (status === "READY") {
      console.log("Document is ready for search!");
      return true;
    }

    if (status === "FAILED") {
      throw new Error("Document processing failed");
    }

    // Wait 5 seconds before next check
    await new Promise((resolve) => setTimeout(resolve, 5000));
    attempts++;
  }

  // This is your client giving up, not the server failing the document.
  throw new Error("Stopped polling; the document may still be processing");
}
```

```python Python theme={"system"}
import time

def check_document_status(document_id, api_key):
    """Check the processing status of a document"""

    response = requests.get(
        f'https://api.anam.ai/v1/knowledge/documents/{document_id}',
        headers={'Authorization': f'Bearer {api_key}'}
    )
    response.raise_for_status()

    document = response.json()
    return document['status']

def wait_for_processing(document_id, api_key, max_attempts=360):
    """Poll until document is ready or fails.

    360 attempts x 5s = 30 minutes, past the server's own 15-25 minute window.
    """

    attempts = 0

    while attempts < max_attempts:
        status = check_document_status(document_id, api_key)
        print(f'Status: {status}')

        if status == 'READY':
            print('Document is ready for search!')
            return True

        if status == 'FAILED':
            raise Exception('Document processing failed')

        # Wait 5 seconds before next check
        time.sleep(5)
        attempts += 1

    # This is your client giving up, not the server failing the document.
    raise Exception('Stopped polling; the document may still be processing')
```

**Document statuses**:

* `UPLOADED`: File uploaded, waiting for processing
* `PROCESSING`: Content is being extracted and indexed
* `READY`: Document is searchable
* `FAILED`: Processing failed (check `errorMessage`)

Only `READY` documents are returned by search. A document that stays in `PROCESSING` for more than 15 minutes is marked `FAILED` by a periodic sweep, so a status change can take up to roughly 25 minutes to appear. Poll for longer than that before treating a document as stuck: your own polling loop giving up is not the same as the server failing the document.

## Batch uploads

Upload multiple documents efficiently:

```javascript theme={"system"}
// Uses uploadLargeDocument from the complete example above.
async function batchUpload(files, folderId, apiKey) {
  const results = [];

  // Process files concurrently (4 at a time)
  const batchSize = 4;
  for (let i = 0; i < files.length; i += batchSize) {
    const batch = files.slice(i, i + batchSize);

    const batchPromises = batch.map(async (file) => {
      try {
        const document = await uploadLargeDocument(file, folderId, apiKey);

        console.log(`✓ Uploaded: ${file.name}`);
        return { file: file.name, success: true, documentId: document.id };
      } catch (error) {
        console.error(`✗ Failed: ${file.name}`, error.message);
        return { file: file.name, success: false, error: error.message };
      }
    });

    const batchResults = await Promise.all(batchPromises);
    results.push(...batchResults);
  }

  const successful = results.filter((r) => r.success).length;
  const failed = results.filter((r) => !r.success).length;

  console.log(`\nBatch upload complete:`);
  console.log(`  Successful: ${successful}`);
  console.log(`  Failed: ${failed}`);

  return results;
}
```

> [!TIP]
>
> Process documents in batches of 4 for optimal performance. The system handles
> up to 4 documents concurrently.

A successful batch means every file was accepted and queued, not that it is searchable. Wait for each document to reach `READY` before running retrieval tests against the folder.

## Replace an existing document

To change a document's contents, upload the revised file as a new document and remove the superseded document. The [update document API](https://anam.ai/docs/api-reference/knowledge/update-knowledge-document.md) only renames a file; it does not replace its contents or extracted passages. Track document IDs rather than assuming a matching filename identifies the active revision.

**1. Prepare and record the replacement**

Open the exact file you intend to upload and check a distinctive changed passage. Record the source revision, effective date, target folder, and old document ID. Preserve the original file and relevant persona settings outside the active knowledge folders so you can roll back.

**2. Upload and check processing**

Use the [upload process](#upload-process) above, record the new document ID, and wait for it to reach `READY`. Uploading a second file does not retire the first.

If conflicting revisions must not be searchable during the update, prepare a separate replacement folder containing the complete intended document set. Test it before changing the folders selected by the [knowledge tool](https://anam.ai/docs/personas/knowledge/tools.md).

**3. Retire the old version**

For an update in the same folder, [delete the superseded document](https://anam.ai/docs/api-reference/knowledge/delete-knowledge-document.md) after the replacement is ready. Check the [folder's document list](https://anam.ai/docs/api-reference/knowledge/list-knowledge-group-documents.md) using the IDs you recorded.

For a replacement folder, update the tool's selected folders to use it and remove the old folder from that selection. Check other tools attached to the persona for references to the old material too.

**4. Verify retrieval and a fresh conversation**

[Search for the changed fact](https://anam.ai/docs/personas/knowledge/testing.md#inspect-retrieved-passages) and inspect its text, scope, and source document ID. Confirm that superseded IDs no longer appear in results from the selected folders.

Start a fresh persona session and [test its answer](https://anam.ai/docs/personas/knowledge/testing.md#test-persona-answers). If an old rule still appears, check the selected files, active IDs, and processing status before editing the prompt.

For example, a fictional returns policy changes from 14 to 30 days for a specified region. Ask about that region and check the returned policy text and effective date; the new filename alone does not prove the updated policy is in use.

If verification fails, correct the release or restore the previous coherent set of files and settings from your saved copies, then verify again. Keep a short change record. A local SHA-256 hash can help identify exact file contents; equal file sizes do not prove two files are identical.

## Troubleshooting

> [accordion] File too large
>
> **Error**: `File size exceeds limit`
>
> **Solutions**:
>
> * Split the document at useful topic boundaries, keeping conditions and exceptions together
> * Compress images while preserving essential text and diagrams
> * Remove unnecessary pages only after checking for unique facts, qualifications, and source references
> * Contact us about Enterprise plans with higher file size limits

> [accordion] Monthly upload quota exceeded
>
> **Error**: `Monthly upload quota exceeded`
>
> Your plan allows a fixed number of megabytes uploaded per calendar month. The response states how much of the allowance is left.
>
> **Solutions**:
>
> * Upload only the files the persona needs, so the allowance is not spent on drafts or duplicates
> * Wait for the allowance to reset at the start of next month
> * Contact us about Enterprise plans with higher upload limits

> [accordion] Upload fails immediately
>
> **Possible causes**:
>
> * Invalid API key
> * Invalid folder ID
> * Unsupported file type
>
> **Solutions**:
>
> 1. Verify API key is valid and has proper permissions
> 2. Confirm folder ID exists: `GET /v1/knowledge/groups`
> 3. Check the file is a [supported type](https://anam.ai/docs/personas/knowledge/setup.md#supported-file-types) and that the filename is 255 characters or fewer
> 4. Review error message in response

> [accordion] Processing stuck
>
> **Status remains PROCESSING**
>
> **Possible causes**:
>
> * Very large file (40-50MB)
> * Complex PDF with many images
> * Service temporarily slow
>
> **Solutions**:
>
> 1. Keep polling. A document is only considered stuck after 15 minutes in `PROCESSING`, and the sweep that marks it `FAILED` runs every 10 minutes, so allow roughly 25 minutes before concluding anything.
> 2. Once the status is `FAILED`, read `errorMessage` for the reason.
> 3. Upload the file again as a new document if it failed, then delete the failed one.
> 4. Contact support if the same file fails repeatedly.

> [accordion] Processing failed
>
> **Status changes to FAILED**
>
> **Common causes**:
>
> * Corrupted file
> * Encrypted or password-protected PDF
> * Invalid file format despite correct extension
> * File contains only images (no text)
>
> **Solutions**:
>
> 1. Check error message in document details
> 2. Verify file opens correctly on your computer
> 3. Remove password protection from PDFs
> 4. Ensure PDFs contain extractable text (not just images)
> 5. Try converting to a different supported format

> [accordion] Presigned URL expires
>
> **Error when uploading to presigned URL**
>
> **Solutions**:
>
> * Presigned URLs expire after 1 hour
> * Request a new presigned URL if expired
> * Upload the file immediately after receiving the URL

## Next steps

[Test the retrieved passages and persona answers](https://anam.ai/docs/personas/knowledge/testing.md), then complete the [upload checklist](https://anam.ai/docs/personas/knowledge/upload-checklist.md).

> [card] [Connect knowledge tools](https://anam.ai/docs/personas/knowledge/tools.md)
>
> Enable semantic search with knowledge tools

> [card] [Set up your knowledge base](https://anam.ai/docs/personas/knowledge/setup.md)
>
> Organize documents with folders

> [card] [Single-request upload](https://anam.ai/docs/api-reference/knowledge/upload-knowledge-group-document.md)
>
> The multipart alternative to the presigned flow, for small files

> [card] [Knowledge overview](https://anam.ai/docs/personas/knowledge/overview.md)
>
> Learn how RAG works


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