Architecture
Prerequisites
- Case.dev API key
- Node.js 18+ or Python 3.9+
- Documents to process (PDFs, images, Word docs)
Step 1: Create a vault
casedev vault create --name "Matter 2024-1234 - Discovery"
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}`);
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}')
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)
Step 2: Subscribe to ingestion events
Use webhooks to get notified when documents finish processing instead of polling.casedev vault ingest --id $VAULT_ID --object-id $OBJECT_ID
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}`);
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}')
result, _ := client.Vault.Ingest(ctx, objectID, casedev.VaultIngestParams{
ID: casedev.F(vaultID),
})
fmt.Println(result.Status)
Webhook payload
{
"id": "evt_abc123",
"eventType": "vault.ingest.completed",
"vaultId": "vault_abc123",
"objectId": "obj_xyz789",
"data": { "status": "completed" }
}
Webhook delivery is at-least-once. Use the
id field as an idempotency key and design handlers to safely process duplicates. See Vault Webhooks for signing and retry details.Step 3: Batch upload documents
casedev vault upload \
--id $VAULT_ID \
--filename "document.pdf" \
--content-type "application/pdf"
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;
}
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
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)
Step 4: Trigger ingestion
Ingestion runs OCR (if needed) and generates embeddings for search. Your webhook will fire when each document finishes.casedev vault ingest --id $VAULT_ID --object-id $OBJECT_ID
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
}
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
result, _ := client.Vault.Ingest(ctx, objectID, casedev.VaultIngestParams{
ID: casedev.F(vaultID),
})
fmt.Println(result.Status)
Step 5: Search your documents
Once your webhook confirms ingestion is complete, documents are searchable.casedev vault search \
--id $VAULT_ID \
--query "search query"
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;
}
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
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)
}
Complete example
casedev vault create --name "Matter 2024-1234 - Discovery"
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();
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()
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)
Production tip: For large document sets (1000+), use parallel uploads with a concurrency limit of 10-20 to maximize throughput while avoiding rate limits.

