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

# Upload and manage documents

> Add documents to a ModuleX knowledge base, watch them process into searchable chunks, and retry, inspect, or delete them. Upload size is set by your plan, not a fixed 50 MB cap.

export const MediaEmbed = ({id, type = 'screenshot', caption = '', ext, ratio = '16 / 9'}) => {
  const isVideo = type === 'video' || type === 'app_video';
  const resolvedExt = ext || (isVideo ? 'mp4' : type === 'screenshot' ? 'webp' : 'svg');
  const src = 'https://media.modulex.dev/' + id + '.' + resolvedExt;
  const [status, setStatus] = useState('loading');
  const [isDev, setIsDev] = useState(false);
  const [inView, setInView] = useState(false);
  const boxRef = useRef(null);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const h = window.location.hostname;
    setIsDev(h === 'localhost' || h === '127.0.0.1' || h.endsWith('.mintlify.app'));
  }, []);
  useEffect(() => {
    if (inView) return;
    if (typeof IntersectionObserver === 'undefined') {
      setInView(true);
      return;
    }
    const el = boxRef.current;
    if (!el) return;
    const io = new IntersectionObserver(entries => {
      if (entries.some(e => e.isIntersecting)) {
        setInView(true);
        io.disconnect();
      }
    }, {
      rootMargin: '300px'
    });
    io.observe(el);
    return () => io.disconnect();
  }, [inView]);
  if (status === 'missing') {
    if (!isDev) return null;
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      gap: '0.4rem',
      padding: '1rem 1.25rem',
      margin: '1.25rem 0',
      width: '100%',
      aspectRatio: ratio,
      boxSizing: 'border-box',
      border: '1px dashed rgba(128,128,128,0.45)',
      borderRadius: '0.75rem',
      background: 'rgba(128,128,128,0.06)',
      color: 'currentColor',
      fontSize: '0.85rem',
      lineHeight: 1.45
    }}>
        <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      opacity: 0.75
    }}>
          <span aria-hidden="true">🎬</span>
          <code style={{
      fontSize: '0.75rem'
    }}>{id}</code>
          <span style={{
      fontSize: '0.65rem',
      textTransform: 'uppercase',
      letterSpacing: '0.04em',
      padding: '0.1rem 0.4rem',
      borderRadius: '0.4rem',
      background: 'rgba(128,128,128,0.18)'
    }}>
            {type}
          </span>
        </div>
        <div style={{
      opacity: 0.9
    }}>{caption || 'Media not uploaded yet.'}</div>
        <div style={{
      fontSize: '0.7rem',
      opacity: 0.5
    }}>
          Upload to R2 as <code>{id}.{resolvedExt}</code> — preview only, hidden in production.
        </div>
      </div>;
  }
  const mediaStyle = {
    display: status === 'loaded' ? 'block' : 'none',
    width: '100%',
    height: 'auto',
    borderRadius: '0.75rem'
  };
  const media = isVideo ? <video src={inView ? src : undefined} autoPlay loop muted playsInline preload="metadata" onLoadedData={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} /> : <img src={inView ? src : undefined} alt={caption} onLoad={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} />;
  return <figure style={{
    margin: '1.25rem 0'
  }}>
      <div ref={boxRef} style={status === 'loaded' ? {
    width: '100%'
  } : {
    width: '100%',
    aspectRatio: ratio,
    borderRadius: '0.75rem',
    background: 'rgba(128,128,128,0.06)'
  }}>
        {media}
      </div>
      {status === 'loaded' && caption ? <figcaption style={{
    marginTop: '0.5rem',
    textAlign: 'center',
    fontSize: '0.85rem',
    opacity: 0.7
  }}>
          {caption}
        </figcaption> : null}
    </figure>;
};

Documents are the files you add to a [knowledge base](/platform/knowledge/overview)
so ModuleX can answer from your own material. When you upload a file, ModuleX
extracts its text, splits it into smaller passages called **chunks**, and turns
each chunk into a searchable vector. This page is the hands-on guide to that
lifecycle: how to add files, watch them process, fix the ones that fail, inspect
their chunks, and remove the ones you no longer need.

If you are setting up a knowledge base for the first time, start with
[Knowledge overview](/platform/knowledge/overview). For the ideas behind chunks,
embeddings, and retrieval, see [Knowledge & RAG](/concepts/knowledge-rag).

<Note>
  Documents live inside a knowledge base, which belongs to your **organization**.
  Adding and managing documents requires the **owner** or **admin** role — see
  [Roles & permissions](/security/roles-permissions). Every request is scoped to
  your organization by the `X-Organization-ID` header (see
  [Org context](/security/org-context)).
</Note>

## Add documents

Open a knowledge base, then drag files onto the upload area or click to choose
them. You can select several files at once; ModuleX uploads them one after another.

<Steps>
  <Step title="Choose your files">
    Drag and drop, or click the upload area and pick files. Each file is checked
    against the [supported formats](#supported-file-types) and your
    [plan's size limit](#upload-size-is-set-by-your-plan) before it is sent.
  </Step>

  <Step title="ModuleX uploads each file">
    Files upload one at a time. As soon as a file is accepted, it appears as a
    document with the status **pending**.
  </Step>

  <Step title="Processing starts automatically">
    ModuleX prepares each document in the background — extracting text, splitting
    it into chunks, and embedding each chunk. You can keep working while large
    files process. See [Watch documents process](#watch-documents-process).
  </Step>
</Steps>

<MediaEmbed id="MX-MEDIA-3410" type="app_video" caption={"Adding several documents to a knowledge base and watching them move from pending to completed."} />

### Supported file types

A knowledge base accepts these formats. The file type is read from the file
extension first, then from its content type if the extension is missing.

<CardGroup cols={2}>
  <Card title="Documents" icon="file-text">
    PDF (`.pdf`), Word (`.docx`, `.doc`), plain text (`.txt`), and
    Markdown (`.md`).
  </Card>

  <Card title="Web and structured data" icon="file-code">
    HTML (`.html`), CSV (`.csv`), and JSON (`.json`).
  </Card>

  <Card title="Spreadsheets and slides" icon="table">
    Excel (`.xlsx`) and PowerPoint (`.pptx`).
  </Card>

  <Card title="Check the live list" icon="list-checks">
    The app fetches the current list of supported types when you open the upload
    area, so it always matches what the service accepts.
  </Card>
</CardGroup>

A file must contain at least some extractable text. An empty file is rejected
before processing begins.

### Upload size is set by your plan

The largest file you can upload is a **plan entitlement**, not a single fixed
cap. Although some older surfaces still mention a flat 50 MB limit, the value that
is actually enforced comes from your organization's plan:

| Plan       | Max file size per upload |
| ---------- | ------------------------ |
| Free       | 10 MB                    |
| Pro        | 50 MB                    |
| Max        | 100 MB                   |
| Enterprise | Unlimited                |

<Warning>
  The static "supported file types" info that the upload area reads still advertises
  **50 MB** regardless of plan. That number is a fallback only. The limit enforced
  when you actually upload is the plan value above — so on the **Free** plan a 40 MB
  file is rejected even though 50 MB is shown, and on the **Max** plan a 90 MB file
  is accepted. Treat your **plan** as the source of truth, and confirm your tier on
  [Plans & pricing](/billing/plans).
</Warning>

A second limit applies per knowledge base: there is a cap on how many documents a
single knowledge base can hold, and your plan can set this too. If you hit it,
delete some documents or use another knowledge base. See
[Plan limits](/platform/knowledge/overview#plan-limits) for the full table.

### Duplicate files are skipped

ModuleX fingerprints each file's contents. If you upload a file whose contents
exactly match a document already in that knowledge base, the upload is rejected as
a duplicate rather than stored twice. Change the file (even slightly) or upload it
to a different knowledge base if you genuinely need a second copy.

### Add a note to a document (optional)

When you upload over the API, you can attach a small JSON object of metadata — for
example `{"tags": ["product"]}` — to label or group documents. The metadata must
be valid JSON; an invalid value is rejected. See
[Add documents over the API](#add-documents-over-the-api).

## Watch documents process

Every document reports its progress, so you always know whether it is ready to
search. While files are still being prepared, the app refreshes the status
automatically every few seconds — you do not need to reload the page.

<Accordion title="The four document states: pending, processing, completed, failed">
  A document moves through these states in order:

  * **Pending** — uploaded and waiting in the queue. No chunks yet.
  * **Processing** — being parsed, split into chunks, and embedded. This is where
    text extraction and vectorizing happen.
  * **Completed** — fully prepared. The document now has a **chunk count** and a
    **token count**, and its chunks are searchable.
  * **Failed** — something went wrong while preparing the file. The document keeps
    an error message explaining what happened, and you can [retry](#retry-a-failed-document)
    it.

  A failed document can be retried, which sends it back to **pending** and runs the
  preparation again. Only **completed** and **failed** documents appear in the
  document table; documents that are still **pending** or **processing** are shown
  as in-flight in the knowledge base header.
</Accordion>

A completed document shows two useful numbers:

<CardGroup cols={2}>
  <Card title="Chunk count" icon="layers">
    How many searchable passages the document was split into. More chunks usually
    means a longer document, or smaller chunk-size settings on the knowledge base.
  </Card>

  <Card title="Token count" icon="hash">
    The total number of tokens across the document's chunks. This is what managed
    knowledge bases meter for embedding — see [What processing costs](#what-processing-costs).
  </Card>
</CardGroup>

<MediaEmbed id="MX-MEDIA-3411" type="screenshot" caption={"The document table inside a knowledge base, showing documents with their status, chunk count, and token count."} />

## Retry a failed document

If a document ends up **failed**, the most common causes are a corrupt file, an
unreadable scan with no extractable text, or a format the parser could not open.
Fix the source file if needed, then retry.

<Steps>
  <Step title="Open the failed document">
    Find the document in the table — failed documents show an error message that
    usually explains what went wrong.
  </Step>

  <Step title="Select retry">
    Retrying resets the document to **pending**, clears the old error and
    timestamps, and runs the preparation pipeline again from the start.
  </Step>

  <Step title="Watch it process again">
    The document moves back through **processing** to **completed** (or **failed**
    again if the underlying problem is not fixed).
  </Step>
</Steps>

<Tip>
  Retry only works on a document that is in the **failed** state. There is no
  separate "reindex" action — retrying a failed document re-runs the full
  parse, chunk, and embed pipeline, which produces fresh chunks. To rebuild a
  document that already **completed** (for example after changing the file), delete
  it and upload the new version.
</Tip>

<Info>
  **Retrying does not double-charge.** On a [managed (modulexdb)](/platform/knowledge/managed)
  knowledge base, the embedding cost for a document is recorded against its content,
  so retrying an unchanged document does not spend embedding credits again. See
  [What processing costs](#what-processing-costs).
</Info>

## Inspect a document's chunks

Once a document is **completed**, you can look at the individual chunks it was
split into. This is the quickest way to understand why a search returns a
particular passage, or to check that a document was extracted cleanly.

Each chunk carries:

| Field                     | What it is                                                 |
| ------------------------- | ---------------------------------------------------------- |
| `chunk_index`             | The chunk's position within the document, starting at `0`. |
| `content`                 | The chunk's text.                                          |
| `token_count`             | How many tokens the chunk holds.                           |
| `start_char` / `end_char` | Where the chunk falls in the original document.            |
| `has_embedding`           | Whether the chunk has a searchable vector yet.             |
| `metadata`                | Any metadata carried with the chunk.                       |

You can also run the knowledge base's **search test** to preview which chunks a
question returns, ranked by relevance — see
[Use a knowledge base](/platform/knowledge/overview#use-a-knowledge-base).

## Delete a document

Deleting a document removes it and all of its chunks from the knowledge base.
By default it also removes the stored file. Deletion is permanent — there is no
undo — so the document stops contributing to answers immediately.

<Warning>
  Deleting a document removes its chunks from search right away. Any chat, the
  [Assistant](/concepts/assistant), or [workflow](/concepts/workflows-and-runs)
  that relied on that material will no longer find it. If you only want to pause a
  whole library rather than lose a file, **archive the knowledge base** instead —
  see [Manage a knowledge base over time](/platform/knowledge/overview#manage-a-knowledge-base-over-time).
</Warning>

## What processing costs

Whether preparing a document costs anything depends on where the knowledge base
stores its vectors.

<CardGroup cols={2}>
  <Card title="Managed (modulexdb)" icon="database" href="/platform/knowledge/managed">
    On a [managed](/platform/knowledge/managed) knowledge base, ModuleX hosts the
    vector storage. Uploading and processing a document meters
    [credits](/billing/credits): a base ingest charge plus the embedding cost
    based on the document's token count.
  </Card>

  <Card title="Bring your own (BYOK)" icon="key" href="/platform/knowledge/external-providers">
    On a knowledge base pointed at your own vector store, ModuleX does not charge
    credits for processing — your provider bills you directly. See
    [External knowledge providers](/platform/knowledge/external-providers).
  </Card>
</CardGroup>

<Info>
  On a managed knowledge base, an upload that would exceed your plan allowance can
  return a **billing denial** — a `402`, `403`, or `429` response carrying a
  `{code, layer, key, current, limit, reason}` envelope — instead of storing the
  document. Plan size and document-count limits, by contrast, return a `403` with a
  plain `{detail}` message. See [Credits & metering](/billing/credits),
  [Usage gating & limits](/billing/usage-gating), and
  [Errors & status codes](/api-reference/errors).
</Info>

## Add documents over the API

Everything above is available programmatically. Upload is a
`multipart/form-data` request: send the file as the `file` part, and an optional
JSON `metadata` part. Authenticate every request the same way as every other
endpoint — an `Authorization: Bearer mx_live_…` header plus `X-Organization-ID`
(see [Authentication](/api-reference/authentication)).

<CodeGroup>
  ```bash cURL theme={null}
  # Upload a document to a knowledge base
  curl -X POST https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents \
    -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \
    -H "X-Organization-ID: org_3xampleOrg42" \
    -F "file=@manual.pdf" \
    -F 'metadata={"tags":["product"]}'

  # List the documents in a knowledge base
  curl https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents \
    -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \
    -H "X-Organization-ID: org_3xampleOrg42"

  # Check one document's processing status
  curl https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents/d1f0a2b3-4c5d-6e7f-8a9b-0c1d2e3f4a5b/status \
    -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \
    -H "X-Organization-ID: org_3xampleOrg42"

  # Retry a failed document
  curl -X POST https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents/d1f0a2b3-4c5d-6e7f-8a9b-0c1d2e3f4a5b/retry \
    -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \
    -H "X-Organization-ID: org_3xampleOrg42"

  # Delete a document
  curl -X DELETE https://api.modulex.dev/knowledge-bases/7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f/documents/d1f0a2b3-4c5d-6e7f-8a9b-0c1d2e3f4a5b \
    -H "Authorization: Bearer mx_live_8Kd2pQ7rExample" \
    -H "X-Organization-ID: org_3xampleOrg42"
  ```

  ```python Python theme={null}
  import asyncio
  from modulex import Modulex

  KB_ID = "7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f"


  async def main():
      async with Modulex(
          api_key="mx_live_8Kd2pQ7rExample",
          organization_id="org_3xampleOrg42",
      ) as mx:
          # Upload a document
          with open("manual.pdf", "rb") as file:
              doc = await mx.knowledge.documents.upload(
                  knowledge_base_id=KB_ID,
                  file=file,
                  metadata={"tags": ["product"]},
              )
          print(doc.id, doc.status)  # e.g. "...", "pending"

          # List the documents in the knowledge base
          documents = await mx.knowledge.documents.list(knowledge_base_id=KB_ID)
          for d in documents:
              print(d.id, d.filename, d.status, d.chunk_count)


  asyncio.run(main())
  ```

  ```javascript JavaScript theme={null}
  import { readFileSync } from "node:fs";
  import { Modulex } from "modulex";

  const KB_ID = "7c1e8f20-1d2a-4b6c-9e3f-2a1b4c5d6e7f";

  const client = new Modulex({
    apiKey: "mx_live_8Kd2pQ7rExample",
    organizationId: "org_3xampleOrg42",
  });

  // Upload a document
  const doc = await client.knowledge.documents.upload({
    knowledgeBaseId: KB_ID,
    file: new Blob([readFileSync("manual.pdf")]),
    filename: "manual.pdf",
    metadata: { tags: ["product"] },
  });
  console.log(doc.id, doc.status); // e.g. "...", "pending"

  // List the documents in the knowledge base
  const documents = await client.knowledge.documents.list({
    knowledgeBaseId: KB_ID,
  });
  for (const d of documents) {
    console.log(d.id, d.filename, d.status, d.chunkCount);
  }
  ```
</CodeGroup>

A freshly uploaded document comes back with status `pending`. Poll its `status`
to follow the move through `processing` to `completed` (or `failed`). A completed
document reports its `chunk_count` and `token_count`; a failed one carries an
`error_message`.

<Note>
  The exact SDK method names and argument shapes for document upload, list, status,
  retry, and delete are not yet pinned in the SDK reference; the snippets above show
  the operation and the request shape the REST API expects. For the authoritative
  per-operation signatures, see the [SDKs overview](/sdks/overview) and the
  [SDK ⇄ API parity matrix](/sdks/parity).
</Note>

## Where to go next

<CardGroup cols={2}>
  <Card title="Knowledge overview" icon="library" href="/platform/knowledge/overview">
    Create a knowledge base, set its chunking, and use it across chats and
    workflows.
  </Card>

  <Card title="Managed knowledge (modulexdb)" icon="database" href="/platform/knowledge/managed">
    How ModuleX-hosted vector storage and retrieval work, and what they cost.
  </Card>

  <Card title="Knowledge & RAG" icon="book-open" href="/concepts/knowledge-rag">
    The concepts behind documents: chunks, embeddings, ingest, and retrieval.
  </Card>

  <Card title="Build a RAG knowledge base" icon="hammer" href="/guides/build-a-knowledge-base">
    A start-to-finish guide: create a knowledge base, ingest documents, and query
    it.
  </Card>

  <Card title="Credits & metering" icon="coins" href="/billing/credits">
    What a credit is and exactly what processing a document consumes.
  </Card>

  <Card title="Chat with your knowledge" icon="messages-square" href="/platform/chat/knowledge-chat">
    Ask questions in chat answered from your completed documents.
  </Card>
</CardGroup>
