Document Service — API Contracts
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 05 api-design
1. Auth Model
| Header | Requirement |
|---|---|
Authorization: Bearer {JWT} | All endpoints; Keycloak OIDC |
Idempotency-Key: {uuid} | Required on: POST generate, POST publish, POST upload finalize |
X-Client-Mutation-Id: {uuid} | Optional; for offline queue replay |
tenantId always extracted from JWT. Never accepted from request body alone.
2. Template APIs
2.1 GET /v1/document-templates
List templates. Paginated (cursor-based).
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
category | string | No | Filter by document category |
status | string | No | draft | published | retired |
facilityId | string | No | Filter by facility scope |
origin | string | No | platform | tenant | all (default all) |
platformFormKey | string | No | Exact match on reference form key |
cursor | string | No | Pagination cursor |
limit | number | No | Default 20, max 100 |
Response 200:
{
"items": [
{
"id": "tmpl_01JRXXXX",
"name": "General Test Requisition",
"category": "lab_requisition",
"status": "published",
"origin": "platform",
"platformFormKey": "platform.dms.general-lab.general-test-requisition",
"currentVersionId": "tv_01JRXXXX",
"updatedAt": "2026-04-18T09:00:00Z"
}
],
"nextCursor": "eyJ..."
}
2.2 POST /v1/document-templates
Create a draft template.
Auth: DOCUMENT_AUTHOR or TENANT_ADMIN (requires ehr.documents.designer license)
Request:
{
"name": "Outpatient Prescription",
"category": "prescription",
"facilityId": null
}
Response 201:
{
"id": "tmpl_01JRXXXX",
"name": "Outpatient Prescription",
"category": "prescription",
"status": "draft",
"origin": "tenant",
"createdAt": "2026-04-18T09:00:00Z"
}
2.3 GET /v1/document-templates/:templateId
Template detail including version list metadata.
Response 200: Template object with versions[] array.
2.4 PATCH /v1/document-templates/:templateId
Update draft template metadata. Cannot update published templates.
Request: { "name"?: string, "category"?: string, "facilityId"?: string }
Response 200: Updated template object.
Errors: 409 if template is not in draft status.
2.5 POST /v1/document-templates/fork
Create a tenant-owned draft from a platform reference template.
Auth: DOCUMENT_AUTHOR (requires ehr.documents.designer license)
Request:
{ "platformFormKey": "platform.dms.general-lab.general-test-requisition" }
Response 201:
{
"templateId": "tmpl_01JRXXXX",
"status": "draft",
"forkedFromPlatformFormKey": "platform.dms.general-lab.general-test-requisition"
}
2.6 POST /v1/document-templates/:templateId/versions
Create a new draft version from a definition.
Request:
{
"definition": { "sections": [], "bindings": [] },
"semver": "1.0.0"
}
Response 201: Template version object with id, semver, checksum.
2.7 POST /v1/document-templates/versions/:versionId/publish
Publish a draft version (immutable after this).
Auth: DOCUMENT_AUTHOR (requires ehr.documents.designer license)
Headers: Idempotency-Key: {uuid}
Request:
{
"effectiveFrom": "2026-04-18",
"effectiveTo": null
}
Response 200: Published version object with publishedAt.
Errors: 409 VERSION_ALREADY_PUBLISHED if version already published.
3. Generation APIs
3.1 POST /v1/documents/generate
Synchronous PDF generation (p95 < 5 s).
Headers: Idempotency-Key: {uuid}
Request:
{
"templateVersionId": "tv_01JRXXXX",
"patientId": "pat_01JRXXXX",
"encounterId": "enc_01JRXXXX",
"context": {
"resourceType": "MedicationRequest",
"id": "medreq_01JRXXXX"
},
"locale": "ps-AF",
"clientMutationId": "uuid-v4"
}
Response 200:
{
"documentReferenceId": "DocRef/01JRXXXX",
"binaryId": "Binary/01JRXXXX",
"download": {
"url": "https://storage.ghasi-health.af/presigned/...",
"expiresAt": "2026-04-18T09:15:00Z"
}
}
Errors:
| Code | HTTP | Description |
|---|---|---|
TEMPLATE_VERSION_NOT_PUBLISHED | 422 | Version is draft or retired |
BINDING_RESOLUTION_FAILED | 422 | Required FHIR resource not found |
CONTEXT_RESOURCE_NOT_FOUND | 422 | Context FHIR resource missing |
MODULE_NOT_LICENSED | 403 | ehr.documents not active for tenant |
3.2 POST /v1/documents/render-jobs
Enqueue asynchronous render job.
Request: Same body as POST /v1/documents/generate.
Response 202:
{ "jobId": "rjob_01JRXXXX", "status": "queued" }
3.3 GET /v1/documents/render-jobs/:jobId
Poll render job status.
Response 200:
{
"jobId": "rjob_01JRXXXX",
"status": "completed",
"documentReferenceId": "DocRef/01JRXXXX",
"binaryId": "Binary/01JRXXXX",
"durationMs": 3420
}
Status values: queued | running | completed | failed
On failure: includes errorCode and message (no PHI in message).
4. Artifact APIs
4.1 GET /v1/documents
Search documents by patient.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
patientId | string | Yes | — |
encounterId | string | No | Filter by encounter |
category | string | No | Filter by document category |
dateFrom | string (ISO-8601) | No | — |
dateTo | string (ISO-8601) | No | — |
cursor | string | No | Pagination cursor |
limit | number | No | Default 20 |
Response 200:
{
"items": [
{
"documentReferenceId": "DocRef/01JRXXXX",
"name": "General Test Requisition",
"category": "lab_requisition",
"date": "2026-04-18",
"author": "Dr. Bilal Wardak",
"downloadUrl": "https://..."
}
],
"nextCursor": "eyJ..."
}
4.2 GET /v1/documents/:artifactId/download
Returns a presigned download URL (default TTL 15 min) or streams bytes depending on deployment policy.
Response 200:
{
"url": "https://storage.ghasi-health.af/presigned/...",
"expiresAt": "2026-04-18T09:15:00Z",
"contentType": "application/pdf"
}
Audit record emitted on every call (action=download).
4.3 POST /v1/documents/uploads
Initiate upload. Returns staged upload URL for large files or accepts multipart body.
Request: multipart/form-data with file field, OR
{
"filename": "scan-referral-letter.pdf",
"contentType": "application/pdf",
"patientId": "pat_01JRXXXX",
"encounterId": "enc_01JRXXXX",
"category": "referral"
}
Response 201:
{
"uploadId": "upl_01JRXXXX",
"stagedUploadUrl": "https://storage.ghasi-health.af/staged/...",
"expiresAt": "2026-04-18T09:10:00Z"
}
4.4 POST /v1/documents/uploads/:uploadId/complete
Finalize upload after blob written to staged URL. Triggers virus scan.
Headers: Idempotency-Key: {uuid}
Response 201 (virus clean):
{
"documentReferenceId": "DocRef/01JRXXXX",
"status": "registered"
}
Response 422 (virus detected):
{
"error": {
"code": "VIRUS_DETECTED",
"message": "Upload failed virus scan and has been quarantined."
}
}
5. FHIR REST (via interop-service / FHIR Gateway)
Clients use fhir-gateway for standard FHIR interactions:
| Interaction | FHIR URL |
|---|---|
| Search documents | GET /fhir/R4/DocumentReference?patient=Patient/{id} |
| Read binary metadata | GET /fhir/R4/Binary/{id} |
| Search by type | GET /fhir/R4/DocumentReference?type={loinc-code} |
Direct FHIR create via POST /fhir/R4/DocumentReference is supported for rare system integrations; prefer the REST generate flow for standard use.
6. Standard Error Envelope
{
"success": false,
"error": {
"code": "BINDING_RESOLUTION_FAILED",
"message": "Human-readable message",
"details": { "bindingPath": "Patient.name" }
},
"correlationId": "req_doc_xxx",
"timestamp": "2026-04-18T09:00:00Z"
}
7. Service-Specific Error Codes
| Code | HTTP | Description |
|---|---|---|
MODULE_NOT_LICENSED | 403 | ehr.documents license not active for tenant |
DESIGNER_LICENSE_REQUIRED | 403 | ehr.documents.designer required for authoring |
TEMPLATE_NOT_FOUND | 404 | Template ID not found for tenant |
TEMPLATE_VERSION_NOT_PUBLISHED | 422 | Version is draft or retired |
TEMPLATE_IMMUTABLE | 409 | Attempt to edit a published version |
PLATFORM_TEMPLATE_IMMUTABLE | 403 | Attempt to edit a platform reference template |
BINDING_RESOLUTION_FAILED | 422 | FHIR binding could not be resolved |
CONTEXT_RESOURCE_NOT_FOUND | 422 | Required context FHIR resource not found |
VERSION_ALREADY_PUBLISHED | 409 | Version already in published state |
CIRCULAR_SUPERSESSION | 409 | Supersession would create a circular reference chain |
VIRUS_DETECTED | 422 | Upload failed virus scan; quarantined |
UPLOAD_NOT_FOUND | 404 | Upload ID not found |
RENDER_JOB_NOT_FOUND | 404 | Job ID not found |