Documents — MDDB API
Document management operations
Add or update document #
POST /v1/add
Add a new document or update an existing one. If a document with the same collection, key, and language exists, it will be updated and a new revision will be created.
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: AddRequest
{
"collection": "blog",
"contentMd": "# Hello World\n\nWelcome to MDDB!",
"expectedRevision": "string",
"key": "hello-world",
"lang": "en_US",
"meta": {
"author": [
"John Doe"
],
"category": [
"blog"
]
}
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Document added/updated successfully. Since v2.18.8 the document carries revision (INT-021) and, when the write queued an embedding, indexing (GO-042): searchable by text now, by meaning once the job finishes. |
AddResponse (application/json) |
409 |
expectedRevision did not match; nothing was written | RevisionConflict (application/json) |
507 |
(v2.18.9+) Refused by a collection quota or the disk reserve / vector RAM limit; nothing was written (GO-043) | QuotaExceeded (application/json) |
400 |
Invalid request | ErrorResponse (application/json) |
403 |
Read-only mode | ErrorResponse (application/json) |
Batch add documents #
POST /v1/add-batch
Add multiple documents to a collection in a single request. Uses the optimized batch processor for high throughput. Fires all post-commit hooks (embedding, FTS indexing, webhooks, TTL, automation triggers).
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: AddBatchRequest
{
"collection": "blog",
"documents": [
{
"contentMd": "# Hello World",
"key": "post1",
"lang": "en",
"meta": {
"category": [
"blog"
]
},
"saveRevision": false
}
]
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Batch result | AddBatchResponse (application/json) |
507 |
(v2.18.9+) Refused by a collection quota or the disk reserve / vector RAM limit; nothing was written (GO-043) | QuotaExceeded (application/json) |
400 |
Invalid request | ErrorResponse (application/json) |
403 |
Read-only mode | ErrorResponse (application/json) |
Submit async bulk ingest job #
POST /v1/bulk-ingest-job
Queue a long-running bulk ingest job and return immediately with a
job identifier. Clients should poll /v1/bulk-ingest-job/{id} for
progress or supply callbackUrl to receive a POST with the final
job record on completion.
Jobs are processed by a single background worker in FIFO order.
Payloads are kept in memory — if the server restarts mid-job, the
status record is flipped to failed on the next start.
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: BulkIngestSubmitRequest
{
"callbackUrl": "https://example.com/webhook",
"collection": "blog",
"documents": [
{
"contentMd": "# Hello World",
"key": "post1",
"lang": "en",
"meta": {
"category": [
"blog"
]
},
"saveRevision": false
}
]
}
Responses #
| Status | Description | Body |
|---|---|---|
202 |
Job accepted | BulkIngestJob (application/json) |
400 |
Invalid request | ErrorResponse (application/json) |
Get bulk ingest job status #
GET /v1/bulk-ingest-job/{id}
Returns the current status record — including counters, timestamps, and up to 50 error messages.
Authorization: BearerAuth or APIKeyAuth or none
Parameters #
| Name | In | Type | Description |
|---|---|---|---|
id (required) |
path | string |
Job identifier returned from submit |
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Job status | BulkIngestJob (application/json) |
404 |
Job not found |
Cancel pending bulk ingest job #
DELETE /v1/bulk-ingest-job/{id}
Only jobs in pending status can be cancelled. In-flight jobs must run to completion.
Authorization: BearerAuth or APIKeyAuth or none
Parameters #
| Name | In | Type | Description |
|---|---|---|---|
id (required) |
path | string |
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Job cancelled | |
400 |
Job already started or completed |
List bulk ingest jobs #
GET /v1/bulk-ingest-jobs
Returns all jobs newest-first, optionally filtered by collection.
Authorization: BearerAuth or APIKeyAuth or none
Parameters #
| Name | In | Type | Description |
|---|---|---|---|
collection |
query | string |
Filter by collection name |
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Job list | object (application/json) |
400 |
Bulk ingest is not initialized | ErrorResponse (application/json) |
401 |
Auth is on (MDDB_AUTH_ENABLED=true) and the request carries no credentials, an invalid or expired JWT, or an unknown API key. | ErrorResponse (application/json) |
403 |
The caller lacks the permission the route needs (SEC-019 access gate, auth on: read, write or admin on the collections the request names), or a write meets read-only mode (MDDB_MODE=read or the protocol's own mode). | ErrorResponse (application/json) |
429 |
The client is over the API rate limit (MDDB_RATE_LIMIT_ENABLED=true, MDDB_RATE_LIMIT_REQUESTS per MDDB_RATE_LIMIT_WINDOW seconds). A call that embeds can also be refused by the collection's daily embedding budget, with a QuotaExceeded body. | RateLimitError (application/json) |
401 example (application/json)
{
"error": "missing authentication"
}
403 example (application/json)
{
"error": "forbidden"
}
Bulk ingest documents #
POST /v1/ingest
Advanced bulk ingest endpoint for scraping pipelines and data import workflows. Supports URL key derivation, YAML frontmatter extraction, content deduplication, auto-metadata injection, and collection auto-configuration.
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: IngestRequest
{
"collection": "imported",
"documents": [
{
"contentBase64": "string",
"contentMd": "# Page 1\n\nContent here.",
"extractFrontmatter": false,
"format": "pdf",
"images": [
{}
],
"key": "page1",
"lang": "en",
"scrapedAt": 1709500000,
"scraper": "my-crawler",
"ttl": 86400,
"url": "https://example.com/page1"
}
],
"options": {
"autoConfigureCollection": false,
"extractImages": false,
"fetchRemoteImages": false,
"profile": "default",
"saveRevision": false,
"skipDuplicates": false,
"skipEmbeddings": false,
"skipFts": false,
"skipWebhooks": false,
"textOnly": false
}
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Ingest result | IngestResponse (application/json) |
507 |
(v2.18.9+) Refused by a collection quota or the disk reserve / vector RAM limit; nothing was written (GO-043) | QuotaExceeded (application/json) |
400 |
Invalid request | ErrorResponse (application/json) |
403 |
Read-only mode | ErrorResponse (application/json) |
Upload files with auto-conversion to Markdown #
POST /v1/upload
Upload files via multipart/form-data. Files are auto-converted to Markdown and stored as documents. Supports single and batch upload.
Supported formats: .md, .txt, .html, .htm, .pdf, .docx, .odt, .rtf, .xlsx, .ods, .csv, .tsv, .pptx, .odp, .epub, .eml, .mbox, .yaml, .yml, .log, .lex, .tex, .latex.
Default (md/txt) is stored as-is with frontmatter extraction; others are converted.
ODT and DOCX are ZIP-based with XML extraction. RTF is parsed to plain text.
LaTeX/TeX is converted (sections, formatting, environments). YAML/LOG/LEX are wrapped in code blocks.
(v2.18.9+, GO-046) XLSX and ODS become one section per sheet with a
table, dates as ISO 8601; CSV/TSV a table (separator detected).
PPTX and ODP become one section per slide, speaker notes included.
EPUB is read in spine order. EML and MBOX put from, to, date, subject
and messageId into meta, and each convertible attachment (and each
message of a mailbox) becomes a document of its own with
meta.parent, listed under parts. The format is taken from the
content where it has a signature (%PDF, a zip's main part, {\rtf),
so a PDF named .txt is converted as a PDF; binary content no
converter recognises is refused. Every converted document carries
meta.conversionConfidence (0-1) and meta.conversionWarnings.
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type multipart/form-data: object
| Property | Type | Description |
|---|---|---|
file |
string (binary) |
Single file to upload |
files[] |
array of string (binary) |
Multiple files to upload |
collection (required) |
string |
Target collection name |
lang (required) |
string |
Document language code (e.g. en_US) |
key |
string |
Document key (derived from filename if empty) |
meta |
string |
JSON-encoded metadata map |
ttl |
integer |
Time-to-live in seconds |
maxSize |
integer |
Per-file size limit in bytes (default 10MB, max 100MB) |
profile |
string |
Ingest profile, as for /v1/ingest (default, fast) |
textOnly |
string |
Extract plain text from html, docx, odt and epub instead of rebuilding Markdown structure One of "true", "false". |
extractImages |
string |
(v2.18.9, RAG-010) Images in the files become image documents; image files sent with a Markdown file are the files its links name, not documents of their own. On by default in a collection of type images, where an image file uploaded on its own is an image document. One of "true", "false". |
fetchRemoteImages |
string |
(v2.18.9, RAG-010) Fetch Markdown image links to http(s) URLs, SSRF-guarded. One of "true", "false". |
{
"collection": "string",
"extractImages": "true",
"fetchRemoteImages": "true",
"file": "string",
"files[]": [
"string"
],
"key": "string",
"lang": "string",
"maxSize": 0,
"meta": "string",
"profile": "string",
"textOnly": "true",
"ttl": 0
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
File(s) uploaded and stored | oneOf UploadResponse | UploadBatchResponse (application/json) |
400 |
Bad request | ErrorResponse (application/json) |
Get document #
POST /v1/get
Retrieve a document by collection, key, and language. Supports template variable substitution using the env parameter.
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: GetRequest
{
"collection": "blog",
"env": {
"siteName": "My Blog",
"year": "2025"
},
"key": "hello-world",
"lang": "en_US"
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Document retrieved successfully | Document (application/json) |
400 |
Invalid request | ErrorResponse (application/json) |
404 |
Document not found | ErrorResponse (application/json) |
Delete document #
POST /v1/delete
Delete a specific document by collection, key, and language
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: DeleteRequest
{
"collection": "blog",
"key": "hello-world",
"lang": "en_US"
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Document deleted successfully | object (application/json) |
400 |
Invalid request | ErrorResponse (application/json) |
404 |
Document not found (v2.18.8; was 400 before, DOC-018) | ErrorResponse (application/json) |
403 |
Read-only mode | ErrorResponse (application/json) |
Batch delete documents #
POST /v1/delete-batch
Delete multiple documents in a single request
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: object
| Property | Type | Description |
|---|---|---|
collection (required) |
string |
Target collection |
documents (required) |
array of object |
|
documents[].key |
string |
|
documents[].lang |
string |
{
"collection": "blog",
"documents": [
{
"key": "post-1",
"lang": "en"
},
{
"key": "post-2",
"lang": "en"
}
]
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Batch delete result | object (application/json) |
400 |
Invalid request | ErrorResponse (application/json) |
403 |
Forbidden | ErrorResponse (application/json) |
Delete collection #
POST /v1/delete-collection
Delete all documents in a collection
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: DeleteCollectionRequest
{
"collection": "blog"
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Collection deleted successfully | object (application/json) |
400 |
Invalid request | ErrorResponse (application/json) |
403 |
Read-only mode | ErrorResponse (application/json) |
Partial document update #
PATCH /v1/update
Update document metadata and/or content independently.
Only provided fields are updated; omitted fields remain unchanged.
Use meta: {} to clear all metadata.
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: object
| Property | Type | Description |
|---|---|---|
collection (required) |
string |
|
key (required) |
string |
|
lang (required) |
string |
|
meta |
object |
If provided, replaces all metadata. Use {} to clear. |
contentMd |
string |
If provided, replaces document content. |
ttl |
integer |
If provided, updates TTL in seconds. 0 = no expiry. |
expectedRevision |
string |
(v2.18.8, INT-021) Update only if the document is at this revision (from a read or write); otherwise 409. |
{
"collection": "blog",
"key": "p1",
"lang": "en",
"meta": {
"tag": [
"go",
"updated"
]
}
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Updated document, with its new revision | AddResponse (application/json) |
507 |
(v2.18.9+) Refused by a collection quota or the disk reserve / vector RAM limit; nothing was written (GO-043) | QuotaExceeded (application/json) |
400 |
Missing required fields or no fields to update | |
404 |
Document not found | |
409 |
expectedRevision did not match; nothing was written | RevisionConflict (application/json) |
405 |
Method not allowed |
Get document metadata only #
GET /v1/doc-meta
Returns metadata for a document without content. Lightweight read useful for checking tags without transferring document body.
Authorization: BearerAuth or APIKeyAuth or none
Parameters #
| Name | In | Type | Description |
|---|---|---|---|
collection (required) |
query | string |
Collection name |
key (required) |
query | string |
Document key |
lang |
query | string |
Language code (defaults to "en") Default "en". |
Responses #
| Status | Description | Body |
|---|---|---|
200 |
Document metadata | object (application/json) |
400 |
Missing required parameters | |
404 |
Document not found |
An image document's bytes #
GET /v1/image
(v2.18.9, RAG-010) The bytes of an image document, served with the
type the bytes have (never a stored claim) and nosniff. The url
of every ImageRef a search returns points here.
Authorization: BearerAuth or APIKeyAuth or none
Parameters #
| Name | In | Type | Description |
|---|---|---|---|
collection (required) |
query | string |
|
key (required) |
query | string |
|
lang (required) |
query | string |
Responses #
| Status | Description | Body |
|---|---|---|
200 |
The image | string (binary) (image/jpeg) |
400 |
Missing collection, key or lang | |
403 |
Forbidden | |
404 |
No image under that key | |
405 |
Method not allowed |
Delete every document a metadata filter matches #
POST /v1/documents:deleteWhere
GO-045, v2.18.8. Each document goes through the single-document
delete. Use dryRun first. At most limit per call; continue with
cursor.
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: WhereRequest
{
"collection": "string",
"cursor": "string",
"dryRun": false,
"filter": {
"kind": {
"not": [
"test"
]
},
"modified": {
"gte": "2026-09-01"
},
"path": {
"prefix": "internal/"
},
"size": {
"lt": 10000
},
"summary": {
"exists": true
},
"title": {
"contains": "hnsw"
}
},
"limit": 1000,
"ttl": 0
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
What matched and what was affected | WhereResult (application/json) |
400 |
No filter, bad limit, or invalid request | ErrorResponse (application/json) |
403 |
Forbidden, or read-only mode (a dry run is allowed) |
Merge metadata into every document a filter matches #
POST /v1/documents:updateWhere
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: WhereRequest
{
"collection": "string",
"cursor": "string",
"dryRun": false,
"filter": {
"kind": {
"not": [
"test"
]
},
"modified": {
"gte": "2026-09-01"
},
"path": {
"prefix": "internal/"
},
"size": {
"lt": 10000
},
"summary": {
"exists": true
},
"title": {
"contains": "hnsw"
}
},
"limit": 1000,
"ttl": 0
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
What matched and what was affected | WhereResult (application/json) |
400 |
No filter, no meta, or invalid request | |
403 |
Forbidden, or read-only mode (a dry run is allowed) |
Set the time to live of every document a filter matches #
POST /v1/documents:setTTLWhere
Authorization: BearerAuth or APIKeyAuth or none
Request body #
Required.
Content type application/json: WhereRequest
{
"collection": "string",
"cursor": "string",
"dryRun": false,
"filter": {
"kind": {
"not": [
"test"
]
},
"modified": {
"gte": "2026-09-01"
},
"path": {
"prefix": "internal/"
},
"size": {
"lt": 10000
},
"summary": {
"exists": true
},
"title": {
"contains": "hnsw"
}
},
"limit": 1000,
"ttl": 0
}
Responses #
| Status | Description | Body |
|---|---|---|
200 |
What matched and what was affected | WhereResult (application/json) |
400 |
No filter, no ttl, or invalid request | |
403 |
Forbidden, or read-only mode (a dry run is allowed) |