> ## 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.

# Managed knowledge (modulexdb)

> The complete technical reference for ModuleX-hosted vector storage: the per-knowledge-base embedding and chunking config with every field, type, and default; the ingest and retrieval pipelines; the credit cost of ingest and retrieval; the billing gate that returns 402/403/429; and full worked examples in cURL, Python, and JavaScript.

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>;
};

**Managed knowledge** is the ModuleX-hosted vector store, identified on the wire by the
provider name `modulexdb`. When a [knowledge base](/platform/knowledge/overview) embeds on
the managed pool, ModuleX provisions the embedding model, runs the parse → chunk → embed
pipeline, stores vectors in the managed vector store, and serves retrieval — and **meters that work in
[credits](/billing/credits)**. This page is the exact reference for the embedding and
chunking configuration, the ingest and retrieval pipelines, what each operation costs, the
billing gate that can deny managed work, and worked examples in three languages.

A knowledge base is **managed** when its `embedding_config.integration_name` is `modulexai`.
Any other value makes the knowledge base **bring-your-own-key (BYOK)** — you supply the
vector store and embedding credential, and that usage is **never credited** (see
[External knowledge providers](/platform/knowledge/external-providers)). Everything on this
page about credit cost and the billing gate applies **only** to managed (`modulexdb`)
knowledge bases.

<Note>
  **Managed knowledge usage is gated.** Managed ingest and managed retrieval pass through the
  ModuleX billing admission gate before any embedding runs. When your plan allowance or wallet
  is exhausted, or a rate limit is hit, the gate returns a flat `DenialEnvelope` as
  `402` / `403` / `429` and **no document or search result is produced**. The plain knowledge
  CRUD routes (create, list, update a knowledge base) are **not** gated and return the
  ordinary `{detail}` error shape instead. See [The billing gate](#the-billing-gate-402-403-429)
  below and [Usage gating & limits](/billing/usage-gating).
</Note>

## How managed knowledge fits together

Managed knowledge is one configuration of a knowledge base, not a separate object. The same
knowledge base, documents, and chunks back both managed and BYOK knowledge — only the
embedding provider differs.

<Steps>
  1. **Create a knowledge base** with a managed `embedding_config` (`integration_name`
     `modulexai`). ModuleX auto-creates the linked credential so [workflow knowledge
     nodes](/workflow-builder/nodes/knowledge) can reference it. This step is **not** credit-gated.

  2. **Upload a document.** The managed-ingest gate reserves `1` ingest credit before any
     write. The document is stored with status `pending`, and a background
     worker is enqueued.

  3. **The worker ingests it**: parse → chunk → embed → write vectors to the managed vector store. The
     per-document embedding token cost is recorded when the worker finishes.

  4. **Retrieve.** A search, hybrid search, retrieve-context, or multi-knowledge-base search
     reserves `1` retrieval credit before embedding the query, runs the vector search, and
     records the query-embedding token cost on success.
</Steps>

<MediaEmbed id="MX-MEDIA-3390" type="image" caption={"Managed-knowledge lifecycle diagram from create through ingest to retrieval."} />

## Embedding configuration

`embedding_config` is a per-knowledge-base JSON object that controls which model produces
the vectors. ModuleX reads the keys defensively, so two naming conventions are accepted for
the same settings; document and accept both.

<Warning>
  **Key drift — two accepted conventions.** The model default uses `provider` + `model_id`,
  while the service default uses `integration_name` + `provider_id` + `model_id` +
  `credential_id`. The embed code reads either spelling: `provider` or `provider_id`, `model`
  or `model_id`, `provider_credential_id` or `credential_id`. The managed-vs-BYOK decision is
  made **only** on `integration_name == "modulexai"`, so a managed knowledge base must set
  `integration_name`.
</Warning>

<ParamField path="embedding_config.integration_name" type="string" default="openai">
  The ModuleX integration that owns the embedding pool. Set to `modulexai` to make the
  knowledge base **managed** (billed in credits). Any other value (for example `openai`,
  `cohere`) is treated as BYOK and is not credited.
</ParamField>

<ParamField path="embedding_config.provider_id" type="string" default="openai">
  The actual embedding provider. Validated against `openai`, `cohere`, `azure`,
  `huggingface`, but only `openai` and `cohere` have an embedding implementation — anything
  else raises `Unsupported embedding provider` at embed time. Also accepted under the key
  `provider`.
</ParamField>

<ParamField path="embedding_config.model_id" type="string" default="text-embedding-3-small">
  The embedding model. Also accepted under the key `model`. For managed knowledge bases the
  model resolves through the ModuleX model pool so that ingest-time and search-time vectors
  are produced by the same model.
</ParamField>

<ParamField path="embedding_config.dimension" type="integer" default="1536">
  The embedding vector dimension. Range `64`–`4096`. The embedding vector is sized for the
  default `1536` (the `text-embedding-3-small` dimension); `text-embedding-3-large` supports
  up to `4096`. Out-of-range values are rejected at create or update with a
  `KnowledgeBaseValidationError` → `400`.
</ParamField>

<ParamField path="embedding_config.credential_id" type="string" required={false}>
  The embedding credential. Also accepted as `provider_credential_id`. If omitted, ModuleX
  auto-discovers an org credential whose integration exposes an embedding-capable model
  (`integration_type == "llm_provider"` with a model where `is_embedding` is true). If none
  exists, create fails with `NoEmbeddingCredentialError` → `400`.
</ParamField>

A managed `embedding_config` looks like this:

```json theme={null}
{
  "integration_name": "modulexai",
  "provider_id": "openai",
  "model_id": "text-embedding-3-small",
  "dimension": 1536,
  "credential_id": "a1b2c3d4-0000-4000-8000-000000000001"
}
```

## Chunking configuration

`chunking_config` controls how each document is split before embedding. It is validated on
create and merged-then-revalidated on update.

<ParamField path="chunking_config.strategy" type="string" default="recursive">
  The chunking strategy. One of `recursive` (a recursive character splitter that prefers the
  configured separators), `token` (a fixed token window using the `cl100k_base` tokenizer),
  or `simple` (a fixed character window). Any other value is rejected with a
  `KnowledgeBaseValidationError` → `400`.
</ParamField>

<ParamField path="chunking_config.chunk_size" type="integer" default="1000">
  Target chunk size. Range `100`–`4000`. For `token` strategy this is a token count; for the
  others it is a character count.
</ParamField>

<ParamField path="chunking_config.chunk_overlap" type="integer" default="200">
  Overlap between adjacent chunks. Range `0`–`500`, and it cannot exceed `50%` of
  `chunk_size`. The default `200` is `20%` of the default `chunk_size` of `1000`. A breach
  raises `KnowledgeBaseValidationError` → `400`.
</ParamField>

<ParamField path="chunking_config.separators" type="string[]" default={`["\\n\\n", "\\n", " ", ""]`}>
  Ordered separators tried by the `recursive` strategy, from coarsest to finest. The default
  is paragraph, then line, then word, then character. Ignored by `token` and `simple`.
</ParamField>

A default `chunking_config`:

```json theme={null}
{
  "strategy": "recursive",
  "chunk_size": 1000,
  "chunk_overlap": 200,
  "separators": ["\n\n", "\n", " ", ""]
}
```

## Storage, documents, and chunks

Each managed knowledge base stores three kinds of records. The `id` values below are UUIDs.

<ResponseField name="knowledge_base" type="object">
  The knowledge base itself: `name`, `description`, the `embedding_config` and
  `chunking_config` above, a `status` of `active` / `processing` / `error` / `archived`, and
  an auto-created linked `credential_id` so [workflow knowledge
  nodes](/workflow-builder/nodes/knowledge) can reference it.
</ResponseField>

<ResponseField name="document" type="object">
  One per uploaded file: `filename`, `file_type`, `file_size_bytes`, a `file_hash`
  (a content hash, used for in-knowledge-base deduplication), a `status` of `pending` /
  `processing` / `completed` / `failed`, `chunk_count`, `token_count`, and an
  `error_message` when ingest fails.
</ResponseField>

<ResponseField name="chunk" type="object">
  One per chunk: the chunk `content`, its `token_count`, `chunk_index`, `start_char` /
  `end_char`, and the `embedding` vector. Chunks are what retrieval searches over.
</ResponseField>

### Document limits

These limits apply to managed and BYOK knowledge bases alike.

| Limit                        | Value                                                                    | Notes                                                                                                                                                                                                                                                                 |
| ---------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Supported file types         | `pdf`, `docx`, `doc`, `txt`, `md`, `html`, `csv`, `json`, `xlsx`, `pptx` | Resolved from the file extension first, then MIME type.                                                                                                                                                                                                               |
| Documents per knowledge base | `500`                                                                    | Hard cap.                                                                                                                                                                                                                                                             |
| Per-file size cap            | **Plan entitlement**                                                     | The enforced cap is your plan's `max_file_size_bytes` (Free 10 MB, Pro 50 MB, Max 100 MB, Enterprise unlimited), **not** a flat 50 MB. The `/knowledge-bases/info/supported-file-types` endpoint advertises 50 MB as a fallback, but the plan value is authoritative. |
| Total org storage            | **Plan entitlement**                                                     | Free 1 GiB, Pro 50 GiB, Max 500 GiB, Enterprise unlimited.                                                                                                                                                                                                            |

<Warning>
  **The upload cap is a plan entitlement, not a fixed 50 MB.** A document larger than your
  plan's `max_file_size_bytes`, or one that would push your org over its storage quota, is
  rejected with a `KnowledgeBaseQuotaExceededError` → `403` carrying the ordinary `{detail}`
  shape — **not** the billing `DenialEnvelope`. See [Managing documents](/platform/knowledge/documents)
  and [Plans & pricing](/billing/plans).
</Warning>

## Credit cost of ingest and retrieval

Managed knowledge is metered with **flat per-operation credits** plus the
**token-metered embedding cost**. One credit is worth `$0.01` (`100` credits = `$1.00`).
BYOK knowledge bases incur **none** of these charges. The authoritative cost reference is
[Credits & metering](/billing/credits).

| Operation                                    | `usage_type`  | Flat cost (credits) | Plus token cost                                                   | When charged                                                            |
| -------------------------------------------- | ------------- | :-----------------: | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Document ingest                              | `file_ingest` |        **1**        | Per-document embedding tokens (input-only) recorded by the worker | Reserved before the upload write; recorded once per document on success |
| Knowledge search / hybrid / retrieve-context | `retrieval`   |        **1**        | Query-embedding tokens (input-only) recorded on success           | Reserved before the query is embedded; recorded on success              |
| Multi-knowledge-base search                  | `retrieval`   |     **1 total**     | Query-embedding tokens (see caveat below)                         | One retrieval base if **any** searched knowledge base is managed        |

<ParamField path="FILE_INGEST_BASE" type="integer" default="1">
  Credits reserved per managed document ingest (`usage_type="file_ingest"`). Charged once
  per document and **idempotent on the document ID**, so a retry of an unchanged document is
  not double-charged.
</ParamField>

<ParamField path="RETRIEVAL_BASE" type="integer" default="1">
  Credits reserved per managed retrieval (`usage_type="retrieval"`), covering
  `/search`, `/hybrid-search`, `/retrieve-context`, and multi-knowledge-base search.
</ParamField>

The embedding token cost is computed as input tokens only — embedding forces
`completion_tokens` to `0` — and added to the flat base. The full formula and the credit
unit live in [Credits & metering](/billing/credits).

<Note>
  **Reprocessing a document does not re-charge the flat base.** Retrying a failed document or
  reprocessing one does not run the ingest gate again, and the per-document embedding cost is
  keyed on `{doc_id}:embedding:{token_count}`, so an unchanged reprocess is not re-charged. A
  document whose content changed enough to produce a different token count will record a fresh
  embedding cost.
</Note>

<Warning>
  **Multi-knowledge-base search records embedding tokens for the last knowledge base only.**
  A `POST /knowledge-bases/search` across several knowledge bases reserves exactly **one**
  retrieval base credit (if any of them is managed), but the per-query embedding token cost is
  recorded for the **last** searched knowledge base only — a known in-code accounting gap. The
  flat retrieval credit is always correct; the token line may undercount across several managed
  knowledge bases.
</Warning>

## The ingest pipeline

When you upload a document to a managed knowledge base, this is what happens, in order:

<Steps>
  1. **Gate before write.** The managed-ingest gate reserves `1` `file_ingest` credit. If your
     allowance or wallet is exhausted, or you are rate-limited, the gate raises a
     `DenialEnvelope` (`402` / `429`) and **no document row, no storage write, and no worker
     job** is created.

  2. **Validate and store.** The file is checked against your plan's size and storage quota
     (breach → `403` quota error), its content hash is computed for deduplication (a duplicate hash
     in the same knowledge base → `400`), and it is written to storage. The document is
     created with status `pending`.

  3. **Enqueue and respond.** A background ingest job is enqueued and the API responds `201`
     with the `pending` document. The flat ingest credit is recorded once on this success,
     idempotent on the document ID.

  4. **Worker ingests.** The worker sets the document to `processing`, parses it
     (per file type), chunks it with your `chunking_config`, embeds the chunks with your
     `embedding_config` model, writes the chunks with their vectors, and sets the document to
     `completed` with its final `chunk_count` and `token_count`. The per-document embedding
     token cost is recorded here.

  5. **On failure**, the document is set to `failed` with an `error_message`, and the worker
     retries up to its retry budget. You can re-trigger a `failed` document with the retry
     endpoint — which is **not** credit-gated.
</Steps>

Poll a document's processing with `GET /knowledge-bases/{kb_id}/documents/{document_id}/status`;
the full document and chunk APIs are covered in [Managing documents](/platform/knowledge/documents).

### Ingest a document

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.modulex.dev/knowledge-bases/7c1e8b2a-0000-4000-8000-000000000abc/documents \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: org_123" \
    -F "file=@manual.pdf" \
    -F 'metadata={"tags":["product"]}'
  ```

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

  async def main():
      async with ModuleX(
          api_key="mx_live_xxx",
          organization_id="org_123",
      ) as mx:
          with open("manual.pdf", "rb") as f:
              doc = await mx.knowledge_bases.documents.upload(
                  "7c1e8b2a-0000-4000-8000-000000000abc",
                  file=f,
                  metadata={"tags": ["product"]},
              )
          print(doc.id, doc.status)  # -> "pending"

  asyncio.run(main())
  ```

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

  const mx = new ModuleX({
    apiKey: "mx_live_xxx",
    organizationId: "org_123",
  });

  const doc = await mx.knowledgeBases.documents.upload(
    "7c1e8b2a-0000-4000-8000-000000000abc",
    {
      file: createReadStream("manual.pdf"),
      metadata: { tags: ["product"] },
    },
  );
  console.log(doc.id, doc.status); // -> "pending"
  ```
</CodeGroup>

The upload accepts `multipart/form-data` with a required `file` part and an optional
`metadata` part (a JSON string; invalid JSON → `400 {"detail":"Invalid metadata JSON"}`).
The response is the new document with `status` `pending`:

```json theme={null}
{
  "id": "d1f0a3b4-0000-4000-8000-0000000000d1",
  "knowledge_base_id": "7c1e8b2a-0000-4000-8000-000000000abc",
  "filename": "manual.pdf",
  "file_type": "pdf",
  "file_size_bytes": 820341,
  "status": "pending",
  "chunk_count": 0,
  "token_count": 0,
  "error_message": null,
  "created_at": "2026-06-20T12:00:00"
}
```

## The retrieval pipeline

A managed retrieval embeds your query, then runs a cosine vector search over the
knowledge base's chunks. The flat retrieval credit is reserved **before** the query is
embedded, so an exhausted allowance denies the search before any model call.

There are four retrieval shapes:

| Route                                            | What it returns                                                                                   |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| `POST /knowledge-bases/{kb_id}/search`           | Top-`k` chunks by cosine similarity.                                                              |
| `POST /knowledge-bases/{kb_id}/hybrid-search`    | Vector results blended with keyword full-text ranking, by `keyword_weight` / `semantic_weight`.   |
| `POST /knowledge-bases/{kb_id}/retrieve-context` | A single token-budgeted context string with `[Source: ...]` headers, ready to feed an LLM prompt. |
| `POST /knowledge-bases/search`                   | Multi-knowledge-base search: merges and re-ranks results across several knowledge bases.          |

### Search parameters

These are the parameters for `POST /knowledge-bases/{kb_id}/search`.

<ParamField body="query" type="string" required={true}>
  The search query. Minimum length `1`. It is embedded with the knowledge base's
  `embedding_config` model and compared against stored chunk vectors.
</ParamField>

<ParamField body="top_k" type="integer" default="5">
  Number of chunks to return. Range `1`–`50`.
</ParamField>

<ParamField body="min_score" type="number" default="0.0">
  Minimum cosine similarity, computed as `1 - distance`. Range `0.0`–`1.0`. Chunks below the
  floor are dropped. In `hybrid-search` the floor instead applies to the combined weighted
  score, so the two scales are not identical.
</ParamField>

<ParamField body="filters" type="object" required={false}>
  Optional filters. Supports `document_id` and `document_ids` to scope the search to specific
  documents.
</ParamField>

<ParamField body="include_content" type="boolean" default="true">
  Whether to include each chunk's text content in the matches.
</ParamField>

<ParamField body="include_metadata" type="boolean" default="true">
  Whether to include each chunk's metadata in the matches.
</ParamField>

### Search a managed knowledge base

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.modulex.dev/knowledge-bases/7c1e8b2a-0000-4000-8000-000000000abc/search \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: org_123" \
    -H "Content-Type: application/json" \
    -d '{"query":"how to install","top_k":5,"min_score":0.2}'
  ```

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

  async def main():
      async with ModuleX(
          api_key="mx_live_xxx",
          organization_id="org_123",
      ) as mx:
          result = await mx.knowledge_bases.search(
              "7c1e8b2a-0000-4000-8000-000000000abc",
              query="how to install",
              top_k=5,
              min_score=0.2,
          )
          for match in result.matches:
              print(match.score, match.document_filename)

  asyncio.run(main())
  ```

  ```javascript JavaScript theme={null}
  import { ModuleX } from "modulex";

  const mx = new ModuleX({
    apiKey: "mx_live_xxx",
    organizationId: "org_123",
  });

  const result = await mx.knowledgeBases.search(
    "7c1e8b2a-0000-4000-8000-000000000abc",
    { query: "how to install", topK: 5, minScore: 0.2 },
  );
  for (const match of result.matches) {
    console.log(match.score, match.documentFilename);
  }
  ```
</CodeGroup>

The response carries the matched chunks with their cosine `score`:

<ResponseField name="query" type="string">
  The query you sent, echoed back.
</ResponseField>

<ResponseField name="knowledge_base_id" type="string">
  The knowledge base searched.
</ResponseField>

<ResponseField name="top_k" type="integer">
  The effective `top_k`.
</ResponseField>

<ResponseField name="total_matches" type="integer">
  How many chunks matched at or above `min_score`.
</ResponseField>

<ResponseField name="matches" type="object[]">
  The ranked matches.

  <Expandable title="match object">
    <ResponseField name="chunk_id" type="string">The chunk's id.</ResponseField>
    <ResponseField name="document_id" type="string">The source document's id.</ResponseField>
    <ResponseField name="document_filename" type="string">The source document's filename.</ResponseField>
    <ResponseField name="chunk_index" type="integer">The chunk's position within its document.</ResponseField>
    <ResponseField name="score" type="number">Cosine similarity, `1 - distance`.</ResponseField>
    <ResponseField name="content" type="string">The chunk text, present when `include_content` is true.</ResponseField>
    <ResponseField name="metadata" type="object">The chunk metadata, present when `include_metadata` is true.</ResponseField>
  </Expandable>
</ResponseField>

```json theme={null}
{
  "query": "how to install",
  "knowledge_base_id": "7c1e8b2a-0000-4000-8000-000000000abc",
  "top_k": 5,
  "total_matches": 3,
  "matches": [
    {
      "chunk_id": "c1a2b3c4-0000-4000-8000-0000000000c1",
      "document_id": "d1f0a3b4-0000-4000-8000-0000000000d1",
      "document_filename": "manual.pdf",
      "chunk_index": 4,
      "score": 0.83,
      "content": "To install, download the package and run the setup script.",
      "metadata": {}
    }
  ]
}
```

### Retrieve a RAG context string

For feeding an LLM, `retrieve-context` returns one token-budgeted string instead of a list
of chunks. It takes `query` (required), `max_tokens` (`100`–`10000`, default `2000`),
`top_k` (`1`–`50`, default `10`), and `min_score` (`0.0`–`1.0`, default `0.3`), and returns
`{context, query}` where `context` joins the selected chunks with `[Source: <filename>,
Chunk <n>]` headers. It is billed identically to `/search` — one retrieval credit plus the
query-embedding token cost.

### Inside a workflow

A [knowledge node](/workflow-builder/nodes/knowledge) resolves a managed knowledge base from
its linked credential and retrieves at run time. Its `output_format` selects what the node
writes into run state — `chunks`, `context`, or `both` — and downstream nodes read it with a
`{{node_id.field}}` reference (see [Variables &
references](/workflow-builder/variables-and-references)). Retrieval inside a workflow is
metered through the same managed-retrieval path as the REST API, so it costs the same `1`
retrieval credit plus query-embedding tokens.

## The billing gate (402 / 403 / 429)

Managed ingest and managed retrieval flow through the ModuleX usage gate. On denial the gate
returns a flat `DenialEnvelope` — `{code, layer, key, current, limit, reason}` — at the
status that matches the layer. This is a **different** shape from the ordinary `{detail}`
errors that the plain knowledge CRUD routes return.

| Status | `layer`  | Typical `code`                                    | Cause                                                                                                                                    |
| :----: | -------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|  `402` | `credit` | `credit_plan_exhausted`                           | Plan credit allowance exhausted (and no wallet overage).                                                                                 |
|  `402` | `wallet` | `wallet_overage_disabled` / `wallet_insufficient` | Allowance gone and wallet overage is off or the balance is too low.                                                                      |
|  `403` | `quota`  | `quota_exceeded`                                  | A metered quota was hit at the gate.                                                                                                     |
|  `429` | `rate`   | `rate_limit_exceeded`                             | Per-minute rate limit hit. Carries `Retry-After` (default `60`) and `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset`. |

A `402` credit denial looks like this — note there is **no** `detail` wrapper:

```json theme={null}
{
  "code": "credit_plan_exhausted",
  "layer": "credit",
  "key": "org_123",
  "current": null,
  "limit": 5000.0,
  "reason": "credit_plan_exhausted"
}
```

<Warning>
  **Two error contracts coexist on the knowledge surface.** Managed-ingest and
  managed-retrieval credit / rate / quota denials use the flat `DenialEnvelope` above.
  Knowledge-base **count** quota and **per-file / storage** limits raise a
  `KnowledgeBaseQuotaExceededError` → `403` carrying the ordinary `{"detail": "..."}` shape —
  **not** the envelope. Branch on both. The full taxonomy is on [Errors & status
  codes](/api-reference/errors).
</Warning>

The Python SDK maps these by status to typed errors — `402` → `PaymentRequiredError` (with
`CreditExhaustedError` / `WalletError` subclasses), `403` → `PermissionError` /
`QuotaExceededError`, `429` → `RateLimitError`. See [Errors &
retries](/sdks/errors-retries) and [Usage gating & limits](/billing/usage-gating).

## Other errors

These are the non-billing errors managed-knowledge operations can return. They use the
ordinary `{"detail": "..."}` shape.

<ResponseField name="400 KnowledgeBaseValidationError" type="error">
  Invalid `embedding_config` or `chunking_config` (out-of-range dimension, chunk size,
  overlap, or an unknown strategy), invalid `status` on update, or an unsupported embedding
  provider configuration.
</ResponseField>

<ResponseField name="400 DocumentValidationError" type="error">
  An empty file, a duplicate file (same content hash already in the knowledge base), an
  unsupported file type, or invalid `metadata` JSON. Also returned when retrying a document
  that is not in the `failed` state.
</ResponseField>

<ResponseField name="400 NoEmbeddingCredentialError" type="error">
  No embedding credential was supplied and none could be auto-discovered for the org.
</ResponseField>

<ResponseField name="403" type="error">
  `KnowledgeBaseQuotaExceededError` (knowledge-base count, per-file size, or storage quota),
  or `SearchAccessDeniedError` / `DocumentAccessDeniedError` when the caller lacks access.
</ResponseField>

<ResponseField name="404" type="error">
  `KnowledgeBaseNotFoundError` or `DocumentNotFoundError`. A wrong-org document also returns
  `404`, not `403`.
</ResponseField>

<ResponseField name="422" type="error">
  Pydantic request validation failed — for example a missing required `query`, or a missing
  `file` part on upload.
</ResponseField>

<ResponseField name="500 EmbeddingError" type="error">
  The embedding call failed — including `Unsupported embedding provider` when the configured
  provider has no embedding implementation. `SearchServiceError` covers other retrieval-time
  failures.
</ResponseField>

## Authentication

Every managed-knowledge request requires an [API key](/api-reference/authentication) and an
[organization context](/security/org-context). Knowledge routes require the **owner** or
**admin** role; the retired `member` role cannot call them (see [Roles &
permissions](/security/roles-permissions)).

```bash Headers theme={null}
Authorization: Bearer mx_live_xxx
X-Organization-ID: org_123
Content-Type: application/json
```

The backend also accepts the API key as `X-API-KEY: mx_live_xxx`. Use
`Authorization: Bearer` for new code.

## Related pages

<CardGroup cols={2}>
  <Card title="Credits & metering" icon="coins" href="/billing/credits">
    The credit unit, the per-operation cost table, and the reserve → charge → settle
    lifecycle.
  </Card>

  <Card title="Usage gating & limits" icon="shield-check" href="/billing/usage-gating">
    The admission gate and its `402` / `403` / `429` `DenialEnvelope` responses.
  </Card>

  <Card title="Managing documents" icon="file-text" href="/platform/knowledge/documents">
    Upload, monitor processing, retry, and manage documents and chunks.
  </Card>

  <Card title="External knowledge providers" icon="database" href="/platform/knowledge/external-providers">
    Bring your own vector store — Qdrant, Pinecone, MongoDB Atlas, Weaviate — uncredited.
  </Card>

  <Card title="modulexdb (managed)" icon="layers" href="/integrations/knowledge-providers/modulexdb">
    The managed vector store as a knowledge provider.
  </Card>

  <Card title="Knowledge node" icon="square-function" href="/workflow-builder/nodes/knowledge">
    Retrieve from a managed knowledge base inside a workflow.
  </Card>
</CardGroup>
