Skip to main content

Update API

Update custom metadata or the source filename for an existing document without re-uploading it.

Updates are asynchronous. The endpoint validates the request, binds a job to the calling client, and returns 202 Accepted with a job ID. The worker merges the patch, republishes canonical metadata and its Metadata Leaves as a new immutable generation, and activates it through the publication authority. Poll GET /status/{job_id} for progress and completion. If a publication is already FENCED, acceptance still means queued rather than applied; the worker retries until the authority can publish or reports the bounded failed state. See the publication lifecycle comparison for how acceptance differs from search and synchronous deletion.

Publication required

This endpoint requires the publication authority (PUBLICATION_ENABLED=true). A deployment with publication disabled is rejected at startup because search has no synchronous fallback path.

Client Isolation

Updates are scoped to the caller's client. The endpoint accepts before resolving the document, so a document that belongs to a different client is simply invisible to the worker: the job fails, and GET /status/{job_id} reports the same bounded {"status": "failed", "error": "Metadata update failed"} it reports for a document that does not exist. That masking is deliberate — the status response never distinguishes an unknown document from another client's document, so no cross-client information is leaked. The job itself stays client-bound: GET /status/{job_id} returns 404 for a job owned by a different client.

Endpoint​

PATCH /update

Request​

Headers​

HeaderRequiredDescription
X-API-KeyYesValid API key from API_KEY_CLIENTS env var
Content-TypeYesapplication/json

Body Parameters​

ParameterTypeRequiredDescription
document_idstringYesThe document ID to update
version_idstringNoTarget one document version. Omit to target every version of the document under the calling client
metadataobjectNo*JSON Merge Patch applied to Custom Metadata: objects merge recursively, null deletes a member, omitted members stay unchanged. An empty object is accepted and completes as a no-op
filenamestringNo*Non-empty display name written to metadata.source.filename in the new published generation

*At least one of metadata or filename is required. A filename is trimmed and cannot contain control characters. It does not change source.mime_type or source.extension. filename is a request field, not a custom metadata key; putting filename inside metadata still stores metadata.custom.filename.

Response​

{
"job_id": "6f1c8f0a2b7d4e6b8c1d2e3f4a5b6c7d",
"status": "pending",
"document_id": "doc-123",
"version_id": null,
"updated_fields": ["metadata.custom.title", "metadata.custom.author"]
}
FieldDescription
job_idClient-bound job ID to poll on GET /status/{job_id}
statusAlways "pending" — the update has been accepted, not applied
document_idThe document ID targeted by the update
version_idThe targeted version, or null when every version is targeted
updated_fieldsCanonical stored paths carried by the patch. Custom keys are listed under metadata.custom; a filename rename is listed as metadata.source.filename

Canonical labels quote a literal custom key when needed. For example, the raw key foo.bar remains one custom dictionary key and is reported as metadata.custom."foo.bar", rather than as a nested path.

A filename update is reported as metadata.source.filename in updated_fields.

Examples​

Update Metadata​

Update user metadata for an existing document:

curl -X PATCH http://localhost:8000/update \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"document_id": "doc-123",
"metadata": {
"title": "Updated Report",
"author": "Jane Smith",
"department": "Engineering"
}
}'

Updated fields are stored under metadata.custom.* and become searchable using the explicit metadata.custom. prefix (e.g. metadata.custom.author). For example, author here is user metadata; system-derived source properties remain under metadata.source.*.

The worker validates the complete merged Custom Metadata object against the Metadata Discovery V1 contract before staging anything; a violation fails the job and leaves the previous generation authoritative. See the Metadata and Entities Reference for eligible values, exact limits, path semantics, and the value-free diagnostic shape, and the Status API for the job response fields.

# Search for documents updated with a specific department
curl -X POST http://localhost:8000/search \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{
"query": "quarterly report",
"scope": "Content",
"filters": {
"metadata.custom.department": "Engineering"
}
}'

Rename the source filename​

PATCH /update can set metadata.source.filename on every live chunk of the document without re-uploading. Soft-deleted points keep the previous name. Later /search and /documents reads show the new name.

curl -X PATCH http://localhost:8000/update \
-H "X-API-Key: super-secret-key" \
-H "Content-Type: application/json" \
-d '{"document_id":"doc-123","filename":"renamed-report.pdf"}'

The worker republishes the selected document version or versions with the new metadata.source.filename. Custom metadata and the remaining system metadata stay unchanged. Lifecycle visibility is preserved as well: updating a soft-deleted version carries its deleted, deleted_at, and deleted_by values into the new generation and does not restore it.

Update from Python​

import requests

url = "http://localhost:8000/update"
headers = {
"X-API-Key": "super-secret-key",
"Content-Type": "application/json"
}
data = {
"document_id": "doc-123",
"filename": "FY2026-board-pack.pdf",
"metadata": {"title": "Updated Report"}
}
response = requests.patch(url, headers=headers, json=data)
job_id = response.json()["job_id"]

status = requests.get(f"http://localhost:8000/status/{job_id}", headers=headers)
print(status.json()["operation"], status.json()["status"])
# metadata_update completed

Response Codes​

CodeDescription
202Accepted - Update job queued and bound to the calling client
400Bad Request - Invalid document_id/version_id, reserved metadata, or filename control characters
401Unauthorized - Invalid API key
500Internal Server Error - Job binding or enqueue failed; nothing was queued
503Service Unavailable - Publication authority is disabled or unavailable
422Validation Error - Missing update fields, invalid request shape, or a Metadata Discovery contract violation

Publication-disabled deployments fail this path closed with 503. Enable the publication authority to accept metadata updates; do not use direct Qdrant writes as a workaround. A document that does not exist — or belongs to a different client — is only detected by the worker, so the job fails rather than returning 404 at accept time. Poll GET /status/{job_id}: the job reports status: "failed" with the bounded error "Metadata update failed". Merged-metadata contract violations are the one failure that carries structured detail, in error_details (code, path, observed, limit, unit); no other failure reason is disclosed, and no Custom Metadata value ever appears there. GET /status/{job_id} still returns 404 for a job bound to a different client.

Error Responses​

{
"detail": "Invalid API key"
}
{
"detail": "Reserved metadata keys are not allowed: classification, metadata.created_at, processing"
}

A failed update job reports its outcome on GET /status/{job_id}, not on PATCH /update:

{
"status": "failed",
"operation": "metadata_update",
"error": "Metadata update validation failed",
"error_details": {
"code": "metadata_canonical_json_bytes_limit",
"path": [],
"observed": 65537,
"limit": 65536,
"unit": "utf8_bytes"
}
}

System and legacy metadata paths inside metadata remain read-only and are rejected here. The dedicated filename field is the supported way to rename metadata.source.filename. See the Metadata and Entities Reference for the authoritative group and reserved-key contract.

Best Practices​

  1. Use for user metadata corrections - Fix user-facing titles, authors, or custom fields without reprocessing the entire document
  2. Partial updates - Only include the fields you want to change; omitted members are preserved and null deletes a member
  3. Custom metadata - All ordinary update keys are stored under metadata.custom.* and become filterable in search using the explicit metadata.custom. prefix
  4. Verify first - Use GET /documents to confirm the document exists before updating
  5. Poll the job - Treat 202 as queued; confirm status: "completed" and search_ready: true on GET /status/{job_id} before relying on the change in search