> For a complete page index of the Captain API documentation, fetch https://docs.captain.dev/llms.txt?excludeSpec=true

# Quickstart

> Create a collection, index files from S3/GCS/Azure, and query with natural language in 3 steps. Includes curl, Python, TypeScript, and Ruby examples.

## Agent Quick Reference

Three-step workflow:

1. **Create collection**: `PUT /v2/collections/{name}` with JSON body `{"description": "..."}`. Returns 201 with `{"collection_name", "collection_id", "organization_id", "created_at", "description", "metadata"}`. Returns 200 if the collection already exists.
2. **Index files**: `POST /v2/collections/{name}/index/s3` (or `/gcs`, `/azure`, `/r2`). `processing_type` is required on these cloud-storage index requests (`"advanced"`, `"basic"` or `"auto"`). Returns `{"job_id", "status": "pending"}`. Poll `GET /v2/jobs/{job_id}` until status is `completed`.
3. **Query**: `POST /v3/collections/{name}/query` with `{"query": "...", "limit": 10}` for deterministic File Search results. Read `results`, including `document_id`, `text`, `chunk_id`, and source metadata.

All requests require the `Authorization: Bearer {CAPTAIN_API_KEY}` header. `X-Organization-ID` is optional; when omitted, the organization is derived from the API key.

Optional v3 query body fields: `rerank` (boolean or object), `limit` (integer), `filter` (object), `include` (object), `max_chunks_per_document` (integer), `semantic_ratio` (number), `exclude_chunk_types` (array). v2 remains available at `/v2/collections/{name}/query` with the older `search_results`/`content` field names.

For MCP integrations, skip this REST walkthrough and connect to `https://mcp.captain.dev/mcp`. See [MCP Server](/guides/mcp).

Three steps get a collection searchable: create it, index files into it, query it. This
page walks through all three with the API directly. To skip straight to a working
integration from your editor, use the [MCP Server](/guides/mcp) instead.

> **Info**
>
> The [MCP Server](/guides/mcp) connects Claude Code, Cursor, VS Code, and Windsurf to Captain in one step, no API code required.

## Prerequisites

### Get Your API Credentials

You'll need:

* **API Key** from [Captain API Studio](https://filesearch.dev/studio) (format: `cap_dev_...`, `cap_prod_...`)
* **Organization ID** (UUID format, also available in the [Studio](https://filesearch.dev/studio))

Store your API key securely, such as in an environment variable:

**`Environment Variables`**

```env title="Environment Variables"
CAPTAIN_API_KEY="cap_prod_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

## \[1/3] Create a Collection

In order for Captain to be able to search files, we need to first create a **Collection** for our files to be indexed into.

This is as easy as a single API call: See the [Create Collection - API Reference](/reference/collections/create-collection)

#### curl

```bash
curl -X PUT https://api.filesearch.dev/v2/collections/my_first_collection \
     -H "Authorization: Bearer $CAPTAIN_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"description": "My first Captain Collection"}'
```

#### Python

```python
import requests

response = requests.put(
    "https://api.filesearch.dev/v2/collections/my_first_collection",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json"
    },
    json={"description": "My first Captain Collection"}
)

print(response.json())
```

#### TypeScript

```typescript
const response = await fetch(
  "https://api.filesearch.dev/v2/collections/my_first_collection",
  {
    method: "PUT",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ description: "My first Captain Collection" })
  }
);

console.log(await response.json());
```

After the collection is created, we should get a response like this:

**`Example: (201 Created)`**

```json Example: (201 Created)
{
  "collection_name": "my_first_collection",
  "collection_id": "019e94e8-3b50-74d7-9459-6ef378401335",
  "organization_id": "019ae889e89efd0029a25d559fda8cd4e3e4",
  "created_at": "2026-01-15T12:00:00.000000",
  "description": "My first Captain Collection",
  "metadata": null
}
```

> **Note**
>
> If the collection already exists, the call returns 200 instead of 201 with the existing collection details.

## \[2/3] Index Files into Collections

Next, index files into the collection. Captain connects to ten storage sources:

AWS S3

Google Cloud Storage

Azure Blob Storage

Cloudflare R2

Supabase Storage

Backblaze B2

Dropbox

Google Drive

SharePoint

OneDrive

The three worked examples below cover S3, GCS, and Azure. The same
`processing_type` and job-polling pattern carries over to every other connector;
see [Connect Cloud Storage](/guides/connect-cloud-storage)
for setup on each one.

Beyond cloud storage, Captain also reads from Notion, Slack, Snowflake, Linear,
Jira, and more. See [Integrations](/guides/integrations)
for the full list.

For a collection that stays matched to a bucket over time instead of a one-off index
call, see [Set Up Sync](/guides/sync/set-up) after finishing this walkthrough.

![Captain Indexing API Endpoints](https://y2i6auvwuaxlu05f.public.blob.vercel-storage.com/Captain-Indexing-API-Endpoints.png)

> **Info**
>
> Cloud-storage indexing requests (S3, GCS, Azure, R2, and the other bucket
> connectors) require a `processing_type` field. Omitting it on those endpoints
> returns a 422 validation error.
>
> | Value        | Best for                                                                                                                                                                                           |
> | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
> | `"advanced"` | Complex layouts, tables, figures, images. Agentic OCR with AI-enhanced extraction.                                                                                                                 |
> | `"basic"`    | General documents, high-volume processing. Reliable OCR.                                                                                                                                           |
> | `"auto"`     | Mixed collections. Captain inspects each PDF and uses `advanced` only for files with photos, drawings, tables or scanned pages, and `basic` for the rest. Each file is billed at the tier it used. |
>
> `index/file` defaults to `basic` when omitted. `index/text` and `index/youtube`
> don't take this field at all.

### Option A: Index AWS S3 Bucket

See the [Index S3 Bucket - API Reference](/reference/indexing/s3/bucket)

**Need AWS credentials?** See the [Connect Cloud Storage Guide](/guides/connect-cloud-storage) for step-by-step instructions.

#### curl

```bash
curl -X POST https://api.filesearch.dev/v2/collections/my_first_collection/index/s3 \
     -H "Authorization: Bearer $CAPTAIN_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "bucket_name": "my-s3-bucket",
       "aws_access_key_id": "AKIAIOSFODNN7EXAMPLE",
       "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
       "bucket_region": "us-east-1",
       "processing_type": "advanced"
     }'
```

#### Python

```python
import requests

BASE_URL = "https://api.filesearch.dev"
COLLECTION_NAME = "my_first_collection"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}

response = requests.post(
    f"{BASE_URL}/v2/collections/{COLLECTION_NAME}/index/s3",
    headers=headers,
    json={
        "bucket_name": "my-s3-bucket",
        "aws_access_key_id": "AKIAIOSFODNN7EXAMPLE",
        "aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        "bucket_region": "us-east-1",
        "processing_type": "advanced"  # Required: "advanced", "basic" or "auto"
    }
)

job_id = response.json()['job_id']
print(f"Indexing started! Job ID: {job_id}")
```

#### TypeScript

```typescript
const BASE_URL = "https://api.filesearch.dev";
const COLLECTION_NAME = "my_first_collection";

const response = await fetch(
  `${BASE_URL}/v2/collections/${COLLECTION_NAME}/index/s3`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      bucket_name: "my-s3-bucket",
      aws_access_key_id: "AKIAIOSFODNN7EXAMPLE",
      aws_secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
      bucket_region: "us-east-1",
      processing_type: "advanced"  // Required: "advanced", "basic" or "auto"
    })
  }
);

const result = await response.json();
const jobId = result.job_id;
console.log(`Indexing started! Job ID: ${jobId}`);
```

### Option B: Index Google Cloud Storage Bucket

See the [Index GCS Bucket - API Reference](/reference/indexing/gcs/bucket)

**Need GCS credentials?** See the [Connect Cloud Storage Guide](/guides/connect-cloud-storage) for step-by-step instructions.

#### curl

```bash
curl -X POST https://api.filesearch.dev/v2/collections/my_first_collection/index/gcs \
     -H "Authorization: Bearer $CAPTAIN_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "bucket_name": "my-gcs-bucket",
       "service_account_json": "{\"type\":\"service_account\",\"project_id\":\"...\"}",
       "processing_type": "advanced"
     }'
```

#### Python

```python
import requests

BASE_URL = "https://api.filesearch.dev"
COLLECTION_NAME = "my_first_collection"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}

# Load your service account JSON
with open('service-account-key.json', 'r') as f:
    service_account_json = f.read()

response = requests.post(
    f"{BASE_URL}/v2/collections/{COLLECTION_NAME}/index/gcs",
    headers=headers,
    json={
        "bucket_name": "my-gcs-bucket",
        "service_account_json": service_account_json,
        "processing_type": "advanced"  # Required: "advanced", "basic" or "auto"
    }
)

job_id = response.json()['job_id']
print(f"Indexing started! Job ID: {job_id}")
```

#### TypeScript

```typescript
import { readFileSync } from 'fs';

const BASE_URL = "https://api.filesearch.dev";
const COLLECTION_NAME = "my_first_collection";

// Load your service account JSON
const serviceAccountJson = readFileSync('service-account-key.json', 'utf-8');

const response = await fetch(
  `${BASE_URL}/v2/collections/${COLLECTION_NAME}/index/gcs`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      bucket_name: "my-gcs-bucket",
      service_account_json: serviceAccountJson,
      processing_type: "advanced"  // Required: "advanced", "basic" or "auto"
    })
  }
);

const result = await response.json();
const jobId = result.job_id;
console.log(`Indexing started! Job ID: ${jobId}`);
```

### Option C: Index Azure Blob Storage

See the [Index Azure Container - API Reference](/reference/indexing/azure/container)

#### curl

```bash
curl -X POST https://api.filesearch.dev/v2/collections/my_first_collection/index/azure \
     -H "Authorization: Bearer $CAPTAIN_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "container_name": "my-container",
       "account_name": "mystorageaccount",
       "account_key": "your_account_key_base64",
       "processing_type": "advanced"
     }'
```

#### Python

```python
import requests

BASE_URL = "https://api.filesearch.dev"
COLLECTION_NAME = "my_first_collection"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}

response = requests.post(
    f"{BASE_URL}/v2/collections/{COLLECTION_NAME}/index/azure",
    headers=headers,
    json={
        "container_name": "my-container",
        "account_name": "mystorageaccount",
        "account_key": "your_account_key_base64",
        "processing_type": "advanced"  # Required: "advanced", "basic" or "auto"
    }
)

job_id = response.json()['job_id']
print(f"Indexing started! Job ID: {job_id}")
```

#### TypeScript

```typescript
const BASE_URL = "https://api.filesearch.dev";
const COLLECTION_NAME = "my_first_collection";

const response = await fetch(
  `${BASE_URL}/v2/collections/${COLLECTION_NAME}/index/azure`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      container_name: "my-container",
      account_name: "mystorageaccount",
      account_key: "your_account_key_base64",
      processing_type: "advanced"  // Required: "advanced", "basic" or "auto"
    })
  }
);

const result = await response.json();
const jobId = result.job_id;
console.log(`Indexing started! Job ID: ${jobId}`);
```

### Monitor Indexing Progress

See the [Get Job Status - API Reference](/reference/jobs/get)

#### Python

```python
import time

while True:
    response = requests.get(
        f"{BASE_URL}/v2/jobs/{job_id}",
        headers={"Authorization": f"Bearer {API_KEY}"}
    )

    result = response.json()
    status = result.get('status')
    progress = result.get('progress_message', '')
    print(f"Status: {status} - {progress}")

    if status in ['completed', 'completed_with_errors', 'failed', 'cancelled', 'timed_out']:
        if status in ['completed', 'completed_with_errors']:
            print("Indexing complete!")
            final = result.get('result', {})
            print(f"Files indexed: {final.get('files_indexed', 0)}")
        break

    time.sleep(5)
```

#### TypeScript

```typescript
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

while (true) {
  const response = await fetch(
    `${BASE_URL}/v2/jobs/${jobId}`,
    {
      headers: { "Authorization": `Bearer ${API_KEY}` }
    }
  );

  const result = await response.json();
  const status = result.status;
  const progress = result.progress_message || '';
  console.log(`Status: ${status} - ${progress}`);

  if (['completed', 'completed_with_errors', 'failed', 'cancelled', 'timed_out'].includes(status)) {
    if (status === 'completed' || status === 'completed_with_errors') {
      console.log("Indexing complete!");
      const final = result.result || {};
      console.log(`Files indexed: ${final.files_indexed || 0}`);
    }
    break;
  }

  await sleep(5000);
}
```

To be notified when a job finishes instead of polling, register a webhook endpoint. Captain sends one signed event per job with its final status and file counts. A retry can deliver the same event more than once, so deduplicate on the `webhook-id` header. `GET /v2/jobs/{job_id}` stays available for per-file detail. See [Webhooks](/guides/webhooks).

## \[3/3] Querying Collections

Once your files are indexed, query the collection to retrieve source chunks. See the [v3 Query - API Reference](/reference/v3/query) or the [File Search API guide](/guides/file-search-api).

### Deterministic File Search

#### curl

```bash
curl -X POST https://api.filesearch.dev/v3/collections/my_first_collection/query \
     -H "Authorization: Bearer $CAPTAIN_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "query": "What are the revenue projections for Q4?",
       "limit": 10,
       "rerank": true
     }'
```

#### Python

```python
import uuid

response = requests.post(
    f"{BASE_URL}/v3/collections/{COLLECTION_NAME}/query",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4())
    },
    json={
        "query": "What are the revenue projections for Q4?",
        "limit": 10,
        "rerank": True
    }
)

result = response.json()
print("Query results:")
for r in result.get("results", []):
    print(f"  - {r['chunk_id']} of {r['document']['filename']} (score: {r['score']})")
    print(f"    {r['text'][:120]}...")
```

#### TypeScript

```typescript
import { randomUUID } from "crypto";

const response = await fetch(
  `${BASE_URL}/v3/collections/${COLLECTION_NAME}/query`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": randomUUID()
    },
    body: JSON.stringify({
      query: "What are the revenue projections for Q4?",
      limit: 10,
      rerank: true
    })
  }
);

const result = await response.json();
console.log("Query results:");
for (const r of result.results || []) {
  console.log(`  - ${r.chunk_id} of ${r.document.filename} (score: ${r.score})`);
  console.log(`    ${r.text.slice(0, 120)}...`);
}
```

> **Note**
>
> v2 Query (`POST /v2/collections/{name}/query`) remains available for existing clients. It uses `search_results` and `content` instead of `results` and `text`. See the [v2 Query reference](/reference/v2/query).

## Other Info

### Environment Scoping

API keys are scoped to environments:

* **Development** (`cap_dev_*`) - For testing and development
* **Staging** (`cap_stage_*`) - For pre-production testing
* **Production** (`cap_prod_*`) - For production use

Collections created with a development key can only be accessed with development keys from the same organization.

### Supported File Types

Captain indexes 30+ file types: documents, images, video, and audio. See
[Supported File Types](/guides/supported-file-types) for the full extension list and the
[File Size Limits](/guides/file-size-limits) page for the ceiling on each type.

## What Else Captain Does

* **MCP Server**: connect Captain to Claude, Cursor, or any MCP client without writing
  API calls by hand. See [MCP Server](/guides/mcp).
* **Sync**: keep a collection matched to a bucket automatically, with backfills,
  reconciliation, and webhooks on file changes. See [Set Up Sync](/guides/sync/set-up).
* **PII Masking**: redact names, IDs, and other sensitive text and faces before content
  is indexed. See [PII Masking](/guides/pii-masking).
* **Edge Routing**: queries connect at an edge location near the caller, cutting connection
  latency by 19-39%. See [Edge Routing](/guides/edge-routing).

## Getting Help

Need assistance? We're here to help!

* **Email:** [humans@captain.dev](mailto:humans@captain.dev)
* **Documentation:** [docs.captain.dev](https://docs.captain.dev)

## Send Feedback

Captain wants your feedback  -  both **while you build** and **while Captain runs live** inside
your application or agent. **No API key required.**

Send feedback when you:

* **Finish building** an integration  -  tell us what was smooth or rough.
* **Hit a solid, server-side Captain roadblock**  -  an endpoint that errored, behaved
  unexpectedly, or blocked you with no clean workaround. Include what you tried.
* **Want to shape the product**  -  suggest a feature, flag a missing capability, or weigh in on
  direction. Welcome anytime, in development or in production.
* Run into confusing or incorrect docs, or any friction worth flagging.

`POST https://api.filesearch.dev/feedback` with a plain-text body:

```bash
curl -X POST "https://api.filesearch.dev/feedback?agent=my-agent&source=quickstart" \
  -H "Content-Type: text/plain" \
  --data "Finished the S3 integration. Roadblock: /index/s3 returned 500 on a 50k-object bucket with no pagination guidance. Feature request: a dry-run to validate IAM before indexing."
```

Optional metadata via query params or `X-Feedback-*` headers: `agent` (your tool/agent name),
`client_version`, and `source` (the page or endpoint your feedback is about). The endpoint is
rate-limited per IP, so keep each submission focused on one clear issue. This is the fastest way
for AI agents and developers to flag friction while it's fresh.