> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developers.meshapi.ai/llms.txt.
> For full documentation content, see https://developers.meshapi.ai/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.meshapi.ai/_mcp/server.

# RAG (Retrieval-Augmented Generation)

> Upload your documents, search them semantically, and ground AI answers in your own content — all through the Mesh API.

RAG lets you upload files, automatically convert them into searchable chunks, and then query them with plain-language questions. The API returns the most relevant passages, which you can feed directly into a chat completion to get answers grounded in your own content.

The full flow is three steps:

1. **Upload** a file and get a `file_id`
2. **Wait** for embeddings to finish processing
3. **Search** with a natural-language query

```http
POST https://api.meshapi.ai/v1/files        # upload
POST https://api.meshapi.ai/v1/files/embed  # (re-)trigger embeddings
POST https://api.meshapi.ai/v1/files/search # semantic search
```

***

## Step 1 — Upload a file

Call `POST /v1/files` to register the file. You get back a `file_id` and a short-lived `signed_url` — use the signed URL to PUT the actual bytes directly to storage.

```bash
# 1a. Register the file and get an upload URL
INIT=$(curl -s https://api.meshapi.ai/v1/files \
  -H "Authorization: Bearer <YOUR_RSK_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "file_name": "handbook.pdf",
    "mime_type": "application/pdf",
    "embed": true
  }')

FILE_ID=$(echo $INIT | jq -r '.file_id')
SIGNED_URL=$(echo $INIT | jq -r '.signed_url')

# 1b. Upload the file bytes to the signed URL
curl -X PUT "$SIGNED_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @handbook.pdf

echo "File ID: $FILE_ID"
```

```python
import httpx

client = httpx.Client(headers={"Authorization": "Bearer rsk_..."})

# Register the file
init = client.post(
    "https://api.meshapi.ai/v1/files",
    json={
        "file_name": "handbook.pdf",
        "mime_type": "application/pdf",
        "embed": True,
    },
).json()

file_id = init["file_id"]
signed_url = init["signed_url"]

# Upload bytes to the signed URL (no auth header needed here)
with open("handbook.pdf", "rb") as f:
    httpx.put(signed_url, content=f.read(), headers={"Content-Type": "application/pdf"})

print(f"File ID: {file_id}")
```

```ts
import fs from "fs";

const init = await fetch("https://api.meshapi.ai/v1/files", {
  method: "POST",
  headers: {
    Authorization: "Bearer <YOUR_RSK_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    file_name: "handbook.pdf",
    mime_type: "application/pdf",
    embed: true,
  }),
}).then((r) => r.json());

const { file_id, signed_url } = init;

// Upload bytes directly to the signed URL
await fetch(signed_url, {
  method: "PUT",
  headers: { "Content-Type": "application/pdf" },
  body: fs.readFileSync("handbook.pdf"),
});

console.log("File ID:", file_id);
```

### Init upload fields

| Field       | Type    | Notes                                                                         |
| ----------- | ------- | ----------------------------------------------------------------------------- |
| `file_name` | string  | Name of the file (used for display and file-type detection)                   |
| `mime_type` | string  | MIME type, e.g. `application/pdf`, `text/plain`, `text/markdown`              |
| `embed`     | boolean | Default `true` — automatically triggers embedding after upload                |
| `metadata`  | object  | Optional key-value pairs attached to every chunk (useful for filtering later) |

### Init upload response

```json
{
  "file_id": "file_abc123",
  "signed_url": "https://storage.example.com/...",
  "expires_at": "2026-05-26T12:30:00Z"
}
```

The `signed_url` expires in a few minutes. PUT your file bytes immediately after receiving it.

***

## Step 2 — Wait for embeddings

After the PUT completes, the API automatically chunks and embeds your file (because `embed: true`). You can poll `GET /v1/files/{file_id}` to watch progress.

```bash
curl https://api.meshapi.ai/v1/files/$FILE_ID \
  -H "Authorization: Bearer <YOUR_RSK_KEY>"
```

```python
import time

while True:
    status = client.get(f"https://api.meshapi.ai/v1/files/{file_id}").json()
    print(status["upload_status"], status["embedding_status"])
    if status["embedding_status"] in ("completed", "failed"):
        break
    time.sleep(3)
```

```ts
const poll = async () => {
  while (true) {
    const status = await fetch(
      `https://api.meshapi.ai/v1/files/${file_id}`,
      { headers: { Authorization: "Bearer <YOUR_RSK_KEY>" } }
    ).then((r) => r.json());

    console.log(status.upload_status, status.embedding_status);
    if (["completed", "failed"].includes(status.embedding_status)) break;
    await new Promise((r) => setTimeout(r, 3000));
  }
};
await poll();
```

### Status fields

| Field              | Values                                                        | Meaning                         |
| ------------------ | ------------------------------------------------------------- | ------------------------------- |
| `upload_status`    | `pending` → `ready`                                           | File bytes have been received   |
| `embedding_status` | `pending` → `queued` → `processing` → `completed` \| `failed` | Chunking and embedding progress |

Once `embedding_status` is `completed`, the file is ready to search.

***

## Step 3 — Search your files

Send a natural-language query to `POST /v1/files/search`. The API converts your query into a vector, finds the closest chunks, and returns the raw text with relevance scores.

```bash
curl https://api.meshapi.ai/v1/files/search \
  -H "Authorization: Bearer <YOUR_RSK_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "What is the refund policy?",
    "top_k": 5
  }'
```

```python
results = client.post(
    "https://api.meshapi.ai/v1/files/search",
    json={
        "query": "What is the refund policy?",
        "top_k": 5,
    },
).json()

for r in results["results"]:
    print(f"[{r['score']:.3f}] {r['text'][:200]}")
```

```ts
const results = await fetch("https://api.meshapi.ai/v1/files/search", {
  method: "POST",
  headers: {
    Authorization: "Bearer <YOUR_RSK_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    query: "What is the refund policy?",
    top_k: 5,
  }),
}).then((r) => r.json());

for (const r of results.results) {
  console.log(`[${r.score.toFixed(3)}] ${r.text.slice(0, 200)}`);
}
```

### Search fields

| Field       | Type      | Notes                                                            |
| ----------- | --------- | ---------------------------------------------------------------- |
| `query`     | string    | Plain-language question or phrase                                |
| `top_k`     | integer   | Number of chunks to return (1–50, default 5)                     |
| `file_ids`  | string\[] | Restrict search to specific files; omit to search all your files |
| `filter`    | object    | Match chunks by metadata key-value pairs (exact match)           |
| `date_from` | integer   | Unix timestamp — only return chunks created after this time      |
| `date_to`   | integer   | Unix timestamp — only return chunks created before this time     |

### Search response

```json
{
  "results": [
    {
      "score": 0.912,
      "text": "Refunds are processed within 5–7 business days...",
      "parent_text": "Section 4: Returns and Refunds\n\nRefunds are processed...",
      "file_id": "file_abc123",
      "file_name": "handbook.pdf",
      "chunk_index": 14,
      "metadata": {}
    }
  ]
}
```

***

## End-to-end: RAG chat

Combine search results with a chat completion to answer questions from your documents.

```python
import httpx, json

API = "https://api.meshapi.ai"
HEADERS = {"Authorization": "Bearer rsk_...", "Content-Type": "application/json"}
client = httpx.Client(headers=HEADERS)

question = "What is the refund policy?"

# 1. Retrieve relevant chunks
search = client.post(f"{API}/v1/files/search", json={
    "query": question,
    "top_k": 3,
}).json()

context = "\n\n".join(r["text"] for r in search["results"])

# 2. Ask the model
chat = client.post(f"{API}/v1/chat/completions", json={
    "model": "openai/gpt-4o-mini",
    "messages": [
        {
            "role": "system",
            "content": f"Answer using only the context below.\n\n{context}",
        },
        {"role": "user", "content": question},
    ],
}).json()

print(chat["choices"][0]["message"]["content"])
```

```ts
const API = "https://api.meshapi.ai";
const headers = {
  Authorization: "Bearer <YOUR_RSK_KEY>",
  "Content-Type": "application/json",
};

const question = "What is the refund policy?";

// 1. Retrieve relevant chunks
const search = await fetch(`${API}/v1/files/search`, {
  method: "POST",
  headers,
  body: JSON.stringify({ query: question, top_k: 3 }),
}).then((r) => r.json());

const context = search.results.map((r: any) => r.text).join("\n\n");

// 2. Ask the model
const chat = await fetch(`${API}/v1/chat/completions`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    model: "openai/gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: `Answer using only the context below.\n\n${context}`,
      },
      { role: "user", content: question },
    ],
  }),
}).then((r) => r.json());

console.log(chat.choices[0].message.content);
```

***

## Filtering by metadata

Tag files at upload time and filter at search time — useful when you have documents from different departments, clients, or time periods.

```bash
# Upload with metadata
curl https://api.meshapi.ai/v1/files \
  -H "Authorization: Bearer <YOUR_RSK_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "file_name": "q4-report.pdf",
    "mime_type": "application/pdf",
    "metadata": { "department": "finance", "quarter": "Q4-2025" }
  }'

# Search only finance docs
curl https://api.meshapi.ai/v1/files/search \
  -H "Authorization: Bearer <YOUR_RSK_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "What were the operating expenses?",
    "filter": { "department": "finance" }
  }'
```

***

## Re-triggering embeddings

If `embed` was set to `false` at upload time, or if embedding failed, you can kick it off manually:

```bash
curl https://api.meshapi.ai/v1/files/embed \
  -H "Authorization: Bearer <YOUR_RSK_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "file_ids": ["file_abc123", "file_def456"]
  }'
```

The response returns per-file status:

```json
{
  "results": [
    { "file_id": "file_abc123", "embedding_status": "queued", "chunk_count": null },
    { "file_id": "file_def456", "embedding_status": "queued", "chunk_count": null }
  ]
}
```

You can pass `"wait": true` to block until all embeddings finish (useful for small files in scripts).

***

## List your files

```bash
curl "https://api.meshapi.ai/v1/files?limit=20&offset=0" \
  -H "Authorization: Bearer <YOUR_RSK_KEY>"
```

Response includes `files` (array of file status objects), `total`, `limit`, and `offset`.

***

## Common errors

| HTTP  | Meaning                                      |
| ----- | -------------------------------------------- |
| `401` | Missing or invalid API key                   |
| `402` | Balance or spend limit exceeded              |
| `422` | Invalid request body (check required fields) |
| `429` | Rate limit exceeded — back off and retry     |