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

# Discovery Pipeline

> Ingest documents, extract text via OCR, generate embeddings, and enable semantic search

## Architecture

```mermaid theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
flowchart LR
    A["Document Upload"] --> B["OCR Processing"]
    B --> C["Embeddings Generation"]
    C --> D["Search Index"]
```

## Prerequisites

* Case.dev API key
* Node.js 18+ or Python 3.9+
* Documents to process (PDFs, images, Word docs)

## Step 1: Create a vault

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  casedev vault create --name "Matter 2024-1234 - Discovery"
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import Casedev from 'casedev';
  import fs from 'fs';
  import path from 'path';

  const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });

  const vault = await client.vault.create({
    name: 'Matter 2024-1234 - Discovery',
    description: 'Documents received from opposing counsel'
  });

  console.log(`Created vault: ${vault.id}`);
  ```

  ```python title="Python" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import os
  import casedev

  client = casedev.Casedev(api_key=os.environ['CASEDEV_API_KEY'])

  vault = client.vault.create(
      name='Matter 2024-1234 - Discovery',
      description='Documents received from opposing counsel'
  )

  print(f'Created vault: {vault.id}')
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  vault, _ := client.Vault.New(ctx, casedev.VaultNewParams{
  	Name: casedev.F("Matter 2024-1234 - Discovery"),
  	Description: casedev.F("Documents received from opposing counsel"),
  })
  fmt.Println(vault.ID)
  ```
</CodeGroup>

## Step 2: Subscribe to ingestion events

Use webhooks to get notified when documents finish processing instead of polling.

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  casedev vault ingest --id $VAULT_ID --object-id $OBJECT_ID
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  const subscription = await client.vault.events.subscriptions.create(vault.id, {
    callbackUrl: 'https://your-app.com/webhooks/case-vault',
    eventTypes: ['vault.ingest.completed', 'vault.ingest.failed']
  });

  console.log(`Webhook subscription: ${subscription.id}`);
  ```

  ```python title="Python" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  subscription = client.vault.events.subscriptions.create(vault.id,
      callback_url='https://your-app.com/webhooks/case-vault',
      event_types=['vault.ingest.completed', 'vault.ingest.failed']
  )

  print(f'Webhook subscription: {subscription.id}')
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  result, _ := client.Vault.Ingest(ctx, objectID, casedev.VaultIngestParams{
  	ID: casedev.F(vaultID),
  })
  fmt.Println(result.Status)
  ```
</CodeGroup>

Your webhook endpoint receives events like:

```json title="Webhook payload" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "id": "evt_abc123",
  "eventType": "vault.ingest.completed",
  "vaultId": "vault_abc123",
  "objectId": "obj_xyz789",
  "data": { "status": "completed" }
}
```

<Info>
  Webhook delivery is **at-least-once**. Use the `id` field as an idempotency key and design handlers to safely process duplicates. See [Vault Webhooks](/vault/webhooks) for signing and retry details.
</Info>

## Step 3: Batch upload documents

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  casedev vault upload \
    --id $VAULT_ID \
    --filename "document.pdf" \
    --content-type "application/pdf"
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  async function uploadDocuments(vaultId: string, documentsDir: string) {
    const files = fs.readdirSync(documentsDir);
    const results = [];
    
    const contentTypes: Record<string, string> = {
      '.pdf': 'application/pdf',
      '.doc': 'application/msword',
      '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      '.jpg': 'image/jpeg',
      '.jpeg': 'image/jpeg',
      '.png': 'image/png',
      '.tiff': 'image/tiff',
      '.txt': 'text/plain',
    };
    
    for (const file of files) {
      const filePath = path.join(documentsDir, file);
      const stat = fs.statSync(filePath);
      
      if (!stat.isFile()) continue;
      
      const ext = path.extname(file).toLowerCase();
      const contentType = contentTypes[ext] || 'application/octet-stream';
      
      // Get presigned upload URL
      const upload = await client.vault.upload(vaultId, {
        filename: file,
        contentType,
        metadata: {
          source: 'discovery',
          original_path: filePath,
        }
      });
      
      // Upload file to S3
      const fileBuffer = fs.readFileSync(filePath);
      await fetch(upload.uploadUrl, {
        method: 'PUT',
        headers: { 'Content-Type': contentType },
        body: fileBuffer
      });
      
      console.log(`Uploaded: ${file}`);
      results.push({ file, objectId: upload.objectId });
    }
    
    return results;
  }
  ```

  ```python title="Python" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import requests
  from pathlib import Path

  def upload_documents(vault_id: str, documents_dir: str):
      files = list(Path(documents_dir).iterdir())
      results = []
      
      content_types = {
          '.pdf': 'application/pdf',
          '.doc': 'application/msword',
          '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
          '.jpg': 'image/jpeg',
          '.jpeg': 'image/jpeg',
          '.png': 'image/png',
          '.tiff': 'image/tiff',
          '.txt': 'text/plain',
      }
      
      for file_path in files:
          if not file_path.is_file():
              continue
          
          content_type = content_types.get(file_path.suffix.lower(), 'application/octet-stream')
          
          # Get presigned upload URL
          upload = client.vault.upload(vault_id,
              filename=file_path.name,
              content_type=content_type,
              metadata={
                  'source': 'discovery',
                  'original_path': str(file_path),
              }
          )
          
          # Upload file to S3
          with open(file_path, 'rb') as f:
              requests.put(upload.upload_url, data=f,
                  headers={'Content-Type': content_type})
          
          print(f'Uploaded: {file_path.name}')
          results.append({'file': file_path.name, 'object_id': upload.object_id})
      
      return results
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  upload, _ := client.Vault.Upload(ctx, vaultID, casedev.VaultUploadParams{
  	Filename:    casedev.F("document.pdf"),
  	ContentType: casedev.F("application/pdf"),
  })
  // PUT file to upload.UploadURL via net/http
  fmt.Println(upload.ObjectID)
  ```
</CodeGroup>

## Step 4: Trigger ingestion

Ingestion runs OCR (if needed) and generates embeddings for search. Your webhook will fire when each document finishes.

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  casedev vault ingest --id $VAULT_ID --object-id $OBJECT_ID
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  async function ingestDocuments(vaultId: string, uploads: { file: string; objectId: string }[]) {
    for (const { file, objectId } of uploads) {
      await client.vault.ingest(vaultId, objectId);
      console.log(`Ingesting: ${file}`);
    }
    
    // Ingestion runs async — your webhook endpoint receives
    // vault.ingest.completed or vault.ingest.failed for each document
  }
  ```

  ```python title="Python" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  def ingest_documents(vault_id: str, uploads: list):
      for upload in uploads:
          client.vault.ingest(upload['object_id'], id=vault_id)
          print(f'Ingesting: {upload["file"]}')
      
      # Ingestion runs async — your webhook endpoint receives
      # vault.ingest.completed or vault.ingest.failed for each document
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  result, _ := client.Vault.Ingest(ctx, objectID, casedev.VaultIngestParams{
  	ID: casedev.F(vaultID),
  })
  fmt.Println(result.Status)
  ```
</CodeGroup>

## Step 5: Search your documents

Once your webhook confirms ingestion is complete, documents are searchable.

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  casedev vault search \
    --id $VAULT_ID \
    --query "search query"
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  async function searchDiscovery(vaultId: string, query: string) {
    const results = await client.vault.search(vaultId, {
      query,
      method: 'hybrid',  // Combines semantic + keyword
      limit: 10
    });
    
    console.log(`\nResults for: "${query}"\n`);
    
    for (const chunk of results.chunks) {
      console.log(`${chunk.filename} (page ${chunk.page})`);
      console.log(`   Score: ${chunk.hybridScore.toFixed(2)}`);
      console.log(`   "${chunk.text.substring(0, 200)}..."\n`);
    }
    
    return results;
  }
  ```

  ```python title="Python" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  def search_discovery(vault_id: str, query: str):
      results = client.vault.search(vault_id,
          query=query,
          method='hybrid',  # Combines semantic + keyword
          top_k=10
      )
      
      print(f'\nResults for: "{query}"\n')
      
      for chunk in results.chunks:
          print(f'{chunk.filename} (page {chunk.page})')
          print(f'   Score: {chunk.hybrid_score:.2f}')
          print(f'   "{chunk.text[:200]}..."\n')
      
      return results
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
  	Query: casedev.F("search query"),
  	Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
  })
  for _, chunk := range results.Chunks {
  	fmt.Println(chunk.Text)
  }
  ```
</CodeGroup>

## Complete example

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  casedev vault create --name "Matter 2024-1234 - Discovery"
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import Casedev from 'casedev';
  import fs from 'fs';
  import path from 'path';

  const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });

  async function main() {
    const documentsDir = './discovery_dump';
    
    // 1. Create vault
    const vault = await client.vault.create({
      name: 'Matter 2024-1234 - Discovery',
      description: 'Documents from opposing counsel'
    });
    
    // 2. Subscribe to ingestion events
    await client.vault.events.subscriptions.create(vault.id, {
      callbackUrl: 'https://your-app.com/webhooks/case-vault',
      eventTypes: ['vault.ingest.completed', 'vault.ingest.failed']
    });
    
    // 3. Upload and ingest all documents
    const files = fs.readdirSync(documentsDir);
    for (const file of files) {
      const filePath = path.join(documentsDir, file);
      if (!fs.statSync(filePath).isFile()) continue;
      
      const upload = await client.vault.upload(vault.id, {
        filename: file,
        contentType: 'application/pdf'
      });
      
      await fetch(upload.uploadUrl, {
        method: 'PUT',
        body: fs.readFileSync(filePath)
      });
      
      await client.vault.ingest(vault.id, upload.objectId);
      console.log(`Queued: ${file}`);
    }
    
    // 4. Search (after webhook confirms ingestion is complete)
    const results = await client.vault.search(vault.id, {
      query: 'evidence of safety violations in 2023',
      method: 'hybrid',
      limit: 10
    });
    
    console.log(results.chunks);
  }

  main();
  ```

  ```python title="Python" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  import os
  import casedev
  import requests
  from pathlib import Path

  client = casedev.Casedev(api_key=os.environ['CASEDEV_API_KEY'])

  def main():
      documents_dir = './discovery_dump'
      
      # 1. Create vault
      vault = client.vault.create(
          name='Matter 2024-1234 - Discovery',
          description='Documents from opposing counsel'
      )
      
      # 2. Subscribe to ingestion events
      client.vault.events.subscriptions.create(vault.id,
          callback_url='https://your-app.com/webhooks/case-vault',
          event_types=['vault.ingest.completed', 'vault.ingest.failed']
      )
      
      # 3. Upload and ingest all documents
      for file_path in Path(documents_dir).iterdir():
          if not file_path.is_file():
              continue
          
          upload = client.vault.upload(vault.id,
              filename=file_path.name,
              content_type='application/pdf'
          )
          
          with open(file_path, 'rb') as f:
              requests.put(upload.upload_url, data=f)
          
          client.vault.ingest(upload.object_id, id=vault.id)
          print(f'Queued: {file_path.name}')
      
      # 4. Search (after webhook confirms ingestion is complete)
      results = client.vault.search(vault.id,
          query='evidence of safety violations in 2023',
          method='hybrid',
          top_k=10
      )
      
      print(results.chunks)

  if __name__ == '__main__':
      main()
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  vault, _ := client.Vault.New(ctx, casedev.VaultNewParams{
  	Name: casedev.F("Matter 2024-1234 - Discovery"),
  	Description: casedev.F("Documents from opposing counsel"),
  })
  fmt.Println(vault.ID)
  ```
</CodeGroup>

<Info>
  **Production tip:** For large document sets (1000+), use parallel uploads with a concurrency limit of 10-20 to maximize throughput while avoiding rate limits.
</Info>
