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

# Contract Analysis

> Extract key terms, identify risks, and compare clauses across contracts

## What You'll Build

A contract analysis pipeline that:

1. Uploads contracts to a searchable vault
2. Extracts key terms (parties, dates, amounts, obligations)
3. Identifies risky clauses with severity ratings
4. Compares similar clauses across multiple contracts
5. Generates a formatted risk report

**Time to complete:** 25 minutes

## Architecture

```mermaid theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
flowchart LR
    A["Contract Upload"] --> B["OCR Processing"]
    B --> C["Vault Storage<br/>+ Indexing"]
    C --> D["Vault Search"]
    D --> E["LLM Analysis<br/>Key Terms + Risk Flags"]
    E --> F["PDF Report<br/>(Formatted)"]
```

## Prerequisites

* Case.dev API key ([get one here](https://console.case.dev))
* Contract documents (PDF, DOCX, or images)

***

## Step 1: Create a Contract Vault

Set up a vault to store and index your contracts:

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

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

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

  const vault = await client.vault.create({
    name: 'Contracts — Q1 2024'
  });

  console.log(`Vault ID: ${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='Contracts — Q1 2024')

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

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  vault, _ := client.Vault.New(ctx, casedev.VaultNewParams{
  	Name: casedev.F("Contracts — Q1 2024"),
  })
  fmt.Println(vault.ID)
  ```
</CodeGroup>

## Step 2: Upload and Index a Contract

Upload a contract, run OCR if needed, and index it for search:

<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"}}
  import fs from 'fs';

  async function uploadContract(
    vaultId: string,
    filePath: string,
    metadata?: Record<string, string>
  ) {
    const filename = filePath.split('/').pop()!;
    const contentType = filename.endsWith('.pdf') ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';

    // 1. Upload to vault
    const upload = await client.vault.upload(vaultId, {
      filename,
      contentType,
      metadata: {
        type: 'contract',
        ...metadata
      }
    });

    await fetch(upload.uploadUrl, {
      method: 'PUT',
      headers: { 'Content-Type': contentType },
      body: fs.readFileSync(filePath)
    });

    // 2. Ingest (OCR + embedding generation)
    await client.vault.ingest(vaultId, upload.objectId);

    // 3. Wait for indexing
    let obj = await client.vault.objects.retrieve(vaultId, upload.objectId);
    while (obj.ingestionStatus === 'processing') {
      await new Promise(r => setTimeout(r, 5000));
      obj = await client.vault.objects.retrieve(vaultId, upload.objectId);
    }

    console.log(`Uploaded and indexed: ${filename} (${obj.ingestionStatus})`);
    return { objectId: upload.objectId, filename };
  }
  ```

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

  def upload_contract(vault_id: str, file_path: str, metadata: dict = None):
      filename = os.path.basename(file_path)
      content_type = 'application/pdf' if filename.endswith('.pdf') else 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'

      # 1. Upload to vault
      upload = client.vault.upload(vault_id,
          filename=filename,
          content_type=content_type,
          metadata={
              'type': 'contract',
              **(metadata or {})
          }
      )

      with open(file_path, 'rb') as f:
          requests.put(upload.upload_url, data=f,
              headers={'Content-Type': content_type})

      # 2. Ingest (OCR + embedding generation)
      client.vault.ingest(upload.object_id, id=vault_id)

      # 3. Wait for indexing
      obj = client.vault.objects.retrieve(vault_id, upload.object_id)
      while obj.ingestion_status == 'processing':
          time.sleep(5)
          obj = client.vault.objects.retrieve(vault_id, upload.object_id)

      print(f'Uploaded and indexed: {filename} ({obj.ingestion_status})')
      return {'object_id': upload.object_id, 'filename': filename}
  ```

  ```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>

<Info>
  **Ingestion handles everything.** Vault ingestion automatically runs OCR on scanned PDFs, chunks the text, and generates embeddings. You don't need to call OCR separately.
</Info>

## Step 3: Extract Key Terms

Use vault search to retrieve the contract text and extract structured terms:

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # Search vault for key contract sections
  casedev vault search --id $VAULT_ID \
    --query "parties effective date termination payment obligations" \
    --method hybrid --limit 15

  # Then extract terms via LLM (pipe contract text as content)
  casedev llm:v1:chat create-completion \
    --model anthropic/claude-sonnet-4.5 \
    --message '{role: system, content: "You are a contract analyst. Extract key terms and return as JSON."}' \
    --message '{role: user, content: "<contract text from search results>"}' \
    --temperature 0
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  async function extractKeyTerms(vaultId: string, objectId: string) {
    // Search for key sections of this specific contract
    const results = await client.vault.search(vaultId, {
      query: 'parties effective date termination payment obligations liability governing law',
      method: 'hybrid',
      limit: 15
    });

    const contractText = results.chunks.map(c => c.text).join('\n\n');

    const extraction = await client.llm.v1.chat.createCompletion({
      model: 'anthropic/claude-sonnet-4.5',
      messages: [
        {
          role: 'system',
          content: `You are a contract analyst. Extract key terms and return as JSON:
  {
    "parties": [{"name": "...", "role": "..."}],
    "effective_date": "YYYY-MM-DD",
    "termination_date": "YYYY-MM-DD",
    "value": {"amount": 0, "currency": "USD"},
    "governing_law": "...",
    "key_obligations": ["..."],
    "termination_clauses": ["..."],
    "renewal": {"type": "auto|manual|none", "notice_period": "..."},
    "risk_flags": [{"clause": "...", "section": "...", "severity": "high|medium|low", "reason": "..."}]
  }`
        },
        { role: 'user', content: contractText }
      ],
      temperature: 0
    });

    return JSON.parse(extraction.choices[0].message.content);
  }
  ```

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

  def extract_key_terms(vault_id: str, object_id: str):
      # Search for key sections of this specific contract
      results = client.vault.search(vault_id,
          query='parties effective date termination payment obligations liability governing law',
          method='hybrid',
          top_k=15
      )

      contract_text = '\n\n'.join(c.text for c in results.chunks)

      extraction = client.llm.v1.chat.create_completion(
          model='anthropic/claude-sonnet-4.5',
          messages=[
              {
                  'role': 'system',
                  'content': '''You are a contract analyst. Extract key terms and return as JSON:
  {
    "parties": [{"name": "...", "role": "..."}],
    "effective_date": "YYYY-MM-DD",
    "termination_date": "YYYY-MM-DD",
    "value": {"amount": 0, "currency": "USD"},
    "governing_law": "...",
    "key_obligations": ["..."],
    "termination_clauses": ["..."],
    "renewal": {"type": "auto|manual|none", "notice_period": "..."},
    "risk_flags": [{"clause": "...", "section": "...", "severity": "high|medium|low", "reason": "..."}]
  }'''
              },
              {'role': 'user', 'content': contract_text}
          ],
          temperature=0
      )

      return json.loads(extraction.choices[0].message.content)
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // Search for key sections
  results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
  	Query:  casedev.F("parties effective date termination payment obligations liability governing law"),
  	Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
  	TopK:   casedev.F(int64(15)),
  })

  // Build contract text from chunks
  var contractText string
  for _, chunk := range results.Chunks {
  	contractText += chunk.Text + "\n\n"
  }

  // Extract key terms via LLM
  resp, _ := client.Llm.V1.Chat.NewCompletion(ctx, casedev.LlmV1ChatNewCompletionParams{
  	Model: casedev.F("anthropic/claude-sonnet-4.5"),
  	Messages: casedev.F([]casedev.LlmV1ChatNewCompletionParamsMessage{
  		{
  			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleSystem),
  			Content: casedev.F("You are a contract analyst. Extract key terms and return as JSON."),
  		},
  		{
  			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
  			Content: casedev.F(contractText),
  		},
  	}),
  	Temperature: casedev.F(float64(0)),
  })
  fmt.Println(resp.Choices[0].Message.Content)
  ```
</CodeGroup>

```json title="Example Output" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "parties": [
    {"name": "Acme Corp", "role": "Vendor"},
    {"name": "BigCo Inc", "role": "Client"}
  ],
  "effective_date": "2024-01-01",
  "termination_date": "2026-12-31",
  "value": {"amount": 500000, "currency": "USD"},
  "governing_law": "Delaware",
  "key_obligations": [
    "Vendor shall deliver software by Q2 2024",
    "Client shall provide access to systems within 30 days"
  ],
  "termination_clauses": [
    "Either party may terminate with 90 days notice",
    "Immediate termination for material breach"
  ],
  "renewal": {"type": "auto", "notice_period": "60 days"},
  "risk_flags": [
    {
      "clause": "Unlimited liability",
      "section": "Section 8.2",
      "severity": "high",
      "reason": "No cap on liability exposes vendor to unlimited damages"
    },
    {
      "clause": "Non-compete — 5 years",
      "section": "Section 12.1",
      "severity": "high",
      "reason": "Non-compete extends 5 years post-termination, unusually long"
    },
    {
      "clause": "Auto-renewal with price escalation",
      "section": "Section 3.4",
      "severity": "medium",
      "reason": "Annual 8% price increase on auto-renewal without cap"
    }
  ]
}
```

## Step 4: Compare Clauses Across Contracts

Search your vault to find and compare similar clauses across multiple contracts:

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # Search for liability clauses across contracts
  casedev vault search --id $VAULT_ID \
    --query "liability clause terms conditions" \
    --method hybrid --limit 20

  # Compare via LLM
  casedev llm:v1:chat create-completion \
    --model anthropic/claude-sonnet-4.5 \
    --message '{role: system, content: "You are a senior contract attorney. Compare liability clauses."}' \
    --message '{role: user, content: "Compare these liability clauses: <clause text from search>"}' \
    --temperature 0.3
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  async function compareClauses(vaultId: string, clauseType: string) {
    // Search for a specific clause type across all contracts
    const results = await client.vault.search(vaultId, {
      query: `${clauseType} clause terms conditions`,
      method: 'hybrid',
      limit: 20
    });

    const chunks = results.chunks.map(c => ({
      text: c.text,
      source: c.filename
    }));

    const comparison = await client.llm.v1.chat.createCompletion({
      model: 'anthropic/claude-sonnet-4.5',
      messages: [
        {
          role: 'system',
          content: `You are a senior contract attorney. Compare ${clauseType} clauses across multiple contracts.

  For each contract:
  - Summarize the clause terms
  - Note any unusual or favorable/unfavorable provisions
  - Rate the risk level (high/medium/low)

  Then provide:
  - A comparison table
  - A recommendation on which terms are most favorable
  - Suggested negotiation points for future contracts`
        },
        {
          role: 'user',
          content: `Compare these ${clauseType} clauses:\n\n${
            chunks.map(c => `### ${c.source}\n${c.text}`).join('\n\n')
          }`
        }
      ],
      temperature: 0.3
    });

    return comparison.choices[0].message.content;
  }

  // Example: Compare liability clauses across all contracts
  const liabilityComparison = await compareClauses(vault.id, 'liability');
  console.log(liabilityComparison);
  ```

  ```python title="Python" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  def compare_clauses(vault_id: str, clause_type: str):
      # Search for a specific clause type across all contracts
      results = client.vault.search(vault_id,
          query=f'{clause_type} clause terms conditions',
          method='hybrid',
          top_k=20
      )

      chunks = [{'text': c.text, 'source': c.filename} for c in results.chunks]

      comparison = client.llm.v1.chat.create_completion(
          model='anthropic/claude-sonnet-4.5',
          messages=[
              {
                  'role': 'system',
                  'content': f'''You are a senior contract attorney. Compare {clause_type} clauses across multiple contracts.

  For each contract:
  - Summarize the clause terms
  - Note any unusual or favorable/unfavorable provisions
  - Rate the risk level (high/medium/low)

  Then provide:
  - A comparison table
  - A recommendation on which terms are most favorable
  - Suggested negotiation points for future contracts'''
              },
              {
                  'role': 'user',
                  'content': f"Compare these {clause_type} clauses:\n\n" + '\n\n'.join(
                      f"### {c['source']}\n{c['text']}" for c in chunks
                  )
              }
          ],
          temperature=0.3
      )

      return comparison.choices[0].message.content

  # Example: Compare liability clauses across all contracts
  liability_comparison = compare_clauses(vault.id, 'liability')
  print(liability_comparison)
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // Search for a specific clause type across all contracts
  results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
  	Query:  casedev.F("liability clause terms conditions"),
  	Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
  	TopK:   casedev.F(int64(20)),
  })

  // Build context from chunks
  var clauseText string
  for _, chunk := range results.Chunks {
  	clauseText += fmt.Sprintf("### %s\n%s\n\n", chunk.Filename, chunk.Text)
  }

  // Compare clauses via LLM
  comparison, _ := client.Llm.V1.Chat.NewCompletion(ctx, casedev.LlmV1ChatNewCompletionParams{
  	Model: casedev.F("anthropic/claude-sonnet-4.5"),
  	Messages: casedev.F([]casedev.LlmV1ChatNewCompletionParamsMessage{
  		{
  			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleSystem),
  			Content: casedev.F("You are a senior contract attorney. Compare liability clauses across multiple contracts."),
  		},
  		{
  			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
  			Content: casedev.F("Compare these liability clauses:\n\n" + clauseText),
  		},
  	}),
  	Temperature: casedev.F(float64(0.3)),
  })
  fmt.Println(comparison.Choices[0].Message.Content)
  ```
</CodeGroup>

## Step 5: Generate a Risk Report

Combine all analysis into a formatted PDF report:

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # Generate a formatted PDF report from markdown
  casedev format:v1 document \
    --content "$(cat report.md)" \
    --input-format md \
    --output-format pdf \
    --header "CONFIDENTIAL — Contract Risk Report" \
    --footer "Page {{page}} of {{pages}}"
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  async function generateRiskReport(
    vaultId: string,
    contracts: Array<{ objectId: string; filename: string }>
  ) {
    let reportContent = `# Contract Risk Report\n\n**Generated:** ${new Date().toLocaleDateString()}\n**Contracts analyzed:** ${contracts.length}\n\n---\n\n`;

    // Analyze each contract
    for (const contract of contracts) {
      reportContent += `## ${contract.filename}\n\n`;

      const terms = await extractKeyTerms(vaultId, contract.objectId);

      reportContent += `**Parties:** ${terms.parties.map(p => `${p.name} (${p.role})`).join(', ')}\n`;
      reportContent += `**Effective:** ${terms.effective_date} — ${terms.termination_date}\n`;
      reportContent += `**Value:** ${terms.value.currency} ${terms.value.amount.toLocaleString()}\n`;
      reportContent += `**Governing Law:** ${terms.governing_law}\n\n`;

      if (terms.risk_flags.length > 0) {
        reportContent += `### Risk Flags\n\n`;
        reportContent += `| Severity | Clause | Section | Reason |\n|----------|--------|---------|--------|\n`;
        for (const flag of terms.risk_flags) {
          reportContent += `| **${flag.severity.toUpperCase()}** | ${flag.clause} | ${flag.section} | ${flag.reason} |\n`;
        }
        reportContent += '\n';
      }

      reportContent += '---\n\n';
    }

    // Cross-contract comparison
    reportContent += `## Cross-Contract Clause Comparison\n\n`;

    for (const clauseType of ['liability', 'termination', 'indemnification']) {
      reportContent += `### ${clauseType.charAt(0).toUpperCase() + clauseType.slice(1)} Clauses\n\n`;
      const comparison = await compareClauses(vaultId, clauseType);
      reportContent += comparison + '\n\n';
    }

    // Convert to PDF
    const report = await client.format.v1.document({
      content: reportContent,
      input_format: 'md',
      output_format: 'pdf',
      options: {
        header: 'CONFIDENTIAL — Contract Risk Report',
        footer: 'Page {{page}} of {{pages}}',
        styles: {
          h1: { font: 'Times New Roman', size: 16, bold: true, alignment: 'center' },
          h2: { font: 'Times New Roman', size: 13, bold: true },
          h3: { font: 'Times New Roman', size: 11, bold: true },
          p: { font: 'Times New Roman', size: 10, spacingAfter: 6 }
        }
      }
    });

    return report;
  }
  ```

  ```python title="Python" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  from datetime import date

  def generate_risk_report(vault_id: str, contracts: list[dict]):
      report_content = f'# Contract Risk Report\n\n**Generated:** {date.today().isoformat()}\n**Contracts analyzed:** {len(contracts)}\n\n---\n\n'

      # Analyze each contract
      for contract in contracts:
          report_content += f"## {contract['filename']}\n\n"

          terms = extract_key_terms(vault_id, contract['object_id'])

          parties = ', '.join(f"{p['name']} ({p['role']})" for p in terms['parties'])
          report_content += f"**Parties:** {parties}\n"
          report_content += f"**Effective:** {terms['effective_date']} — {terms['termination_date']}\n"
          report_content += f"**Value:** {terms['value']['currency']} {terms['value']['amount']:,}\n"
          report_content += f"**Governing Law:** {terms['governing_law']}\n\n"

          if terms['risk_flags']:
              report_content += '### Risk Flags\n\n'
              report_content += '| Severity | Clause | Section | Reason |\n|----------|--------|---------|--------|\n'
              for flag in terms['risk_flags']:
                  report_content += f"| **{flag['severity'].upper()}** | {flag['clause']} | {flag['section']} | {flag['reason']} |\n"
              report_content += '\n'

          report_content += '---\n\n'

      # Cross-contract comparison
      report_content += '## Cross-Contract Clause Comparison\n\n'

      for clause_type in ['liability', 'termination', 'indemnification']:
          report_content += f'### {clause_type.capitalize()} Clauses\n\n'
          comparison = compare_clauses(vault_id, clause_type)
          report_content += comparison + '\n\n'

      # Convert to PDF
      report = client.format.v1.create_document(
          content=report_content,
          input_format='md',
          output_format='pdf',
          options={
              'header': 'CONFIDENTIAL — Contract Risk Report',
              'footer': 'Page {{page}} of {{pages}}',
              'styles': {
                  'h1': {'font': 'Times New Roman', 'size': 16, 'bold': True, 'alignment': 'center'},
                  'h2': {'font': 'Times New Roman', 'size': 13, 'bold': True},
                  'h3': {'font': 'Times New Roman', 'size': 11, 'bold': True},
                  'p': {'font': 'Times New Roman', 'size': 10, 'spacingAfter': 6}
              }
          }
      )

      return report
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  // Build report content from analysis (see extractKeyTerms + compareClauses above)
  reportContent := fmt.Sprintf("# Contract Risk Report\n\n**Generated:** %s\n\n---\n\n", time.Now().Format("2006-01-02"))

  // ... add per-contract analysis and cross-contract comparisons ...

  // Convert to PDF
  report, _ := client.Format.V1.NewDocument(ctx, casedev.FormatV1NewDocumentParams{
  	Content:      casedev.F(reportContent),
  	InputFormat:  casedev.F("md"),
  	OutputFormat: casedev.F("pdf"),
  	Options: casedev.F(casedev.FormatV1NewDocumentParamsOptions{
  		Header: casedev.F("CONFIDENTIAL — Contract Risk Report"),
  		Footer: casedev.F("Page {{page}} of {{pages}}"),
  	}),
  })
  // report is *http.Response with PDF body
  ```
</CodeGroup>

***

## Production Tips

### Error Handling

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  # CLI displays errors to stderr with status codes
  casedev vault search --id $VAULT_ID --query "key terms"
  # Error: 404 Not Found — Contract not found in vault
  # Error: 429 Too Many Requests — retry after a delay
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  try {
    const terms = await extractKeyTerms(vaultId, objectId);
  } catch (error) {
    if (error.status === 404) {
      console.error('Contract not found in vault');
    } else if (error.status === 429) {
      console.error('Rate limited — retry after a delay');
    } else {
      console.error('Extraction failed:', error.message);
    }
  }
  ```

  ```python title="Python" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  try:
      terms = extract_key_terms(vault_id, object_id)
  except casedev.NotFoundError:
      print('Contract not found in vault')
  except casedev.RateLimitError:
      print('Rate limited — retry after a delay')
  except casedev.APIError as e:
      print(f'Extraction failed: {e.message}')
  ```

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  terms, err := extractKeyTerms(ctx, vaultID, objectID)
  if err != nil {
  	var apiErr *casedev.Error
  	if errors.As(err, &apiErr) {
  		switch apiErr.StatusCode {
  		case 404:
  			fmt.Println("Contract not found in vault")
  		case 429:
  			fmt.Println("Rate limited — retry after a delay")
  		default:
  			fmt.Printf("Extraction failed: %s\n", apiErr.Message)
  		}
  	}
  }
  ```
</CodeGroup>

### Use Webhooks for Production Pipelines

Instead of polling for ingestion status, subscribe to vault events:

<CodeGroup>
  ```bash title="CLI" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  casedev vault:events:subscriptions create \
    --id $VAULT_ID \
    --callback-url "https://your-app.com/webhooks/vault" \
    --event-type object.ingested \
    --event-type object.failed
  ```

  ```typescript title="Typescript" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  await client.vault.events.subscriptions.create(vaultId, {
    url: 'https://your-app.com/webhooks/vault',
    events: ['object.ingested', 'object.failed']
  });
  ```

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

  ```go title="Go" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  client.Vault.Events.Subscriptions.New(ctx, vaultID, casedev.VaultEventSubscriptionNewParams{
      CallbackURL: casedev.F("https://your-app.com/webhooks/vault"),
      EventTypes:  casedev.F([]string{"object.ingested", "object.failed"}),
  })
  ```
</CodeGroup>

<Tip>
  Use `temperature: 0` for key term extraction and risk identification. The lower temperature ensures more deterministic, factual outputs.
</Tip>

***

## Next Steps

* [Vault Search Reference](/vault/search) — Advanced search methods and filtering
* [Chat Completions](/llms/chat-completions) — Streaming, structured output, and more
* [Format Service](/format/index) — PDF and DOCX generation
* [Discovery Pipeline](/cookbooks/discovery-pipeline) — Batch document processing
