Skip to content

Uploading Patient Documents

Uploading Patient Documents

This guide walks you through the full lifecycle of a patient prontuário (medical record) document in DELPHOS: how to upload it, how it is processed asynchronously (OCR text extraction followed by automatic classification), how to poll for completion, how to list and remove documents afterward, and how an uploaded document ultimately enriches the patient’s clinical context through the Knowledge Graph.

Every endpoint on this page requires the x-api-key header and is tenant-scoped: you only ever see and modify documents that belong to your own API key. See Authentication & API Keys for how keys work.

Lifecycle at a glance

  1. Upload the file with POST /v1/patients/{patient_id}/prontuario-documents. The API responds 202 Accepted immediately — it does not wait for processing.

  2. OCR runs in the background. Text is extracted by DELPHOS Inteligência Documental.

  3. Classification runs next (unless you supplied a document_type). The extracted text is classified into one of the supported categories.

  4. Poll the patient’s document list to watch ocr_document.status and the document’s classification progress until processing is complete.

  5. The extracted text feeds the Knowledge Graph, which is what later powers patient context, clinical summaries, and lab-result trends.


Endpoints

MethodPathStatusDescription
POST/v1/patients/{patient_id}/prontuario-documents202Upload a document (multipart)
GET/v1/patients/{patient_id}/prontuario-documents/{document_id}200Poll one document’s status (OCR + classification)
DELETE/v1/patients/{patient_id}/prontuario-documents/{document_id}204Soft-delete a document (is_active=false)
GET/v1/patients/{patient_id}/documents200List a patient’s documents (status included per document)
POST/v1/patients/{patient_id}/documents201Link an already-stored OCR document to a patient
GET/v1/patients/{patient_id}/lab-trends200Lab-result trends derived from processed documents

Authentication

Pass your API key in the x-api-key header on every request. The key is bound to a single tenant; all document operations are automatically filtered to that tenant by row-level security.

Terminal window
curl "https://your-instance.delphos.app/v1/patients" \
-H "x-api-key: YOUR_API_KEY"

A missing or invalid key returns 401.


The patient_id path parameter

The {patient_id} segment accepts two identifier shapes, and you can use either interchangeably:

ShapeExampleNotes
Tenant-scoped opaque tokenpat_3Fq9Lm2Xb7Kd1Rs8Tn0WpRecommended. A pat_ prefix followed by 22 characters.
Internal DELPHOS UUID123e4567-e89b-12d3-a456-426614174000Accepted for backward compatibility; deprecated.

DELPHOS resolves the value you send to the internal patient record before the handler runs, under your tenant’s isolation context. The behaviour is identical regardless of which shape you use.


Upload a document

POST /v1/patients/{patient_id}/prontuario-documents

This endpoint accepts multipart/form-data (not JSON). It stores the file, creates the document records, and returns 202 Accepted while OCR and classification continue in the background.

Form fields

FieldTypeRequiredDescription
filefileYesThe document binary. Supported types: PDF, PNG, JPG, DICOM.
titlestringNoHuman-readable title (max 500 characters).
notesstringNoFree-text notes about the document (max 2000 characters).
document_typestringNoManual type override. When provided, automatic classification is skipped and the document is assigned this type immediately. See the enum below.

Supported file types and size limits

TypeMIMEMax sizeOCR + classification?
PDFapplication/pdf40 MBYes
PNGimage/png20 MBYes
JPGimage/jpeg (image/jpg also accepted)20 MBYes
DICOMapplication/dicom20 MBNo — DICOM cannot be OCR’d, so classification is skipped (status skipped).

The document_type enum

If you omit document_type, DELPHOS classifies the document automatically after OCR. If you supply it, it must be one of these nine values (any other value returns 400):

ValueMeaning
clinical_noteClinical note / evolution note
consent_formPatient consent form (termo de consentimento)
exam_resultExam or diagnostic result
imagingImaging study (X-ray, MRI, CT, ultrasound)
insurance_docInsurance / convênio documentation
lab_reportLaboratory report
otherUnclassified / catch-all
prescriptionPrescription document
referralReferral letter

The 202 response body

On success you receive 202 Accepted with this body (all fields are snake_case):

FieldTypeDescription
document_idUUIDID of the underlying OCR document record.
patient_document_idUUIDID of the patient ↔ document link.
patient_idUUIDThe resolved internal patient UUID.
document_typestringCurrent type. Provisional (other) when auto-classifying; may change once classification completes.
classification_statusstringOne of pending, completed, failed, skipped. At upload time this is pending (auto) or skipped (manual type / DICOM).
original_filenamestringThe filename you uploaded.
file_size_bytesintegerStored file size in bytes.
ocr_statusstringOne of pending, processing, completed, failed. Always pending at upload time.
created_atdatetimeCreation timestamp (UTC).
messagestringInformative status message.
{
"document_id": "9b2c1d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
"patient_document_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"patient_id": "123e4567-e89b-12d3-a456-426614174000",
"document_type": "other",
"classification_status": "pending",
"original_filename": "hemograma.pdf",
"file_size_bytes": 248193,
"ocr_status": "pending",
"created_at": "2026-06-03T14:21:09.482Z",
"message": "Documento recebido. OCR e classificação em processamento."
}

Upload example

Terminal window
curl -X POST \
"https://your-instance.delphos.app/v1/patients/pat_3Fq9Lm2Xb7Kd1Rs8Tn0Wp/prontuario-documents" \
-H "x-api-key: YOUR_API_KEY" \
-F "file=@hemograma.pdf;type=application/pdf" \
-F "title=Hemograma Completo — 2026-06-03" \
-F "notes=Coleta em jejum"

Document lifecycle & status values

A document moves through two independent status tracks after upload: OCR status and classification status. Both start at pending and are updated by the background pipeline.

ocr_status

ValueMeaning
pendingAccepted, OCR not started yet.
processingOCR is running.
completedText extracted successfully — the document is OCR-ready.
failedOCR could not extract text (e.g. unreadable scan, or an unsupported binary such as DICOM).

classification_status

ValueMeaning
pendingAwaiting automatic classification (OCR must finish first).
completedThe document was automatically classified; document_type now holds the final category.
failedClassification could not run — typically because OCR returned no text.
skippedClassification was intentionally not run: you supplied a document_type, or the file is a type that cannot be OCR’d (DICOM).

Typical transitions

ocr_status: pending → processing → completed
classification_status: pending ───────────────→ completed

If OCR fails, classification cannot proceed:

ocr_status: pending → processing → failed
classification_status: pending ───────────────→ failed

If you supplied a document_type (or uploaded a DICOM):

ocr_status: pending → processing → completed (DICOM: → skipped)
classification_status: skipped (unchanged)

Poll for completion

DELPHOS does not push a webhook when processing finishes. Poll until ocr_status and classification_status leave the pending / processing states.

Use the document_id from the upload response to poll exactly the document you uploaded. This is the simplest, lowest-overhead way to track one upload.

GET /v1/patients/{patient_id}/prontuario-documents/{document_id}

Returns 404 if no active document with that id exists for the patient. On success the body carries both statuses directly:

FieldTypeDescription
document_idUUIDThe OCR document id you polled with.
patient_document_idUUIDThe patient ↔ document link id.
patient_idUUIDThe patient UUID.
document_typestringCurrent type (classifier’s result once complete, or your manual value).
ocr_statusstringpendingprocessingcompleted | failed | skipped.
classification_statusstringpendingcompleted | failed, or skipped.
classification_confidencenumber or nullClassifier confidence (0–1) once classification completes.
original_filenamestring or nullOriginal upload filename.
file_size_bytesinteger or nullStored size.
page_countinteger or nullPages detected during OCR.
ocr_errorstring or nullError message if OCR failed.
created_at / updated_at / processed_atdatetime or nullTimestamps (processed_at is null until processing finishes).

The document is fully ready when ocr_status is completed (or skipped for non-OCR formats) and classification_status is completed or skipped.

Terminal window
curl "https://your-instance.delphos.app/v1/patients/pat_3Fq9Lm2Xb7Kd1Rs8Tn0Wp/prontuario-documents/aa5d11cb-4b8f-4da4-99cb-4e105708fa05" \
-H "x-api-key: YOUR_API_KEY"

Bulk: poll the patient’s document list

To reconcile many documents at once, list them — each item now carries classification_status and classification_confidence alongside the OCR ocr_document.status.

GET /v1/patients/{patient_id}/documents

Query parameters

ParameterTypeDefaultDescription
limitinteger20Page size (1–100).
offsetinteger0Pagination offset.
document_typestringFilter by document type.
date_fromdatetimeOnly documents created on/after this date.
date_todatetimeOnly documents created on/before this date.

Response

FieldTypeDescription
documentsarrayPage of patient-document records (see below).
totalintegerTotal matching documents.
limitintegerEcho of the requested page size.
offsetintegerEcho of the requested offset.

Each item in documents:

FieldTypeDescription
idUUIDThe patient ↔ document link ID (equals patient_document_id from upload).
patient_idUUIDThe patient UUID.
document_idUUIDThe OCR document ID (equals document_id from upload).
document_typestringCurrent type (final value once classification completes).
titlestring or nullTitle you supplied at upload.
notesstring or nullNotes you supplied at upload.
uploaded_byUUID or nullThe key/user that created the link.
is_activebooleanfalse after a soft-delete.
classification_statusstring or nullpending | completed | failed | skipped.
classification_confidencenumber or nullClassifier confidence (0–1) once complete.
created_atdatetimeCreation timestamp (UTC).
updated_atdatetimeLast update timestamp (UTC).
ocr_documentobject or nullLightweight OCR metadata (see below).

ocr_document summary (the OCR text itself is never returned here):

FieldTypeDescription
original_filenamestring or nullOriginal filename.
mime_typestring or nullStored MIME type.
statusstring or nullThe ocr_status value — poll this.
page_countinteger or nullPage count, when known.

Polling example

Terminal window
curl "https://your-instance.delphos.app/v1/patients/pat_3Fq9Lm2Xb7Kd1Rs8Tn0Wp/documents?limit=20" \
-H "x-api-key: YOUR_API_KEY"

Managing documents afterward

List a patient’s documents

Covered above under Poll for completion. The same endpoint is your general-purpose “list everything for this patient” call — filter with document_type, date_from, and date_to, and page with limit / offset.

POST /v1/patients/{patient_id}/documents

Use this when an OCR document already exists and you want to associate it with a patient (the upload endpoint already does this for you on new uploads). It is idempotent: linking the same document_id to the same patient twice returns the existing link rather than erroring.

Request body (application/json):

FieldTypeRequiredDefaultDescription
document_idUUIDYesAn existing OCR document to link.
document_typestringNootherOne of the document types below.
titlestringNonullMax 500 characters.
notesstringNonullFree-text notes.

Returns 201 with a single patient-document record (same shape as a list item). Returns 404 if the patient does not resolve for your tenant.

Removing a document

Removal is a soft-delete — the record’s is_active flag is set to false and the stored file and OCR record are retained for audit; nothing is hard-deleted. Use the document_id from the upload response.

DELETE /v1/patients/{patient_id}/prontuario-documents/{document_id}
  • 204 No Content — the document was soft-deleted.
  • 404 Not Found — no active document with that id exists for the patient (including a document you already deleted — the operation is idempotent in effect: the resource is gone either way).

Soft-deleted documents are excluded from GET /v1/patients/{patient_id}/documents by default (it returns only is_active=true records).

Terminal window
curl -X DELETE \
"https://your-instance.delphos.app/v1/patients/pat_3Fq9Lm2Xb7Kd1Rs8Tn0Wp/prontuario-documents/aa5d11cb-4b8f-4da4-99cb-4e105708fa05" \
-H "x-api-key: YOUR_API_KEY"

How a document reaches the Knowledge Graph

Uploading a document is not just storage — it is the entry point into DELPHOS’s clinical intelligence layer.

  1. OCR. After upload, DELPHOS Inteligência Documental extracts the full text of the document. On success ocr_status becomes completed.

  2. Classification. The extracted text is classified into a document_type (unless you supplied one).

  3. Knowledge Graph enrichment. Extracted clinical text is processed asynchronously by the Knowledge Graph pipeline, which uses the DELPHOS language model to identify entities (conditions, medications, allergies, lab results, procedures, symptoms, anthropometric measurements, and more) and the relationships between them, then generates semantic embeddings for retrieval.

Once a document’s data is in the Knowledge Graph, it surfaces through other DELPHOS endpoints:

SurfaceWhat it returns
GET /v1/patients/{patient_id}/contextAggregated clinical context — allergies, conditions, medications, lifestyle, social factors, recent consultations, recent lab alerts.
POST /v1/patients/{patient_id}/summaryAn LLM-generated clinical summary built from the graph.
GET /v1/patients/{patient_id}/lab-trendsTime-series lab-result trends (see below).
GET /v1/patients/{patient_id}/lab-trends

Lab values extracted from uploaded reports become lab_result entities in the Knowledge Graph, and this endpoint groups them by marker for charting.

ParameterTypeDefaultDescription
markersstringComma-separated marker names to filter. Empty tokens are ignored.
periodinteger12Look-back window in months (1–120).

Response:

FieldTypeDescription
patient_idUUIDThe patient.
period_monthsintegerEcho of the requested window.
markersarrayOne series per marker (name, unit, trend, reference range, data points).
total_resultsintegerTotal lab data points considered.
Terminal window
curl "https://your-instance.delphos.app/v1/patients/pat_3Fq9Lm2Xb7Kd1Rs8Tn0Wp/lab-trends?markers=Hemoglobina,Glicose&period=12" \
-H "x-api-key: YOUR_API_KEY"

Returns 422 if you request too many markers or pass invalid parameters, and 404 if the patient does not resolve for your tenant.


Error responses

CodeWhenBody / notes
400Empty file{ "detail": "Arquivo vazio." }
400Unsupported file type{ "detail": "Tipo de arquivo não suportado: <mime>. Tipos aceitos: PDF, PNG, JPG, DICOM." }
400File too large{ "detail": "Arquivo excede o limite de <N> MB." } (40 MB for PDF, 20 MB otherwise)
400Invalid document_type{ "detail": "Tipo de documento inválido: '<value>'. Tipos válidos: [...]" }
401Missing or invalid x-api-keyAuthentication failed.
413File too large (transport-level)The request body exceeded the accepted size before validation. Honor the size limits above.
404Patient not found for your tenantUniform "Not found" body — same for a nonexistent patient and a cross-tenant one.
422Invalid query/body parametersThe body lists the offending fields (e.g. on lab-trends).
500Unexpected server errorUniform internal-error body; no SQL or internal details are leaked.

End-to-end example (happy path)

Upload a lab report, poll until OCR completes, then read the resulting lab trends.

import time
import httpx
BASE = "https://your-instance.delphos.app/v1"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
PATIENT = "pat_3Fq9Lm2Xb7Kd1Rs8Tn0Wp"
# 1) Upload — returns 202 immediately
with open("hemograma.pdf", "rb") as fh:
upload = httpx.post(
f"{BASE}/patients/{PATIENT}/prontuario-documents",
headers=HEADERS,
files={"file": ("hemograma.pdf", fh, "application/pdf")},
data={"title": "Hemograma Completo — 2026-06-03"},
# document_type omitted -> automatic classification
)
upload.raise_for_status() # 202
body = upload.json()
patient_document_id = body["patient_document_id"]
print("accepted:", body["ocr_status"], body["classification_status"])
# 2) Poll the document list until OCR settles
def current(doc_id):
page = httpx.get(
f"{BASE}/patients/{PATIENT}/documents",
headers=HEADERS,
params={"limit": 100},
).json()
return next((d for d in page["documents"] if d["id"] == doc_id), None)
deadline = time.time() + 120
while time.time() < deadline:
doc = current(patient_document_id)
if doc is None:
raise RuntimeError("not_found")
ocr_status = (doc.get("ocr_document") or {}).get("status")
if ocr_status == "completed":
print("processed; final type:", doc["document_type"])
break
if ocr_status == "failed":
raise RuntimeError("OCR failed for this document")
time.sleep(3)
else:
raise TimeoutError("still processing")
# 3) Read graph-derived lab trends (available after processing)
trends = httpx.get(
f"{BASE}/patients/{PATIENT}/lab-trends",
headers=HEADERS,
params={"period": 12},
).json()
print("markers tracked:", [s["marker_name"] for s in trends["markers"]])