Bulk FHIR Export
Export a patient's full FHIR record as NDJSON files by UPID with the beta Group $export operation, including inline documents.
Beta featureThis feature is currently in private beta and may change or experience performance issues at any time.
Customers not enrolled in the private beta who attempt to call the endpoint will receive a 501 response stating "$export submission is not yet generally available". To request access, please reach out to your Zus account manager.
1. Overview
Zus implements the FHIR Bulk Data Group export operation using the patient's Universal Patient Identifier (UPID). The result is a set of NDJSON files containing the supported patient resources and the practitioners, organizations, locations, and medications they reference.
To include inline binary data in exported DocumentReferences, set includeAssociatedData=_inlineAttachmentData.
There are three steps to using the API:
- Start the job: POST /Group/{upid}/$export returns 202 and a status URL.
- Poll it: GET /$export-status/{job_id} returns 202 while the job is running and 200 when it is done.
- Download the results: Request every output URL in the manifest.
2. Access
Environments
| Environment | Base URL |
|---|---|
| Sandbox | https://fqs.sandbox.zusapi.com |
| Production | https://fqs.zusapi.com |
Authentication and authorization
Every request, including status checks and downloads, needs Authorization: Bearer <token>. The token needs the FHIR Group read permission and access to the requested UPID. We return 404 for both unknown and inaccessible UPIDs.
An export belongs to the builder credentials that started it, so use those credentials for status and download requests unless you have a cross-builder grant.
3. Start: POST /Group/{upid}/$export
POST https://fqs.zusapi.com/Group/00112233-4455-6677-8899-aabbccddeeff/$export?includeAssociatedData=_inlineAttachmentData
Authorization: Bearer <token>
There is no request body. You do not need Prefer: respond-async; the operation is asynchronous either way. Send each query parameter at most once.
| Parameter | Meaning | Rules |
|---|---|---|
_type | Resource types to include | Comma-separated, case-sensitive FHIR resource types. Unknown types match nothing but do not cause an error. Omit this parameter to export all supported types (section 6). |
_since | Minimum update time | RFC 3339 date-time, such as 2026-05-01T00:00:00Z. Date-only values are rejected. See section 6 for how recent writes are handled. |
includeAssociatedData | Inline document content | Currently, the only supported value is _inlineAttachmentData. Any other value returns 400. |
One quirk: _inlineAttachmentData needs DocumentReference because that is where the document data lives. If _type omits DocumentReference, we add it and explain the change in an informational OperationOutcome. Other unknown query parameters are ignored.
Responses
| Status | Meaning | Body |
|---|---|---|
202 Accepted | Job created. Poll the URL in the Content-Location header. | Usually empty. When we adjust or ignore a parameter, the response contains an informational OperationOutcome explaining what happened. |
400 Bad Request | Invalid parameter | OperationOutcome with code invalid. Diagnostics explains the problem, and expression identifies the parameter. No job is created. |
401 / 403 | Missing or invalid token, or missing permission | Error document |
404 Not Found | UPID unknown or not accessible to you | OperationOutcome, "no Group resource matches the requested upid" |
409 Conflict | An export is already running for this patient and builder | Poll the job identified by the header: Content-Location. |
501 Not Implemented | Builder not enrolled in the beta | OperationOutcome, code: not-supported |
500 | Could not create or queue the job | OperationOutcome. Retry the request. |
Do not use_type=Binaryto request documents.The export does not emit Binary resources; document content is delivered through DocumentReference. Use
includeAssociatedData=_inlineAttachmentDatainstead. We accept_type=Binary, but it produces no output and returns an informational notice.
4. Poll: GET /$export-status/{job_id}
GET /$export-status/{job_id}Use the Content-Location URL from the start response. The job is scoped to the builder credentials that created it.
202 Accepted- The job is still running. Retry-After tells you how many seconds to wait; it increases from 5 to 60 as the job ages.200 OK- The job is finished. The JSON body is the manifest. Check error[] before processing output[].404- The job does not exist or belongs to another builder.
A 5xx here is a polling failure, not a terminal result for the export. Retry the poll.
Manifest - completed job
{
"transactionTime": "2026-09-03T15:04:05Z",
"request": "https://fqs.zusapi.com/Group/00112233-4455-6677-8899-aabbccddeeff/$export",
"requiresAccessToken": true,
"output": [
{ "type": "Patient", "url": "https://fqs.zusapi.com/$export-output/<job_id>?key=<builder_id>%2F<job_id>%2FPatient.ndjson" },
{ "type": "Observation", "url": "https://fqs.zusapi.com/$export-output/<job_id>?key=<builder_id>%2F<job_id>%2FObservation.ndjson" },
{ "type": "Observation", "url": "https://fqs.zusapi.com/$export-output/<job_id>?key=<builder_id>%2F<job_id>%2FObservation-fqsdelta.ndjson" },
{ "type": "DocumentReference", "url": "https://fqs.zusapi.com/$export-output/<job_id>?key=<builder_id>%2F<job_id>%2FDocumentReference.ndjson" },
{ "type": "Practitioner", "url": "https://fqs.zusapi.com/$export-output/<job_id>?key=<builder_id>%2F<job_id>%2FPractitioner.ndjson" }
],
"error": []
}transactionTimemarks when the job finished.requestis the original start URL without its query string.requiresAccessTokenis always true. Authenticate every output request.- Do not assume one file per resource type. A type may have both a main file and a -fqsdelta file containing recent writes. Download every output entry and combine files by the manifest's type field, not by filename.
- The manifest does not include count, deleted, or extension. Successful jobs have an empty error array.
Manifest - failed job
Failed jobs also return 200. In that case, output is empty and error contains one entry:
{
"transactionTime": "2026-09-03T15:09:41Z",
"request": "https://fqs.zusapi.com/Group/00112233-4455-6677-8899-aabbccddeeff/$export",
"requiresAccessToken": true,
"output": [],
"error": [
{ "type": "OperationOutcome", "url": "https://fqs.zusapi.com/$export-output/<job_id>" }
]
}The error URL returns one NDJSON line:
{"resourceType":"OperationOutcome","issue":[{"severity":"error","code":"transient","diagnostics":"export failed while exporting patient compartment resources due to a temporary service issue; please retry"}]}Note: If issue.code is transient, start a new export with backoff. If it is processing, retry once and contact Zus if the error happens again. Do not parse diagnostics. Once a job reaches 200, you can start another export for the patient.
5. Download: GET /$export-output/{job_id}?key=…
GET /$export-output/{job_id}?key=…Call each output URL with your token. It returns a 302 redirect to a pre-signed link that is valid for 15 minutes. If that link expires, call the output URL again. We retain output files for 7 days. A modified key returns 400; asking for output before the job finishes returns 404.
File format
- Each file is newline-delimited JSON (Content-Type: application/x-ndjson): one complete FHIR resource per line, with no enclosing array.
- Files are not compressed and do not include Content-Encoding.
- Resource order is not guaranteed, either within a file or across files.
- Compare parsed JSON, not raw bytes. Export may change key order or escaping without changing the resource itself, so byte-for-byte checksums are not reliable.
- Inline documents can make a single line tens of megabytes long. Ensure whatever decoder you are using can handle documents of this size.
6. Export contents
The snapshot time is when you start the export. Resources written afterward may or may not be included.
Patient-compartment resources
The export supports these patient-compartment resource types:
AllergyIntolerance, Appointment, CarePlan, CareTeam, Claim, Condition, Consent, Coverage, Device, DeviceUseStatement, DiagnosticReport, DocumentReference, Encounter, EpisodeOfCare, FamilyMemberHistory, Goal, Immunization, MeasureReport, MedicationAdministration, MedicationDispense, MedicationRequest, MedicationStatement, Observation, Patient, Procedure, RelatedPerson.
Foundational resources
To keep exported references usable, we also include directly referenced Practitioner, Organization, Location, and Medication resources. We do not follow references from one foundational resource to another. These resources are currently excluded when _type is set and doesn't list them. Binary-backed attachments on foundational resources, such as Practitioner.photo, are removed.
_since and recent writes
_since and recent writes_since normally filters on meta.lastUpdated. There is one exception: we always include resources written during the 15 minutes before the request. Those resources may arrive in a separate -fqsdelta file. If the same resource appears in both paths, only the newest version is exported.
Not included
- Deleted resources - omitted from both the output and the manifest.
- Restricted-access data - silently excluded when security labels mark it as specially regulated or as restricted/very restricted.
- Binary resources - never emitted. Document content is delivered through DocumentReference (section 7).
- Documents without a DocumentReference - not included. This affects a small fraction of stored binaries. A manifest count is planned but not yet available.
- DiagnosticReport.presentedForm - includes its URL but is not inlined. Inline delivery applies only to DocumentReference.content.attachment.
7. Document delivery
Every exported DocumentReference has an absolute content.attachment.url, regardless of delivery mode. Fetch it with your token. includeAssociatedData only determines whether the attachment also has inline data.
Default mode (no includeAssociatedData)
includeAssociatedData){"resourceType":"DocumentReference","id":"d-123","status":"current","subject":{"reference":"Patient/p-1"},
"content":[{"attachment":{"contentType":"application/pdf","url":"https://fqs.zusapi.com/Binary/b-456"}}]}Fetch the document with GET attachment.url.
Inline mode (includeAssociatedData=_inlineAttachmentData)
includeAssociatedData=_inlineAttachmentData){"resourceType":"DocumentReference","id":"d-123","status":"current","subject":{"reference":"Patient/p-1"},
"content":[{"attachment":{"contentType":"application/pdf","data":"JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoK...","size":48213,"url":"https://fqs.zusapi.com/Binary/b-456"}}]}datais the base64-encoded document - the same bytes returned by GET /Binary/{id}.sizeis the decoded byte count. It may be absent, so do not depend on it.urlremains present and absolute.- Inline mode writes all DocumentReference resources to one file.
Note: Your client only needs one process: decode attachment.data when it is present; otherwise, fetch attachment.url.
When inline data is unavailable
We handle inline delivery one document at a time. If we cannot inline an attachment, we keep its url and continue the export.
| Situation | What you see | What to do |
|---|---|---|
| The stored document is larger than 64 MB | No data; url present | GET url |
| The job has already attempted to inline 5,000 documents | No data; url present | GET url |
| The document is restricted by access controls | No data; url present | GET url; it applies the same access rules |
| The document could not be retrieved at export time | No data; url present | GET url; retry later if that fails |
The stored DocumentReference already carried inline data | Its stored data, unchanged | Decode as usual |
Note: Do not treat missing data as a missing document: fetch attachment.url instead. Per-document errors are not yet included in the manifest.
8. Limits
| Limit | Value | Note |
|---|---|---|
| Concurrent exports per patient | 1 per builder | A second request returns 409 with the running job's URL |
| Inline documents per job | 5,000 | Additional attachments include url only |
| Inline document size | 64 MB stored (≈48 MB decoded) | Larger documents include url only |
| Job runtime | 15 minutes maximum | Most exports complete within 5 minutes. If a job exceeds the limit, it fails with code transient; start a new export. |
| Download link validity | 15 minutes | Call the output URL again for a new link |
| Output retention | 7 days | Start a new export after this period |
| Poll interval | Retry-After, 5–60 s | Use Retry-After |
| Request rate | 4,000 requests / minute / IP | Edge rate limit shared across all endpoints on the host. |
| Throughput limits | ~100 requests / minute. This will become clearer as beta users begin using it. | Beta throughput will be quite limited, but we intend to raise this in the future. |
The export contains only data the requesting builder can read. Jobs cannot be canceled once started.
9. Beta limitations and roadmap
- Per-document reporting. We plan to report attachments that could not be inlined or retrieved, along with a count of documents that have no DocumentReference. For now, fetch attachment.url whenever attachment.data is absent.
- Compression and content type. We plan to move output to gzip with Content-Type: application/fhir+ndjson, following the Bulk Data IG. We will announce the change in advance. Customers should send Accept-Encoding: gzip now.
- OperationDefinition and CapabilityStatement. We still need to publish _inlineAttachmentData formally in both.
- Change alerts. A later phase will notify you when a UPID needs to be exported again. Timing is not yet confirmed.
- General availability and self-service enrollment are still to come.
10. Implementation checklist
- Use fqs.zusapi.com or fqs.sandbox.zusapi.com, not the FHIR proxy host
- Authenticate every request, including status checks and downloads
- On 409, continue with the job in Content-Location
- On 202, wait for Retry-After; on 200, check error[]
- Download every output entry, even when a resource type repeats, and follow the 302
- Stream the NDJSON; do not impose a fixed line-size limit
- Decode attachment.data when present; otherwise fetch attachment.url
- For transient errors, start a new export with backoff; for processing errors, retry once, then contact Zus if the error returns
Appendix A - OpenAPI specification
openapi: 3.0.4
info:
title: Zus FHIR Bulk $export (Group level)
version: 2.0.0-beta
description: |-
Group-level FHIR Bulk Data export keyed on the Zus Universal Patient Identifier (UPID).
Based on the FHIR Bulk Data Access IG (STU2): Group Level Export and the asynchronous request pattern.
The operation is always asynchronous; the Prefer header is not required.
servers:
- url: https://fqs.sandbox.zusapi.com
description: Sandbox
- url: https://fqs.zusapi.com
description: Production
security:
- bearerAuth: []
paths:
/Group/{upid}/$export:
post:
summary: Kick off an export of a patient's chart
operationId: kickoffGroupExport
parameters:
- name: upid
in: path
required: true
schema: { type: string }
description: Universal Patient Identifier of the patient to export
- name: _type
in: query
required: false
schema: { type: string }
description: Comma-separated FHIR resource types to include. Case-sensitive. Unrecognized types narrow the export. Default is all supported types.
- name: _since
in: query
required: false
schema: { type: string, format: date-time }
description: Include only resources updated after this RFC 3339 instant. Resources written within 15 minutes before kick-off are always included.
- name: includeAssociatedData
in: query
required: false
schema:
type: string
enum: [_inlineAttachmentData]
description: _inlineAttachmentData inlines document bytes as base64 in DocumentReference.content.attachment.data alongside an absolute url. Any other value is rejected.
responses:
'202':
description: Accepted. Content-Location is the status URL. Body is empty, or an OperationOutcome of severity "information" describing parameter interpretation.
headers:
Content-Location:
schema: { type: string, format: uri }
content:
application/fhir+json:
schema: { $ref: '#/components/schemas/OperationOutcome' }
'400':
description: Invalid parameter. No job is created. issue.expression names the parameter.
content:
application/fhir+json:
schema: { $ref: '#/components/schemas/OperationOutcome' }
'401': { description: Missing or invalid token }
'403': { description: Token lacks the fhir Group read permission }
'404':
description: UPID unknown or not accessible to the caller
content:
application/fhir+json:
schema: { $ref: '#/components/schemas/OperationOutcome' }
'409':
description: An export is already in flight for this patient and builder. Content-Location points at its status URL.
headers:
Content-Location:
schema: { type: string, format: uri }
content:
application/fhir+json:
schema: { $ref: '#/components/schemas/OperationOutcome' }
'501':
description: Builder not enrolled in the beta
content:
application/fhir+json:
schema: { $ref: '#/components/schemas/OperationOutcome' }
/$export-status/{job_id}:
get:
summary: Poll an export job
operationId: getExportStatus
parameters:
- name: job_id
in: path
required: true
schema: { type: string }
responses:
'202':
description: Still running. Empty body.
headers:
Retry-After:
schema: { type: integer }
description: Seconds to wait before polling again (5–60)
'200':
description: Finished. Inspect error[] to distinguish success from failure.
content:
application/json:
schema: { $ref: '#/components/schemas/Manifest' }
'404': { description: Unknown job, or job owned by another builder }
/$export-output/{job_id}:
get:
summary: Download one output file
operationId: getExportOutput
parameters:
- name: job_id
in: path
required: true
schema: { type: string }
- name: key
in: query
required: true
schema: { type: string }
description: Exactly as given in the manifest output url. Required for completed jobs.
responses:
'302':
description: Redirect to a pre-signed file URL valid for 15 minutes. Files are application/x-ndjson, uncompressed.
headers:
Location:
schema: { type: string, format: uri }
'200':
description: For a FAILED job only — one NDJSON line containing an OperationOutcome (issue.code transient or processing).
content:
application/x-ndjson:
schema: { $ref: '#/components/schemas/OperationOutcome' }
'400': { description: Missing or unknown key }
'404': { description: Unknown job, job still running, or job owned by another builder }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Manifest:
type: object
required: [transactionTime, request, requiresAccessToken, output, error]
properties:
transactionTime:
type: string
format: date-time
description: When the job finished
request:
type: string
format: uri
description: The kick-off URL, without query string
requiresAccessToken:
type: boolean
description: Always true
output:
type: array
description: One entry per output file. A resource type may appear more than once. Empty on a failed job.
items:
type: object
required: [type, url]
properties:
type: { type: string, description: FHIR resource type in the file }
url: { type: string, format: uri }
error:
type: array
description: Empty on success. On failure, one entry of type OperationOutcome whose url serves the failure detail.
items:
type: object
required: [type, url]
properties:
type: { type: string, enum: [OperationOutcome] }
url: { type: string, format: uri }
OperationOutcome:
type: object
required: [resourceType, issue]
properties:
resourceType: { type: string, enum: [OperationOutcome] }
issue:
type: array
minItems: 1
items:
type: object
required: [severity, code]
properties:
severity:
type: string
enum: [fatal, error, warning, information]
code:
type: string
description: FHIR issue-type. Kick-off rejections use invalid; failed jobs use transient or processing.
diagnostics:
type: string
description: Human-readable text. Not a stable contract; do not parse.
expression:
type: array
items: { type: string }
description: Parameter(s) the issue concerns, e.g. ["_type"]. Present on kick-off 400s and notices.
Related pages
Updated 9 days ago