API Reference
Dewey is a document backend for AI applications. Upload your documents; Dewey handles conversion, chunking, embedding, and retrieval. Your application can focus on what to do with the answers, not on building the infrastructure to find them.
What you can build
The API is organized around four core capabilities:
| Capability | Description |
|---|---|
| Ingestion | Upload PDFs, Word docs, PowerPoint decks, HTML, plain text, and more. Dewey converts, chunks, and embeds each document automatically. You get a status lifecycle and real-time upload events via SSE. |
| Retrieval | Hybrid keyword + semantic search with cross-encoder reranking. Returns ranked chunks with section and document provenance. |
| Research | An agentic deep-research endpoint that runs a multi-step tool-call loop over your corpus and streams a cited, markdown-formatted answer over SSE. |
| Claims | Extract discrete, importance-scored facts from every document in a collection. Claims are linked to their source section and browsable, filterable, and searchable in the dashboard. |
| Contradictions | Compare extracted claims across the entire corpus, cluster conflicting statements by severity, and generate suggested resolution instructions. Apply a resolution in one click to keep future research consistent. |
| Duplicates | Identify near-duplicate documents by measuring how much content they share. Promote one member of each cluster to canonical and exclude the rest from retrieval and contradiction detection — useful when the same document arrives in multiple versions or from multiple sources. |
| MCP Server | A Model Context Protocol server exposing your collections as tools. Plug Dewey directly into Claude, Cursor, or any MCP-compatible agent. |
Design philosophy
Provenance over summaries. Every answer comes back with the exact document, section, and chunk it was drawn from. Hallucinations are hard to ship when citations are mandatory.
Structure is preserved, not flattened. Dewey understands the heading hierarchy of your documents. Query at chunk, section, or document granularity, whichever your application needs.
Depth is a dial, not a binary. Retrieval, research at four depth levels, and a full agentic loop all share the same API shape. You choose how much compute to spend; the interface stays consistent.
Bring your own model. Research endpoints support OpenAI, Anthropic, and Google Gemini models via BYOK. Dewey manages the tool loop and context assembly; you decide what intelligence runs it.
Base URL
https://api.meetdewey.com/v1Client libraries
Official SDKs wrap the REST API and handle authentication, retries, and SSE streaming for you.
Data model
Every document produces three queryable layers that are first-class API primitives:
| Layer | Description |
|---|---|
Markdown | Clean normalized text of the document, available once processing completes. |
Sections | The heading hierarchy: title, level, position, extractive summary, and character offsets into Markdown. |
Chunks | Fixed-size overlapping text segments within each section, each embedded as a vector. |
Quickstart#
Create a collection, upload a document, and run your first query. Pick your preferred language below.
# 1. Create a collection curl -X POST https://api.meetdewey.com/v1/collections \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "My docs", "projectId": "YOUR_PROJECT_ID"}' # 2. Upload a document curl -X POST https://api.meetdewey.com/v1/collections/YOUR_COLLECTION_ID/documents \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "file=@report.pdf" # 3. Query curl -X POST https://api.meetdewey.com/v1/collections/YOUR_COLLECTION_ID/query \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"q": "What are the key findings?"}'
Step-by-step walkthrough
The same five steps in detail, with full error handling:
1. Create a collection
A collection is an isolated corpus. All documents and retrieval are scoped to it.
import { DeweyClient } from '@meetdewey/typescript-sdk'
const client = new DeweyClient({ apiKey: process.env.DEWEY_API_KEY! })
const collection = await client.collections.create({
name: 'My Docs',
projectId: process.env.DEWEY_PROJECT_ID!,
})
console.log(collection.id) // 3f7a1b2c-...2. Upload a document
const doc = await client.documents.upload(
collection.id,
new Blob([require('fs').readFileSync('report.pdf')], { type: 'application/pdf' }),
)
console.log(doc.status) // "uploading"3. Wait for ready
Processing is async. Poll the document or subscribe to SSE events (see Real-time Events).
let ready = false
while (!ready) {
await new Promise(r => setTimeout(r, 2000))
const d = await client.documents.get(collection.id, doc.id)
if (d.status === 'ready') ready = true
if (d.status === 'error') throw new Error(d.errorMessage ?? 'Processing failed')
}4. Query
const results = await client.retrieval.query(
collection.id,
'What are the key findings?',
{ limit: 5 },
)
for (const r of results) {
console.log(r.chunk.content, '-', r.document.filename)
}5. Research (optional)
For grounded, cited answers from an agentic loop:
for await (const event of client.research.stream(
collection.id,
'Summarize the key findings and their implications',
{ depth: 'balanced' },
)) {
if (event.type === 'chunk') process.stdout.write(event.content)
if (event.type === 'done') console.log('\nSources:', event.sources)
}Authentication#
Dewey has two auth planes. Most operations use a project API key. A small number of management operations (creating webhooks, managing org membership) require a JWT issued at sign-in.
API keys: data plane
Two keys are provisioned per project: a live key and a test key. Use them for all collection, document, retrieval, research, and provider-key endpoints. Pass the key in the Authorization header:
Authorization: Bearer dwy_live_... # production
Authorization: Bearer dwy_test_... # sandbox - same pipeline, isolated dataimport { DeweyClient } from '@meetdewey/typescript-sdk'
const client = new DeweyClient({ apiKey: 'dwy_live_...' })JWTs: management plane
Webhook endpoints and org/project management routes require a short-lived JWT. Obtain one by calling the login endpoint:
// POST /auth/login
{ "email": "you@example.com", "password": "..." }
// Response
{ "token": "eyJ...", "user": { "id": "...", "email": "..." } }Then pass the token as a Bearer header alongside requests:
Authorization: Bearer eyJ...POST /auth/login programmatically and cache the token.Org and project IDs
Management-plane routes include :orgId and :projectId in their paths. These are UUIDs visible in the dashboard under Settings → General, or returned by GET /orgs and GET /orgs/:orgId/projects. One account can belong to multiple orgs; each org can have multiple projects; each project has its own API keys and collections.
Security
All data is encrypted at rest. PostgreSQL, object storage, and Redis are encrypted at the infrastructure level. Sensitive credentials — project API keys, provider API keys, and webhook signing secrets — are additionally encrypted at the application layer using AES-256-GCM before being written to the database.
Public collections
Collections with visibility: "public" allow unauthenticated reads on /query, /v1/collections/:id/sections/scan, and document reads.
Collections#
A collection is an isolated corpus of documents with its own chunking and embedding settings. All retrieval operations are scoped to a collection.
The Collection object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique collection identifier. |
name | string | Display name of the collection. |
visibility | "private" | "public" | Access control. private requires an API key; public allows unauthenticated reads on /v1/collections/:id/query, /v1/collections/:id/sections/scan, and document reads. |
description | string | nulloptional | Human-readable description. |
chunkSize | number | Token size for each chunk when splitting documents. Range: 64–4096. |
chunkOverlap | number | Overlapping token count between adjacent chunks. Range: 0–512. |
embeddingModel | string | Embedding model used to vectorize chunks. |
enableSummarization | boolean | When true (default), Dewey generates AI summaries for sections with generic titles to improve search quality. Disable during bulk ingestion to skip summarization and speed up processing. |
enableCaptioning | boolean | When true (default), Dewey generates AI captions for images and tables after ingestion. Requires a BYOK provider key; documents are ingested normally without one. |
enableReranking | boolean | When true (default), search results are re-scored with a cross-encoder for higher relevance. Disable for lower query latency at the cost of some result quality. |
enableDeduplication | boolean | When true, Dewey detects near-duplicate documents and excludes non-canonical members from retrieval. Off by default. See the Duplicates section. |
llmModel | string | nulloptional | Default LLM model for this collection's post-processing (section summarization, image captioning, table captioning). Overrides the system default. Accepts any model ID from the supported models list. Set to null to use the system default. |
instructions | string | nulloptional | Natural-language guidance injected into the system prompt for every research session on this collection — for example, noting units, preferred sources, or how to handle missing information. Has no effect on basic search queries. Max 4000 characters. |
createdAt | string (ISO 8601) | Timestamp when the collection was created. |
/v1/collectionsCreate a collection.
// Request
{
"projectId": "proj_...", // required - from Dashboard → Project Settings
"name": "Engineering Docs",
"visibility": "private", // optional - "private" | "public", default "private"
"description": "...", // optional
"chunkSize": 512, // optional - 64-4096, default 512
"chunkOverlap": 64, // optional - 0-512, default 64
"embeddingModel": "text-embedding-3-small" // optional, fixed for the lifetime of the collection
}const collection = await client.collections.create({
projectId: 'proj_...',
name: 'Engineering Docs',
visibility: 'private',
chunkSize: 512,
chunkOverlap: 64,
})
// => { id: '3f7a1b2c-...', name: 'Engineering Docs', ... }/v1/collectionsList all collections for the project.
const collections = await client.collections.list()
// => [{ id: '3f7a1b2c-...', name: 'Engineering Docs', ... }, ...]/v1/collections/:idFetch a single collection.
const collection = await client.collections.get('3f7a1b2c-...')
// => { id: '3f7a1b2c-...', name: 'Engineering Docs', ... }/v1/collections/:id/statsStorage and document counts.
The CollectionStats object
| Field | Type | Description |
|---|---|---|
docCount | number | Total number of documents in the collection. |
totalFileSizeBytes | number | Combined size of all uploaded files in bytes. |
storageBytes | number | Object storage consumed by raw files and rendered Markdown. |
dbBytes | number | Database storage consumed by chunks and embedding vectors. |
totalSections | number | Total section count across all documents in the collection. |
totalChunks | number | Total chunk count across all documents in the collection. |
statusCounts | Record<string, number> | Document count per processing status, e.g. { "ready": 42, "error": 1 }. Only statuses with at least one document are included. |
summarizedCount | number | Number of ready documents that have at least one AI-generated section summary. |
captionedCount | number | Number of ready documents that have at least one AI-generated caption chunk (image or table). |
claimsExtractedCount | number | Number of ready documents from which factual claims have been extracted. |
totalClaimsCount | number | Total number of extracted claims in the collection. |
/v1/collections/:idUpdate collection metadata.
// Request - all fields optional
{
"name": "Renamed Docs",
"visibility": "public",
"description": "...",
"chunkSize": 1024, // 64–4096, affects new documents only
"chunkOverlap": 128, // 0–512, affects new documents only
"enableSummarization": false, // set to false during bulk ingestion to skip summarization
"enableCaptioning": false, // set to false to disable AI image and table captioning
"enableReranking": false, // set to false to skip cross-encoder reranking on queries (lower latency)
"enableDeduplication": true, // set to true to detect near-duplicate documents (off by default)
"llmModel": "gpt-4o", // optional — override default model for summarization and captioning
"instructions": "All figures are in USD unless stated otherwise." // optional — injected into research system prompt; set to null to clear
}const updated = await client.collections.update('3f7a1b2c-...', {
name: 'Renamed Docs',
visibility: 'public',
})
// => { id: '3f7a1b2c-...', name: 'Renamed Docs', visibility: 'public', ... }/v1/collections/:idSoft-delete a collection. Returns 204 No Content.
await client.collections.delete('3f7a1b2c-...')
// => 204 No ContentRecompute
When you change a collection's llmModel, existing summaries and captions were generated with the previous model. Use these endpoints to regenerate them with the new model.
/v1/collections/:id/recompute/summariesReset all AI-generated section summaries and re-enqueue summarization jobs for every ready document. Returns { enqueued: number }.
/v1/collections/:id/recompute/captionsDelete all caption chunks (image and table), reset image captions, and re-enqueue captioning jobs for every ready document. Returns { enqueued: number }.
/v1/collections/:id/recompute/claimsDelete all extracted claims and re-enqueue claim extraction jobs for every ready document. Also clears the cached claim map. Returns 204 No Content.
// Response 200 OK (summaries / captions)
{ "enqueued": 42 }
// Response 204 No Content (claims)await client.collections.recomputeSummaries('3f7a1b2c-...')
await client.collections.recomputeCaptions('3f7a1b2c-...')
await client.collections.recomputeClaims('3f7a1b2c-...')Documents#
Documents are ingested through a pipeline: convert → section → chunk → embed → ready. Status updates are emitted in real time (see Events).
Status lifecycle
pending → uploading → processing → sectioned → embedded → ready
↓
error (retryable)| Status | Description |
|---|---|
pending | Document has been created but the file has not yet been received. |
uploading | File has been received and is being saved to storage. |
processing | File is saved. Dewey is converting it to Markdown and extracting the heading structure. |
sectioned | Section manifest is ready and queryable. Chunking and embedding are in progress. |
embedded | All chunks have been embedded. Final indexing is in progress. |
ready | Fully indexed. All retrieval and research endpoints can use this document. |
error | Processing failed. Check errorMessage for details. Retryable via the retry endpoint. |
The Document object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique document identifier. |
collectionId | string (UUID) | ID of the collection this document belongs to. |
filename | string | Original filename as uploaded. |
status | string | Current processing status. One of: pending, uploading, processing, sectioned, embedded, ready, error. |
fileSizeBytes | number | null | File size in bytes. |
sectionCount | number | null | Number of sections detected. Populated after the document reaches "sectioned". |
chunkCount | number | null | Number of chunks created. Populated after the document reaches "embedded". |
contentHash | string | null | SHA-256 hash of the uploaded file. Used for deduplication: if you supply the same hash on upload-url, Dewey skips re-storing the file. |
markdownStorageKey | string | null | Internal storage key for the rendered Markdown. Use GET /v1/documents/:id/markdown to fetch the content. |
errorMessage | string | null | Error details when status is "error". null otherwise. |
tags | string[] | User-supplied tags for filtering. Always lowercase and deduplicated. Empty array by default. |
metadata | object | Arbitrary key-value metadata supplied at upload time or via PATCH. Empty object by default. Supports nested values. |
createdAt | string (ISO 8601) | Timestamp when the document was created. |
Upload flow (recommended)
For large files, get a presigned URL and upload directly from the client, then confirm.
/v1/collections/:id/documents/upload-urlRequest a presigned upload URL.
// Request
{
"filename": "q4-report.pdf",
"contentType": "application/pdf",
"fileSizeBytes": 2097152,
"contentHash": "sha256:abc123...", // optional - enables deduplication
"tags": ["annual", "finance"], // optional
"metadata": { "region": "us" } // optional
}const { documentId, uploadUrl } = await client.documents.requestUploadUrl(
'3f7a1b2c-...',
{ filename: 'q4-report.pdf', contentType: 'application/pdf' }
)
// PUT the file to uploadUrl, then confirm:
await fetch(uploadUrl, { method: 'PUT', body: fileBlob })
await client.documents.confirmUpload('3f7a1b2c-...', documentId)The UploadUrlResponse object
| Field | Type | Description |
|---|---|---|
documentId | string (UUID) | Document ID to pass to the /confirm endpoint after the upload. |
uploadUrl | string | null | Presigned URL for a direct PUT upload, valid for 15 minutes. null when the content hash matched an existing file (deduplication hit). |
document | Document | undefinedoptional | Present only on a deduplication hit (uploadUrl is null). The existing Document object; no upload or confirm step is needed. |
contentHash matches an already-stored file, the response status is 200 (not 201) and document is populated. Skip the PUT and confirm steps: the document is already ready./v1/collections/:id/documents/:docId/confirmConfirm upload complete and start ingestion. Call after PUT to the presigned URL. Optionally set or override tags and metadata at confirm time.
// Request body (all fields optional)
{
"tags": ["annual", "finance"],
"metadata": { "region": "us" }
}Returns the document object (200 OK). Status transitions to "uploading".
Direct upload
/v1/collections/:id/documentsMultipart upload. Streams the file to storage and starts ingestion immediately.
413 Payload Too Large. For large files use the presigned URL flow above instead.curl https://api.meetdewey.com/v1/collections/:id/documents \
-H "Authorization: Bearer dwy_live_..." \
-F "file=@report.pdf" \
-F 'tags=["annual","finance"]' \ # optional JSON-encoded array
-F 'metadata={"region":"us"}' # optional JSON-encoded object
// Response 202 Accepted
{ ...document }import { readFileSync } from 'node:fs'
const file = new Blob([readFileSync('report.pdf')], { type: 'application/pdf' })
const doc = await client.documents.upload('3f7a1b2c-...', file, {
tags: ['annual', 'finance'],
metadata: { region: 'us' },
})
// => { id: '9a2c4e6f-...', status: 'uploading', tags: ['annual', 'finance'], ... }/v1/collections/:id/documents/batchUpload multiple files in one multipart request.
/v1/collections/:id/documentsList documents in a collection. Supports pagination via ?limit (default 100, max 500), ?offset, and optional ?status filter. Returns { documents, total }.
const { documents, total } = await client.documents.list('3f7a1b2c-...')
// paginate: client.documents.list('3f7a1b2c-...', { limit: 50, offset: 50 })
// filter: client.documents.list('3f7a1b2c-...', { status: 'error' })/v1/documents/:idFetch document metadata.
const doc = await client.documents.get('3f7a1b2c-...', '9a2c4e6f-...')
// => { id: '9a2c4e6f-...', filename: 'report.pdf', status: 'ready', ... }/v1/documents/:id/waitLong-poll until the document reaches a terminal state (ready or error), then return the document object. Times out after 5 minutes with 408. Useful for connector platforms (Power Automate, n8n) that need a single blocking action instead of a polling loop.
// Response 200 OK — document finished processing
{ "id": "9a2c4e6f-...", "status": "ready", "sectionCount": 12, ... }
// Response 200 OK — document failed
{ "id": "9a2c4e6f-...", "status": "error", "errorMessage": "...", ... }
// Response 408 Request Timeout — did not finish within 5 minutes
{ "error": "Document did not finish processing within 5 minutes" }/v1/documents/:id/markdownFetch the full rendered Markdown. Returns 404 until processing completes.
const markdown = await client.documents.getMarkdown('3f7a1b2c-...', '9a2c4e6f-...')
// => "# Q4 Report
## Executive Summary
..."/v1/documents/:id/sectionsFetch the section manifest: an ordered list of sections with titles, levels, and positions.
const sections = await client.sections.list('3f7a1b2c-...', '9a2c4e6f-...')
// => [{ id: 'b1c2d3e4-...', title: 'Executive Summary', level: 2, ... }]The Section object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique section identifier. |
documentId | string (UUID) | ID of the parent document. |
title | string | Section heading text. |
level | number | Heading level (1 = h1, 2 = h2, etc.). |
position | number | Zero-based index of this section within the document. |
summary | string | null | Section summary used by the /sections/scan full-text index. Extractive by default; sections with generic or short titles (e.g. "Introduction", "Chapter 3") receive an AI-generated summary automatically after the document reaches ready. |
summaryType | "extractive" | "generated" | null | How the summary was produced. "extractive" means it was pulled verbatim from the section text; "generated" means an AI model wrote it. null until the summary is available. |
chunkCount | number | Number of chunks created from this section. |
markdownOffsetStart | number | Character offset into the document Markdown where this section begins. |
markdownOffsetEnd | number | Character offset where this section ends. |
/v1/documents/:idDelete a document and its stored files. Returns 204 No Content.
await client.documents.delete('3f7a1b2c-...', '9a2c4e6f-...')
// => 204 No Content/v1/documents/:id/retryRetry a document stuck in error status.
const doc = await client.documents.retry('3f7a1b2c-...', '9a2c4e6f-...')
// => { id: '9a2c4e6f-...', status: 'processing', ... }/v1/collections/:id/documents/retry-failedRetry all documents in a collection that are currently in error status. Returns an array of the documents that were re-queued.
// Response 200 OK - array of re-queued documents (may be empty)
[{ "id": "9a2c4e6f-...", "status": "uploading", ... }, ...]/v1/collections/:id/documents/batch-confirmConfirm upload complete for multiple documents at once and start ingestion. Used after batch presigned-URL uploads.
// Request
{ "documentIds": ["9a2c4e6f-...", "b3d4e5f6-..."] } // 1–500 IDs
// Response 200 OK - array of updated document objects
[{ "id": "9a2c4e6f-...", "status": "uploading", ... }, ...]/v1/collections/:id/documents/batchDelete multiple documents and their stored files in one request. Returns 204 No Content.
// Request
{ "ids": ["9a2c4e6f-...", "b3d4e5f6-..."] } // at least 1 IDManage tab (dashboard UI)
Every collection page in the dashboard has a Manage tab — a document table that ties the endpoints below into a single auditing-and-editing surface. Use it to triage, tag, and edit large corpora without writing code; the same operations are available programmatically if you'd rather script them. The Manage and Overview tabs share the same search bar and filters, so switching between them preserves your context.
Capabilities
- Omnisearch — one bar for filename, tags, and metadata. The tag dropdown injects
tag:<name>tokens into the query. Filename matching is fuzzy via POST /documents/search. - Bulk operations — add or remove tags and edit metadata across every selected document at once. Up to 500 documents per request via PATCH /documents/bulk.
- Sticky selection across pagination — paginate without losing your selection. Navigate to any page, refine the filter, multi-select, then act on the union.
- Single-doc edit panel — click any row to open a side panel for editing that document's tags and metadata. Backed by PATCH /documents/:docId.
- Tag chips — every row shows its tags as colored chips so you can scan a list visually before drilling in.
Search bar DSL
The search bar accepts a small DSL that compiles into the same query parameters you'd send to POST /documents/search. Tokens AND together; the free-text portion fuzzy-matches filenames. Wrap values that contain spaces in double quotes.
| Token | Effect |
|---|---|
tag:<name> | Restrict to documents with this tag. Multiple tag: tokens AND together. |
meta.<key>:<value> | Restrict by metadata key/value (e.g., meta.region:us). Multiple meta. tokens AND together. |
(free text) | Fuzzy match against filenames using trigram similarity. Joined and sent as the q parameter. |
tag:annual meta.region:us 2024-q1Keyboard shortcuts
| Key | Action |
|---|---|
| ⌘A / Ctrl-A | Select every document on the current page. |
| Esc | Clear the current selection. |
| Click row | Open the single-doc edit panel. |
Update tags and metadata
/v1/collections/:id/documents/:docIdUpdate a document's tags and/or metadata. Metadata is shallow-merged with existing values by default.
// Request
{
"tags": ["q1", "finance"], // replaces existing tags entirely
"metadata": { "region": "emea" }, // merged into existing metadata by default
"replaceMetadata": true // optional — replace instead of merge
}
// Response 200 OK
{ ...document }const doc = await client.documents.update(
'3f7a1b2c-...',
'9a2c4e6f-...',
{ tags: ['q1', 'finance'], metadata: { region: 'emea' } },
)Bulk update tags & metadata
/v1/collections/:id/documents/bulkUpdate tags and/or metadata on up to 500 documents in a single request. Metadata is shallow-merged by default.
// Request
{
"documents": [
{
"id": "9a2c4e6f-...",
"tags": ["q1", "finance"],
"metadata": { "region": "emea" }
},
{
"id": "b3d4e5f6-...",
"tags": ["hr"],
"replaceMetadata": true, // optional — replace instead of merge
"metadata": { "year": 2024 }
}
]
}
// Response 200 OK
[ ...documents ]const docs = await client.documents.bulkUpdate(
'3f7a1b2c-...',
[
{ id: '9a2c4e6f-...', tags: ['q1', 'finance'], metadata: { region: 'emea' } },
{ id: 'b3d4e5f6-...', tags: ['hr'], replaceMetadata: true, metadata: { year: 2024 } },
],
)List tags
/v1/collections/:id/tagsReturn all distinct tags used across documents in a collection, with document counts. Useful for building tag-filter UIs.
// Response 200 OK
{
"tags": [
{ "name": "annual", "count": 12 },
{ "name": "finance", "count": 8 }
]
}const { tags } = await client.documents.listTags('3f7a1b2c-...')
// tags[0].name, tags[0].countSearch by filename
/v1/collections/:id/documents/searchFuzzy filename search using trigram similarity. Returns documents whose filenames match the query, ordered by similarity score.
// Request
{
"q": "annual-report", // required — search string
"limit": 20, // optional, 1–100, default 20
"tags": ["finance"], // optional — docs must have ALL these tags
"anyTags": ["internal", "external"], // optional — docs must have ANY of these tags
"metadata": { "region": "us" } // optional — docs must contain all these key-value pairs
}// Response 200 OK
[
{
"score": 0.82,
"document": {
"id": "9a2c4e6f-...",
"filename": "annual-report-2024.pdf",
"status": "ready",
...
}
}
]const results = await client.documents.searchDocuments(
'3f7a1b2c-...',
'annual-report',
{ limit: 10, tags: ['finance'] },
)
// results[0].score, results[0].document.filenameImages#
Images and figures extracted from documents are stored with stable, auth-gated URLs and embedded in the document Markdown as standard  references. Access requires the same collection-level auth as all other document routes.
The DocumentImage object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique image identifier. |
sectionId | string (UUID) | null | ID of the section the image appears in. |
placeholder | string | Internal placeholder used during ingestion (e.g. dewey://img-0). |
mimeType | string | MIME type of the image (e.g. image/png). |
caption | string | null | AI-generated caption. null until the caption job runs (BYOK required). |
position | number | Sequential order of the image within the document. |
/v1/documents/:id/imagesList all images extracted from a document, ordered by position.
// Response 200 OK
[
{
"id": "5a9f1892-...",
"sectionId": "e3d7c1a0-...",
"placeholder": "dewey://img-0",
"mimeType": "image/png",
"caption": "A bar chart showing accuracy vs. model size.",
"position": 0
}
]/v1/documents/:id/images/:imageIdStream raw image bytes with the correct Content-Type. Responses are permanently cached (Cache-Control: public, max-age=31536000, immutable). Pass your API key as a Bearer token for private collections.
Table Captions#
After a document is ingested, Dewey runs an async job that generates a one-to-two sentence AI caption for each table chunk, describing its subject, key columns, and notable values. Captions are stored alongside the chunk and rendered as italicised figcaptions beneath tables in the document viewer.
GET /v1/documents/:id/chunks/captions
Returns all chunks that have a caption, ordered by section position then chunk position within the document. Use this to build a content-keyed lookup for rendering captions in your own viewer.
/v1/documents/:id/chunks/captionsList all table chunks that have an AI-generated caption, ordered by document position.
// Response 200 OK
[
{
"content": "| Quarter | Revenue |\n|---------|---------|\n| Q1 | $2.4M |",
"caption": "Quarterly revenue table showing Q1 results of $2.4M."
}
]Retrieval#
Two retrieval primitives: hybrid chunk search for direct content retrieval, and section scan for lightweight exploration of large corpora without loading chunk content.
/v1/collections/:id/queryHybrid semantic + BM25 search over chunk content. Candidates are ranked by Reciprocal Rank Fusion, then re-scored by a cross-encoder reranker. Returns chunks with full citation lineage.
// Request
{
"q": "what are the side effects of the treatment?",
"limit": 10, // optional, 1–50, default 10
"tags": ["annual", "finance"], // optional — docs must have ALL these tags
"anyTags": ["internal", "external"], // optional — docs must have ANY of these tags
"metadata": { "region": "us" } // optional — docs must contain all these key-value pairs
}// Response 200 OK
[
{
"score": 0.97,
"chunk": {
"id": "c1d2e3f4-...",
"content": "The most common side effects reported were...",
"position": 3,
"tokenCount": 128
},
"section": {
"id": "b1c2d3e4-...",
"title": "Adverse Events",
"level": 2
},
"document": {
"id": "9a2c4e6f-...",
"filename": "clinical-trial-2024.pdf"
}
}
]const results = await client.retrieval.query(
'3f7a1b2c-...',
'what are the side effects of the treatment?',
{ limit: 10, tags: ['annual'], metadata: { region: 'us' } },
)
// results[0].score, results[0].chunk.content, results[0].document.filenamebge-reranker-base) re-scores all candidates and returns the top limit results. Reranking runs in-process with no external API call and is on by default./v1/collections/:id/sections/scanFull-text search over section titles and summaries. Returns ranked sections without chunk content. Use this to identify which sections to fetch before loading chunks.
// Request
{
"query": "adverse events and safety profile",
"top_k": 20, // optional, 1–100, default 20
"tags": ["annual", "finance"], // optional — docs must have ALL these tags
"anyTags": ["internal", "external"], // optional — docs must have ANY of these tags
"metadata": { "region": "us" } // optional — docs must contain all these key-value pairs
}// Response 200 OK
{
"results": [
{
"score": 0.91,
"section": {
"id": "b1c2d3e4-...",
"title": "Adverse Events",
"level": 2,
"summary": "This section describes reported adverse events..."
},
"document": {
"id": "9a2c4e6f-...",
"filename": "clinical-trial-2024.pdf"
}
}
]
}const { results } = await client.sections.scan(
'3f7a1b2c-...',
'adverse events and safety profile',
{ topK: 20, tags: ['annual'], metadata: { region: 'us' } },
)
// results[0].score, results[0].section.title, results[0].document.filenameSection content and chunks
/v1/sections/:idFetch a section with its full Markdown content sliced from the document.
// Response 200 OK
{
...section,
"content": "## Adverse Events
The most common side effects..."
}/v1/sections/:id/chunksFetch all chunks for a section (embedding vectors excluded).
const chunks = await client.sections.getChunks('b1c2d3e4-...')
// => [{ id: 'c1d2e3f4-...', content: '...', position: 0, tokenCount: 128 }, ...]Research#
The research endpoint runs a multi-step agentic loop, searching, scanning, and reading sections, then produces a grounded, cited answer. Two variants are available: a streaming endpoint that returns Server-Sent Events, and a buffered /sync endpoint that returns a single JSON response when the answer is complete.
/v1/collections/:id/researchRun an agentic research query. Returns a Server-Sent Events stream.
// Request
{
"q": "What role does silent adult-to-child transmission play in major polio outbreaks?",
"depth": "exhaustive", // optional - "quick" | "balanced" | "deep" | "exhaustive", default "balanced"
"model": "gpt-5.4", // optional - defaults to "gpt-5.4" for deep/exhaustive, "gpt-4o-mini" otherwise
"tags": ["annual", "finance"], // optional — restrict search to docs with ALL these tags
"anyTags": ["internal", "public"], // optional — restrict search to docs with ANY of these tags
"metadata": { "region": "us" } // optional — restrict search to docs matching this metadata
}
// Headers required for SSE
Accept: text/event-streamfor await (const event of client.research.stream(
'3f7a1b2c-...',
'What role does silent adult-to-child transmission play in major polio outbreaks?',
{ depth: 'balanced', tags: ['annual'], metadata: { region: 'us' } },
)) {
if (event.type === 'chunk') process.stdout.write(event.content)
if (event.type === 'done') console.log(event.sources)
}/v1/collections/:id/research/syncRun an agentic research query and return the complete answer as a single JSON response. Useful for environments that cannot consume Server-Sent Events, such as Power Automate or serverless functions with response-buffering requirements.
// Request — same body as the streaming endpoint
{
"q": "What role does silent adult-to-child transmission play in major polio outbreaks?",
"depth": "balanced", // optional
"tags": ["annual"], // optional
"anyTags": ["internal"], // optional
"metadata": { "region": "us" } // optional
}
// Response
{
"answer": "...", // full markdown answer with inline citations
"sessionId": "uuid",
"sources": [ /* ResearchSource objects */ ]
}Depth levels
| Depth | Max iterations | Credits | Default model | Tools | Best for |
|---|---|---|---|---|---|
quick | 5 | 0.5 | gpt-4o-mini | Hybrid search | Fast factual lookups |
balanced | 10 | 1 | gpt-4o-mini | Hybrid search | General questions (default) |
deep | 20 | 3 | gpt-5.4 | Hybrid search · section scan · section read | Multi-document synthesis |
exhaustive | 50 | 8 | gpt-5.4 | Hybrid search · section scan · section read | Comprehensive research across large corpora |
Supported models
OpenAI, Anthropic, and Google Gemini models are supported. The model field is routed to the appropriate provider based on the model ID prefix (gpt- / o → OpenAI, claude- → Anthropic, gemini- → Google Gemini). Common choices:
| Model | Provider | Notes |
|---|---|---|
gpt-5.4 | OpenAI | Default for deep and exhaustive. Highest quality. |
gpt-4o-mini | OpenAI | Default for quick and balanced. Fast and cost-effective. |
o3 | OpenAI | Reasoning model. Good for complex multi-hop questions. |
claude-opus-4-7 | Anthropic | Most capable Claude model. |
claude-sonnet-4-6 | Anthropic | Fast and capable. Good balance of quality and speed. |
gemini-2.5-pro | Google Gemini | Most capable Gemini model. |
gemini-2.5-flash | Google Gemini | Fast and cost-effective. |
gemini-2.5-flash-lite | Google Gemini | Lightest and fastest Gemini model. |
SSE events
Each event is a data: {...}\n\n line. Parse with JSON.parse(event.data).
| type | Fields | Description |
|---|---|---|
tool_call | query, tool? | Model is calling a tool. tool is undefined for search, "scan_sections", or "get_section_chunks". |
chunk | content | Streamed answer token from the final generation pass. |
done | sessionId, sources[] | Research complete. Sources include document and section for each citation. |
error | message | An error occurred. The stream will close after this event. |
// Example SSE stream
data: {"type":"tool_call","query":"silent transmission polio"}
data: {"type":"tool_call","query":"Blake2014-PNAS.pdf › Results","tool":"get_section_chunks"}
data: {"type":"chunk","content":"In Congo 2010"}
data: {"type":"chunk","content":", adults had an estimated R of 1.85"}
data: {"type":"done","sessionId":"e4f5...","sources":[
{"chunkId":"...","filename":"Blake2014-PNAS.pdf","sectionTitle":"Results"},
{"chunkId":"...","filename":"PatriarcaPA1997.pdf","sectionTitle":"Outbreak Analysis"}
]}The DoneEvent object
| Field | Type | Description |
|---|---|---|
sessionId | string (UUID) | Identifier for this research session. Use to retrieve or delete the session via the history endpoints. |
sources | array | Citations used in the answer. Each item identifies the chunk, document, and section the model drew from. |
The ResearchSource object
| Field | Type | Description |
|---|---|---|
chunkId | string (UUID) | ID of the cited chunk. Use GET /v1/sections/:sectionId/chunks to fetch all chunks for the parent section. |
content | string | Text of the cited chunk. |
documentId | string (UUID) | ID of the source document. |
filename | string | Filename of the source document. |
sectionId | string (UUID) | ID of the section the chunk belongs to. Use GET /v1/sections/:id to fetch the full section content. |
sectionTitle | string | Title of the section. |
sectionLevel | number | Heading level of the section (1 = h1, 2 = h2, etc.). |
Research history
/v1/collections/:id/researchList the last 50 research sessions for a collection.
The ResearchSession object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique session identifier. |
query | string | Original research question. |
response | string | Final answer in Markdown. |
depth | string | Depth level used for this session. |
model | string | Model used for the agentic loop. |
sources | ResearchSource[] | Citations. Each item is a ResearchSource object with chunkId, content, documentId, filename, sectionId, sectionTitle, and sectionLevel. |
createdAt | string (ISO 8601) | Timestamp when the session was created. |
sources field in a stored ResearchSession is a frozen snapshot captured when the session ran. Documents added, updated, or removed from the collection after that moment do not alter the session record — the same session ID always returns the same answer and sources./v1/collections/:id/research/:sessionIdRetrieve a single research session by ID. Returns the ResearchSession object.
/v1/collections/:id/research/:sessionIdDelete a research session. Returns 204 No Content.
Hosted Agents#
Hosted agents are saved configurations that wrap the same agentic loop as /research, but with a fixed identity, prompt, model, and allow-list of collections. You define the agent once via the dashboard or the CRUD API, then invoke it by slug. Each invocation runs the executor against the agent's configured tools and collections, streams events, persists a run record, and enforces credit and concurrency limits.
Agents vs. /research
Both endpoints run the same executor — multi-step tool use, hybrid search, section scanning, and grounded answers with citations. The difference is how the run's configuration gets specified.
| /research | Hosted Agents | |
|---|---|---|
| Configuration | Per-call: query + depth + (optional) tags/metadata | Saved per agent: prompt, model, toolset, allow-list, timeout |
| Caller | Application code that constructs the query inline | Application code that references an agent by slug |
| Scope | One collection per call | A pre-approved set of collections per agent |
| System prompt | Built-in research persona | Author-supplied; the agent is whatever you make it |
| Persistence | ResearchSession — answer, sources, depth | AgentRun + AgentRunStep — full execution trace, replayable |
| Best for | Ad-hoc questions where the caller decides everything | Repeatable workflows where ops controls the prompt and tools |
/research, you probably want a hosted agent. If your app is short-lived and just needs an answer once, stick with /research.Agent
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Stable UUID. Use this in API calls when you have it; the slug is also acceptable. |
slug | string | URL-safe identifier matching ^[a-z0-9](?:[a-z0-9-]{0,98}[a-z0-9])?$. Locked after first save — PATCH requires confirmSlugChange: true. |
name | string | Human-readable name for the dashboard. |
description | string | nulloptional | Optional one-line description. |
systemPrompt | string | Authoring prompt. Dewey appends a footer at invocation time with allow-list collection IDs, available tool names, and an explicit "untrusted content" notice for tool results — so you do not need to add those yourself. |
model | AgentModel | See "Supported models" below. |
timeoutMs | number | Wall-clock cap per invocation. Capped at 600_000 (10 min) in v1. Default 300_000. |
toolset | AgentToolset | Boolean flags for the five tools: search_collection, scan_sections, get_section_chunks, list_documents, get_document. Disable any tool by setting it to false. |
allowedCollectionIds | string[] | null | Restrict the agent to specific collections, or set null to allow all collections in the project. The allow-list is re-resolved against the live agents table on every tool call — updates take effect mid-run. |
createdBy | string (UUID) | User ID of the author. |
createdAt | string (ISO 8601) | |
updatedAt | string (ISO 8601) |
Create an agent
/v1/orgs/:orgId/projects/:projectId/agentsCreate a new agent. Authentication: JWT (admin role on the project). Returns 201 with the Agent object.
// Request
{
"slug": "compliance-helper",
"name": "Compliance Helper",
"description": "Answers questions about our compliance docs.",
"systemPrompt": "You are a compliance specialist. Cite specific clauses.",
"model": "claude-sonnet-4-6",
"timeoutMs": 300000, // optional
"toolset": { // optional - all true by default
"search_collection": true,
"scan_sections": true,
"get_section_chunks": true,
"list_documents": false,
"get_document": false
},
"allowedCollectionIds": ["3f7a1b2c-..."] // null = all collections
}
// 409 if slug collides; 400 if allowedCollectionIds is an empty array.List, read, update, delete
/v1/orgs/:orgId/projects/:projectId/agentsList agents. Returns { agents, hasMore }. Supports limit and offset query params.
/v1/orgs/:orgId/projects/:projectId/agents/:agentIdRead a single agent.
/v1/orgs/:orgId/projects/:projectId/agents/:agentIdUpdate agent fields. Uses JSON Merge Patch semantics: omitted keys are unchanged, null clears nullable fields, allowedCollectionIds: null widens to all collections. Slug changes require confirmSlugChange: true.
/v1/orgs/:orgId/projects/:projectId/agents/:agentIdHard-delete the agent. Cascades to AgentRun and AgentRunStep records via FK. Returns 204.
Invoke (streaming)
/v1/orgs/:orgId/projects/:projectId/agents/:agentSlug/invokeRun an agent against a query. Returns a Server-Sent Events stream. Authentication: JWT (project member) or API key (org-scoped).
// Request
{
"query": "What does our policy say about data residency?"
}
// Headers required for SSE
Accept: text/event-streamfor await (const event of client.agents.stream(
orgId,
projectId,
'compliance-helper',
{ query: 'What does our policy say about data residency?' },
)) {
if (event.type === 'chunk') process.stdout.write(event.content)
if (event.type === 'done') console.log(event.runId)
}402 when BYOK is required and missing, 408 when wall-clock timeout fired before any chunk, 422 when the project's monthly credit quota is exhausted, 429 when the org's tier concurrency cap is reached. Once the stream is open, the same conditions arrive as error events.Invoke (buffered)
/v1/orgs/:orgId/projects/:projectId/agents/:agentSlug/invoke/syncRun an agent and return the complete answer as a single JSON response. Useful for environments that cannot consume SSE — Power Automate, Zapier, response-buffering serverless functions.
// Request — same body as /invoke
{
"query": "What does our policy say about data residency?"
}
// Response
{
"runId": "uuid",
"response": "...", // full markdown with inline citations
"status": "succeeded", // or "failed" | "cancelled" | "timeout"
"sources": [ /* AgentRunSource objects */ ],
"warnings": [] // populated for timeout / context-budget cases
}const result = await client.agents.invokeSync(
orgId,
projectId,
'compliance-helper',
{ query: 'What does our policy say about data residency?' },
)
console.log(result.response)
for (const s of result.sources) {
console.log(`- ${s.filename} § ${s.sectionTitle}`)
}Preview (no persistence)
/v1/orgs/:orgId/projects/:projectId/agents/:agentSlug/previewRun a draft agent definition without saving. Same SSE shape as /invoke but accepts an unsaved draft in the request body and skips persistence — no AgentRun row is written. Counts against credits and concurrency.
// Request
{
"query": "Draft a release-notes blurb",
"draft": {
"systemPrompt": "You write concise release notes.",
"model": "claude-sonnet-4-6",
"timeoutMs": 60000,
"toolset": { "search_collection": true, "scan_sections": false, ... },
"allowedCollectionIds": ["3f7a1b2c-..."]
}
}SSE events
Both /invoke and /preview emit the same event shapes. /invoke/sync waits for done internally and returns its payload as the response body.
| Field | Type | Description |
|---|---|---|
run_started | { runId: string } | First event. runId is the AgentRun.id you can later look up via the runs endpoints. /preview emits a synthetic preview-* runId for shape compatibility. |
tool_call | { tool, collectionId, args, stepIndex } | The model is calling a tool. Fires before each tool execution. |
tool_result | { tool, summary, stepIndex } | The tool returned. summary is a short human-readable description for UIs; the full content is fed back to the model and not echoed in events. |
chunk | { content: string } | Streaming text fragment of the assistant response. Concatenate to reconstruct the answer. |
warning | { message: string } | Non-fatal — the run continues but partial output may be returned. Emitted on context-budget overflow and on wall-clock timeout (in which case the run terminates after the warning with status=timeout). |
error | { message: string, code?: string } | Terminal — the run failed. Emitted before done with status=failed. |
done | { runId, status, response, iterationsUsed } | Terminal — always the last event. status is succeeded | failed | cancelled | timeout. |
Run history
/v1/orgs/:orgId/projects/:projectId/agents/:agentSlug/runsList the agent's runs in reverse chronological order. Returns { runs, hasMore }. Supports limit, offset, and status filter. The agentSnapshot and steps fields are excluded from the list response for performance — fetch a single run to read them.
/v1/orgs/:orgId/projects/:projectId/agents/:agentSlug/runs/:runIdRead a single AgentRun including the agentSnapshot taken at invocation time. Useful for replay and debugging.
/v1/orgs/:orgId/projects/:projectId/agents/:agentSlug/runs/:runId/stepsPaginated trace of the run as AgentRunStep records (one row per tool_call, tool_result, or message). Ordered by stepIndex. Useful for rendering an execution timeline.
/v1/orgs/:orgId/projects/:projectId/agents/:agentSlug/runs/:runIdDelete a run. Cascades to its steps via FK. Returns 204.
Supported models
Same provider matrix as /research — OpenAI, Anthropic, and Google Gemini, routed by model-ID prefix. Hosted agents always require a project provider key (BYOK) regardless of model — the Dewey-managed key is never used for agent invocations because each agent's model and prompt belong to the project that owns it.
Claims Beta#
When claim extraction is enabled on a collection, Dewey automatically extracts factual claims from each document as it is processed. Claims are scored by importance (1 = low, 5 = critical). The claim map endpoint uses UMAP to project all claims into a 2-D space for visualization.
The Claim object
| Field | Type | Description |
|---|---|---|
id | string | Unique claim ID. |
text | string | The extracted factual claim. |
sourceText | string | null | The verbatim source passage the claim was extracted from. |
sectionId | string | ID of the section this claim belongs to. |
sectionTitle | string | Title of the source section. |
documentId | string | ID of the source document. |
importance | number | Importance score from 1 (low) to 5 (critical). |
position | number | Ordinal position within the document. |
/v1/documents/:id/claimsList claims extracted from a specific document. Fast JSON response.
Query parameters: minImportance (1–5, default 1).
// Response 200 OK
{
"documentId": "9a2c4e6f-...",
"claims": [
{
"id": "c1b2a3d4-...",
"text": "Revenue grew 42% year-over-year in Q3.",
"sourceText": "Revenue grew 42% year-over-year in Q3, driven by...",
"sectionTitle": "Financial Results",
"sectionId": "s1a2b3c4-...",
"documentId": "9a2c4e6f-...",
"importance": 4,
"position": 7
}
]
}const { claims } = await client.claims.listByDocument('9a2c4e6f-...', {
minImportance: 3,
})/v1/collections/:id/claims/mapStream all claims in a collection with their UMAP 2-D coordinates. Responds with text/event-stream.
This endpoint streams SSE events. Set the Accept: text/event-stream header. Progress events are emitted as claims are embedded and projected; the final done event contains the full claim list with x/y coordinates.
SSE events
| type | Payload fields | Description |
|---|---|---|
| progress | pct: number | Processing progress 0–100. |
| done | total: number, claims: ClaimMapItem[] | All claims with UMAP coordinates. Stream ends after this event. |
| error | message: string | An error occurred. |
The ClaimMapItem object
| Field | Type | Description |
|---|---|---|
id | string | Claim ID. |
text | string | The factual claim. |
sourceText | string | null | Verbatim source passage. |
documentId | string | Source document ID. |
documentName | string | Source document filename. |
sectionId | string | Source section ID. |
sectionTitle | string | Source section title. |
importance | number | Importance score 1–5. |
x | number | UMAP x coordinate. |
y | number | UMAP y coordinate. |
for await (const event of client.claims.mapStream('3f7a1b2c-...')) {
if (event.type === 'progress') console.log(`${event.pct}%`)
if (event.type === 'done') console.log(`${event.total} claims`, event.claims)
}Contradictions Beta#
Contradiction detection analyzes all extracted claims in a collection for conflicts, groups them into clusters, and generates a resolution suggestion for each cluster. Detection runs are asynchronous — trigger a run and poll its status, then fetch results once complete.
The Contradiction object
| Field | Type | Description |
|---|---|---|
id | string | Unique contradiction ID. |
severity | "low" | "medium" | "high" | Assessed severity of the conflict. |
status | "active" | "dismissed" | "applied" | Resolution status. active = unresolved. |
explanation | string | Natural-language explanation of the conflict. |
clusterTopicSummary | string | null | Short topic label for the cluster of conflicting claims. |
suggestedInstruction | string | null | Suggested resolution instruction to append to collection instructions. |
claims | ContradictionClaimRef[] | The conflicting claims with document provenance. |
createdAt | string | ISO 8601 timestamp. |
/v1/collections/:id/contradictionsList contradictions in a collection. Returns { total, items }.
Query parameters: status (active | dismissed | applied, default active), severity (low | medium | high), limit (1–100, default 20).
const { total, items } = await client.contradictions.list('3f7a1b2c-...', {
status: 'active',
severity: 'high',
})/v1/collections/:id/contradictions/detectTrigger an async contradiction detection run. Returns the run object.
// Response 200 OK
{
"runId": "r1a2b3c4-...",
"status": "enqueued",
"enqueuedAt": "2024-09-01T12:00:00Z"
}const run = await client.contradictions.detect('3f7a1b2c-...')
console.log(run.runId, run.status)The ContradictionRun object
| Field | Type | Description |
|---|---|---|
id | string | Run ID. |
status | string | enqueued | running | completed | failed. |
claimsProcessed | number | null | Claims analysed so far. |
clustersAnalyzed | number | null | Clusters examined. |
contradictionsFound | number | null | Contradictions detected. |
model | string | null | LLM model used for detection. |
startedAt | string | null | ISO 8601 start timestamp. |
completedAt | string | null | ISO 8601 completion timestamp. |
error | string | null | Error message if status is failed. |
createdAt | string | ISO 8601 creation timestamp. |
/v1/collections/:id/contradictions/runs/latestGet the status and stats of the most recent contradiction detection run.
const run = await client.contradictions.getLatestRun('3f7a1b2c-...')
console.log(run.status, run.contradictionsFound)/v1/collections/:id/contradictions/:contradictionIdUpdate a contradiction. Currently used to dismiss it by setting status to 'dismissed'. Returns the updated Contradiction.
// Request
{ "status": "dismissed" }const updated = await client.contradictions.dismiss('3f7a1b2c-...', 'c1b2a3d4-...')/v1/collections/:id/contradictions/:contradictionId/apply-instructionApply a resolution instruction to a contradiction. The instruction is appended to the collection's research instructions. Returns 204 No Content.
Pass a custom instruction in the request body to override the suggested instruction. If omitted, the contradiction's suggestedInstruction is used.
// Request (optional)
{ "instruction": "Use the 2024 annual report as the authoritative source." }
// Response 204 No Content// Use suggested instruction
await client.contradictions.applyInstruction('3f7a1b2c-...', 'c1b2a3d4-...')
// Override with custom instruction
await client.contradictions.applyInstruction(
'3f7a1b2c-...', 'c1b2a3d4-...',
'Use the 2024 annual report as the authoritative source.',
)Duplicates Beta#
Fuzzy deduplication identifies near-duplicate documents within a collection by measuring how much content they share. Each cluster has one canonical member; the rest are marked near_duplicate and excluded from retrieval and contradiction detection. Detection is off by default — enable it per-collection with enableDeduplication: true.
The DuplicateGroup object
| Field | Type | Description |
|---|---|---|
id | string | Group ID. |
canonicalDocumentId | string | ID of the document representing this group in retrieval. |
detectedAt | string | ISO 8601 timestamp of detection. |
members | DuplicateGroupMember[] | Members with filename, relationship, and coverage percentages. |
Each member has relationship (canonical | near_duplicate), coverageToCanonical (fraction 0–1), and coverageFromCanonical.
/v1/collections/:id/duplicates/detectTrigger an async deduplication run across every ready document. Returns 409 if a run is already in flight.
// Response 202 Accepted
{
"runId": "r1a2b3c4-...",
"status": "pending",
"jobsEnqueued": 142,
"enqueuedAt": "2026-04-16T12:00:00Z"
}const run = await client.duplicates.detect('3f7a1b2c-...')
console.log(run.runId, run.jobsEnqueued)The DuplicateRun object
| Field | Type | Description |
|---|---|---|
id | string | Run ID. |
status | string | pending | running | completed | failed. |
jobsEnqueued | number | null | Total per-document jobs queued for the run. |
jobsProcessed | number | null | Jobs completed so far. |
duplicatesDetected | number | null | Near-duplicate documents found. |
duplicateGroupsCreated | number | null | Distinct clusters created. |
startedAt | string | null | ISO 8601 start timestamp. |
completedAt | string | null | ISO 8601 completion timestamp. |
error | string | null | Error message if status is failed. |
createdAt | string | ISO 8601 creation timestamp. |
/v1/collections/:id/duplicates/runs/latestGet the status and stats of the most recent deduplication run.
const run = await client.duplicates.getLatestRun('3f7a1b2c-...')
console.log(run.status, run.duplicateGroupsCreated)/v1/collections/:id/duplicatesList duplicate groups with their members. Returns { total, items }.
Query parameters: limit (1–100, default 50), offset (default 0).
const { total, items } = await client.duplicates.list('3f7a1b2c-...', { limit: 20 })
for (const group of items) {
for (const m of group.members) {
if (m.relationship === 'near_duplicate') {
console.log(m.filename, Math.round((m.coverageToCanonical ?? 0) * 100) + '%')
}
}
}/v1/collections/:id/duplicates/:groupIdPromote a different member to canonical. The previous canonical becomes a near_duplicate. Coverage fields are cleared since they describe the old pairing.
// Request
{ "canonicalDocumentId": "d2e4f6a8-..." }
// Response
{ "success": true, "changed": true }await client.duplicates.promoteCanonical('3f7a1b2c-...', 'g1h2i3j4-...', 'd2e4f6a8-...')/v1/collections/:id/duplicates/:groupIdDisband a duplicate group. All former members rejoin retrieval as distinct documents.
await client.duplicates.disband('3f7a1b2c-...', 'g1h2i3j4-...')Provider Keys#
Store your own OpenAI, Anthropic, or Google Gemini key per project. When configured, Dewey uses your key for research, bypassing the shared managed key. Keys are encrypted at rest with AES-256-GCM.
/v1/projects/:id/provider-keysStore a provider key.
// Request
{
"provider": "openai", // "openai" | "anthropic" | "gemini"
"key": "sk-...",
"name": "Production key"
}const key = await client.providerKeys.create('proj_abc123', {
provider: 'openai',
key: 'sk-...',
name: 'Production key',
})
// => { id: 'k1a2b3c4-...', provider: 'openai', keyPreview: 'sk-t...cdef', ... }The ProviderKey object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique key identifier. |
provider | "openai" | "anthropic" | "gemini" | Key provider. |
name | string | Display label. |
keyPreview | string | First 4 and last 4 characters of the key (e.g. "sk-t...cdef"). Plaintext is never stored or returned. |
createdAt | string (ISO 8601) | Timestamp when the key was stored. |
/v1/projects/:id/provider-keysList stored provider keys. Plaintext is never returned.
const keys = await client.providerKeys.list('proj_abc123')
// => [{ id: 'k1a2b3c4-...', provider: 'openai', keyPreview: 'sk-t...cdef', ... }]/v1/projects/:id/provider-keys/:keyIdRemove a provider key. Returns 204 No Content.
await client.providerKeys.delete('proj_abc123', 'k1a2b3c4-...')
// => 204 No ContentReal-time Events#
Subscribe to document status changes as they happen using Server-Sent Events.
/v1/collections/:id/documents/eventsSSE stream of document status events for a collection. Stays open until the client disconnects.
?key=dwy_live_...const es = new EventSource(
`https://api.meetdewey.com/v1/collections/${collectionId}/documents/events` +
`?key=${apiKey}`
)
es.onmessage = (e) => {
const event = JSON.parse(e.data)
// event.documentId, event.collectionId, event.status, event.sectionCount?
if (event.status === 'ready') {
console.log(`${event.documentId} is queryable`)
}
}
// Ping sent every 20 seconds to keep the connection alive.The DocumentEvent object
| Field | Type | Description |
|---|---|---|
documentId | string (UUID) | ID of the document that changed. |
collectionId | string (UUID) | ID of the containing collection. |
status | string | New document status. Any value from the processing lifecycle. |
sectionCount | numberoptional | Number of sections detected. Present only when status is "sectioned". |
Webhooks#
Webhooks let your server receive push notifications when documents finish processing, no polling required. Configure one or more endpoints per project; Dewey will POST a signed JSON payload to each endpoint whenever a subscribed event fires.
/v1/orgs/:orgId/projects/:projectId/webhooksList all webhook endpoints for a project. Secrets are never returned after creation.
/v1/orgs/:orgId/projects/:projectId/webhooksCreate a webhook endpoint. Returns the signing secret exactly once. Store it securely.
const res = await fetch(
`https://api.meetdewey.com/v1/orgs/${orgId}/projects/${projectId}/webhooks`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://yourapp.com/hooks/dewey',
events: ['document.ready', 'document.error'], // omit for all events
description: 'Production handler',
}),
}
)
const { id, secret } = await res.json()
// Save secret - it will not be returned again/v1/orgs/:orgId/projects/:projectId/webhooks/:endpointIdUpdate a webhook endpoint: change the URL, subscribed events, enabled state, or description.
/v1/orgs/:orgId/projects/:projectId/webhooks/:endpointIdDelete a webhook endpoint and all its delivery history.
/v1/orgs/:orgId/projects/:projectId/webhooks/:endpointId/deliveriesList recent deliveries for an endpoint (newest first). Supports ?limit up to 100, default 50.
The WebhookEndpoint object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier for this endpoint. |
url | string | HTTPS URL that receives POST requests. |
events | string[] | Event types that trigger this endpoint. Empty array means all events. |
enabled | boolean | Whether this endpoint is active. Disabled endpoints are skipped at dispatch time. |
description | stringoptional | Optional human-readable label. |
secret | stringoptional | Signing secret (64-char hex). Returned only in the POST /webhooks response. Store it immediately. |
createdAt | string (ISO 8601) | When the endpoint was created. |
updatedAt | string (ISO 8601) | When the endpoint was last updated. |
Event types
| Event | Fires when |
|---|---|
document.ready | A document has finished processing and is fully queryable. |
document.error | A document failed at some stage of the ingestion pipeline. |
Payload shape
Every webhook POST has Content-Type: application/json and the following top-level shape:
{
"id": "wh_3f7a1b2c4d5e6f...", // unique delivery ID
"type": "document.ready", // event type
"created": 1712345678, // Unix timestamp (seconds)
"data": {
"documentId": "9a2c4e6f-...",
"collectionId": "3f7a1b2c-...",
"projectId": "1a2b3c4d-...",
// document.error only:
"error": "Conversion failed: unsupported file format"
}
}Signature verification
Every delivery includes an X-Dewey-Signature header containing an HMAC-SHA256 of the raw request body, computed with the endpoint's signing secret:
X-Dewey-Signature: sha256=<hex digest>
X-Dewey-Event-Id: <delivery UUID>
X-Dewey-Event-Type: document.readyVerify the signature before processing the event:
import { createHmac, timingSafeEqual } from 'node:crypto'
function verifyWebhook(
rawBody: string,
signature: string,
secret: string,
): boolean {
const expected = 'sha256=' +
createHmac('sha256', secret).update(rawBody).digest('hex')
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}
// Express / Next.js example
app.post('/hooks/dewey', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-dewey-signature'] as string
if (!verifyWebhook(req.body.toString(), sig, process.env.DEWEY_WEBHOOK_SECRET!)) {
return res.sendStatus(401)
}
const event = JSON.parse(req.body.toString())
// handle event.type === 'document.ready' etc.
res.sendStatus(200)
})timingSafeEqual / hmac.compare_digest) to prevent timing attacks. Dewey retries failed deliveries up to 10 times with exponential backoff; return 2xx as quickly as possible and process the event asynchronously.The WebhookDelivery object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique delivery ID (also sent as X-Dewey-Event-Id). |
endpointId | string (UUID) | The endpoint this delivery was sent to. |
eventType | string | The event type that triggered this delivery. |
payload | object | The full JSON payload that was POSTed. |
status | "pending" | "success" | "failed" | Current delivery status. "pending" while retries are in flight. |
attempts | number | Number of delivery attempts made so far. |
lastAttemptAt | string (ISO 8601)optional | When the most recent attempt was made. |
responseStatus | numberoptional | HTTP status code returned by your server on the last attempt. |
responseBody | stringoptional | First 1,024 bytes of your server's response body. |
createdAt | string (ISO 8601) | When this delivery record was created. |
MCP Server#
Dewey ships a native Model Context Protocol server. Any MCP-compatible client (Claude, Cursor, custom agents) can search, research, manage collections and documents, extract claims, and detect contradictions directly, with no API integration required. Two ways to connect: a hosted endpoint with OAuth (recommended), or a local install with an API key.
Hosted MCP
Add Dewey as a custom MCP server in your client with this URL:
https://mcp.meetdewey.com/mcpOn first connect, your client redirects you through an OAuth consent screen on app.meetdewey.com. Sign in to your Dewey account, pick the org the client should access, and approve. The client receives a scoped access token — no API key in your client config, no shared secrets.
Scopes
| Scope | Grants |
|---|---|
dewey:read | Search and read documents, collections, claims, and contradictions. |
dewey:full | Everything in dewey:read plus create, update, delete, and run agentic operations (research, contradiction detection, deduplication). |
Manage and revoke connected MCP clients from Connected apps in your dashboard.
Local install
Prefer to run the MCP server locally? Add Dewey to your claude_desktop_config.json:
{
"mcpServers": {
"dewey": {
"command": "npx",
"args": ["-y", "@meetdewey/mcp"],
"env": {
"DEWEY_API_KEY": "dwy_live_...",
"DEWEY_COLLECTION_ID": "3f7a1b2c-..." // optional - scope to one collection
}
}
}
}"NODE_EXTRA_CA_CERTS": "/etc/ssl/cert.pem" to the env block above to allow Node to trust system certificates.Available tools
Search
| Tool | Description |
|---|---|
dewey_search | Hybrid semantic + keyword search over chunk content in a collection. Supports tags, anyTags, and metadata filters. |
dewey_scan_sections | Lightweight search over section titles and summaries. Fast corpus exploration without loading chunk content. Supports tags, anyTags, and metadata filters. |
dewey_research | Run a full agentic research query with configurable depth. Returns a grounded, cited answer. Supports tags, anyTags, and metadata filters. |
dewey_get_section | Fetch the full Markdown content of a section by its ID. |
dewey_get_section_chunks | Fetch all text chunks for a section. Use when you need finer-grained content than full section Markdown. |
dewey_get_document_sections | List all sections in a document — the table of contents with heading levels and section IDs. |
dewey_get_document_markdown | Fetch the full Markdown content of a document. Use for document-level analysis when individual sections are not enough. |
Claims
| Tool | Description |
|---|---|
dewey_list_claims | List factual claims extracted from documents in a collection or a specific document. Claims are scored by importance (1–5). Filter with min_importance to focus on the most significant findings. |
Quality
| Tool | Description |
|---|---|
dewey_detect_contradictions | Trigger an async contradiction detection run. Analyzes all extracted claims for conflicts and generates resolution suggestions. |
dewey_get_contradiction_run | Get the status of the latest contradiction detection run. Use to poll progress after calling dewey_detect_contradictions. |
dewey_list_contradictions | List contradictions detected in a collection — clusters of conflicting claims with severity ratings and suggested resolution instructions. |
dewey_resolve_contradiction | Apply or dismiss a detected contradiction. Applying appends the resolution instruction to collection settings so future research respects it. |
Management
| Tool | Description |
|---|---|
dewey_list_collections | List all collections in the project. |
dewey_create_collection | Create a new collection in a project with optional chunk size, overlap, and embedding model settings. |
dewey_get_collection_stats | Get statistics for a collection: document count, storage, section and chunk counts, total extracted claims, and processing status breakdown. |
dewey_update_collection | Update collection settings: name, description, custom research instructions, visibility, and feature flags. |
dewey_describe_collection | Auto-generate a description for a collection using an LLM that reads document filenames and section headings. |
dewey_delete_collection | Permanently delete a collection and all its data. Cannot be undone. |
dewey_list_documents | List documents in a collection with their processing status. |
dewey_get_document | Fetch metadata for a single document by ID, including status, tags, and metadata. |
dewey_wait_for_document | Long-poll until a document finishes processing (ready or error). Blocks up to 5 minutes. Useful for "upload → wait → query" agentic loops. |
dewey_list_document_tags | List all tags used in a collection with document counts. Useful for discovering filter values before searching. |
dewey_update_document | Update the tags and/or metadata on a document. Metadata is shallow-merged by default. |
dewey_delete_document | Permanently delete a document and all its sections, chunks, and extracted claims. |
dewey_batch_delete_documents | Delete multiple documents at once. Cannot be undone. |
dewey_retry_document | Retry processing a document that failed ingestion. |
dewey_retry_failed_documents | Retry all documents in a collection that are currently in error status. |
dewey_recompute_summaries | Re-run AI section summarization across all documents. Useful after changing the collection LLM model. |
dewey_recompute_captions | Re-run AI captioning for all images and tables. Useful after changing the collection LLM model. |
dewey_recompute_claims | Re-extract claims from all ready documents. Deletes existing claims and re-runs extraction with the current LLM model. |
CLI#
dewey is a single binary that gives developers terminal-native access to every Dewey capability — upload, retrieval, research, and operations — without writing any code. It is the fastest way to go from an API key to a grounded answer over your own documents.
Install
Download the latest release for your platform from GitHub Releases and move the binary to a directory in your PATH:
curl -fsSL https://raw.githubusercontent.com/meetdewey/dewey-cli/main/install.sh | shThe script detects your OS and architecture, downloads the correct binary, verifies the SHA-256 checksum, and installs to ~/.local/bin (no sudo required). Override with INSTALL_DIR=/usr/local/bin sh install.sh.
Or download a specific release directly — GoReleaser names archives as dewey_<version>_<os>_<arch>.tar.gz:
# Example: v0.1.0 on macOS arm64
curl -Lo dewey.tar.gz \
https://github.com/meetdewey/dewey-cli/releases/download/v0.1.0/dewey_0.1.0_darwin_arm64.tar.gz
tar -xzf dewey.tar.gz && mv dewey /usr/local/bin/Windows users can download the .zip from the Releases page.
Authentication
Set your project API key in the environment. Every data-plane command reads it from there — you never need to pass it on the command line.
export DEWEY_API_KEY=dwy_live_…The --api-key flag is also accepted (prefer the env var). Commands that require auth exit with code 77 and a hint if the key is missing.
Commands
| Command | Description |
|---|---|
dewey upload | Upload one or more files to a collection. Accepts file paths, glob patterns, or stdin (-). Concurrency defaults to 4. |
dewey query | Hybrid BM25 + vector retrieval. Returns ranked chunks with section and document provenance. |
dewey scan | Lightweight semantic scan over section titles and summaries — cheaper than a full query. |
dewey research | Agentic research loop. Streams a cited markdown answer to stdout by default when stdout is a TTY. |
dewey watch | Tail the SSE document-status stream for a collection. Reconnects automatically. |
dewey doctor | Validate the local environment: API key, DNS, TLS, and a live /collections check. |
dewey collections | Subcommands: list, get, create, update, delete, stats. |
dewey docs | Subcommands: list, get, markdown, sections, chunks, images, delete, wait. |
dewey duplicates | Subcommands: detect, list, resolve, dismiss. |
dewey contradictions | Subcommands: detect, list, apply, dismiss. |
dewey claims | Subcommands: list, get. |
dewey provider-keys | Subcommands: list, set, delete. |
dewey config | Subcommands: get, set, reset, path. |
Every command accepts a -c / --collection flag. The last-used collection is cached in ~/.dewey/state.toml so you can omit -c on subsequent commands.
dewey upload
The most-used command. Accepts file paths, glob patterns, or - to read from stdin. Pass --watch to stream live processing events until every document reaches a terminal state, or --wait to block silently (useful in CI).
dewey upload ./papers/*.pdf -c research-papers --watch
Uploading 3 files to research-papers
[ok] attention.pdf uploaded doc_a1
[ok] bert.pdf uploaded doc_a2
[ok] gpt4.pdf uploaded doc_a3
Watching processing events (Ctrl-C to detach):
[ok] doc_a1 attention.pdf ready
[ok] doc_a2 bert.pdf ready
[ok] doc_a3 gpt4.pdf ready
[ok] 3/3 ready in 31s.| Flag | Description |
|---|---|
--watch | Stream live status events and exit when all docs are terminal. |
--wait | Block silently until all docs are ready or errored. Useful in CI. |
--concurrency N | Parallel uploads (default 4). |
--tag name | Assign tags (repeatable). |
--metadata k=v | Attach structured metadata (repeatable, JSON values parsed automatically). |
dewey research
Streams the agentic answer token-by-token when stdout is a TTY, then prints a citation block. Pass --no-stream to return a single response, or pipe the output to get NDJSON automatically.
dewey research research-papers "how does multi-head attention work?" --depth deep
→ search: multi-head attention mechanism
→ search: scaled dot-product attention
Multi-head attention runs H parallel attention functions...
[continues streaming]
— Citations ——————————————————————————————————
[1] attention.pdf § 3. Multi-Head Attention
[2] attention.pdf § 3.2 Scaled Dot-Product Attention| Flag | Description |
|---|---|
--depth | quick · balanced · deep · exhaustive (default: balanced). deep and exhaustive require Pro+ and a BYOK provider key. |
--model | Override the model (e.g. gpt-4o-mini). |
--stream / --no-stream | Force streaming on or off regardless of TTY detection. |
dewey doctor
Validates the full environment in one command: API key presence, base URL, DNS resolution, TLS certificate, a live GET /collections check, and telemetry status.
dewey doctor
Checking environment...
[ok] DEWEY_API_KEY is set (dwy_live_…vJ3K)
[ok] Base URL → https://api.meetdewey.com/v1 (default)
[ok] DNS resolves
[ok] TLS handshake (issued by R12, valid 73 days)
[ok] GET /collections returned 200 in 142ms (3 collections)
[ok] Anonymous telemetry: on (DEWEY_TELEMETRY=0 to opt out)
All systems go.Exits 0 if all checks pass. Exits 1 on any failure with a per-check remediation hint. Point DEWEY_BASE_URL at http://localhost:3000/v1 to run doctor against a local server.
Configuration
The CLI reads ~/.dewey/config.toml on startup. Edit it directly or use dewey config set:
# ~/.dewey/config.toml
default_collection = "research-papers"
output = "human" # "human" | "json"
color = "auto" # "auto" | "always" | "never"
# base_url = "http://localhost:3000/v1" # uncomment for local dev| Env var | Description |
|---|---|
DEWEY_API_KEY | Project API key. Required for all data-plane commands. |
DEWEY_BASE_URL | Override the API endpoint. Defaults to https://api.meetdewey.com/v1. |
DEWEY_TELEMETRY | Set to 0 to opt out of anonymous usage telemetry. |
NO_COLOR | Disable ANSI colour output (honoured automatically). |
--json mode
Every command supports --json for machine-readable output. The contract is stable across minor CLI versions; breaking changes bump the major version. Streamed commands (research --stream --json) emit NDJSON — one object per event — so they pipe cleanly to jq.
# Stream a research answer and capture only the final done event
dewey research my-collection "question" --stream --json | jq -s 'last'
# List collections as JSON and filter by name
dewey --json collections list | jq '.[] | select(.name == "research-papers")'
# Wait for a document and assert it's ready
dewey --json docs wait doc_abc123 | jq -e '.status == "ready"'Diagnostic output (progress, status lines) always goes to stderr, so redirecting stdout captures only the data you asked for.
Usage & Quotas#
Fetch current usage and plan limits for a project. Useful for building quota warnings into your own UI or automation.
/v1/usageReturn current usage across all meters for the project. Authenticated with the project API key.
// Response 200 OK
{
"meters": {
"documents": { "used": 23, "limit": 500 },
"queries": { "used": 1840, "limit": 2000 },
"credits": { "used": 4.5, "limit": 25 },
"storage": { "usedMb": 312, "limitMb": 500 }
}
}limit is null when the meter is unlimited on the current plan.Errors#
All errors return a JSON body with an error field:
{
"error": "Collection not found"
}| Status | Meaning |
|---|---|
| 400 | Bad request: invalid or missing parameters. |
| 401 | Missing or invalid API key or JWT. |
| 402 | Payment required. Returned when deep/exhaustive research requires a configured provider key (BYOK), or when a plan document/collection quota has been reached. |
| 403 | Access denied: the key does not have permission for this resource. |
| 404 | Resource not found. |
| 409 | Conflict, e.g. duplicate slug. |
| 413 | Payload too large. File exceeds the 25 MB limit. |
| 422 | Unprocessable: validation failed. |
| 429 | Rate or quota exceeded. Query or research credit limits have been reached for the current billing period. Check the Retry-After response header for when the limit resets, or upgrade your plan. |
| 500 | Internal server error. |
| 503 | Service degraded: dependency check failed. |