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 feature

This 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:

  1. Start the job: POST /Group/{upid}/$export returns 202 and a status URL.
  2. Poll it: GET /$export-status/{job_id} returns 202 while the job is running and 200 when it is done.
  3. Download the results: Request every output URL in the manifest.

2. Access

Environments

EnvironmentBase URL
Sandboxhttps://fqs.sandbox.zusapi.com
Productionhttps://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.

ParameterMeaningRules
_typeResource types to includeComma-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).
_sinceMinimum update timeRFC 3339 date-time, such as 2026-05-01T00:00:00Z. Date-only values are rejected. See section 6 for how recent writes are handled.
includeAssociatedDataInline document contentCurrently, 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

StatusMeaningBody
202 AcceptedJob 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 RequestInvalid parameterOperationOutcome with code invalid. Diagnostics explains the problem, and expression identifies the parameter. No job is created.
401 / 403Missing or invalid token, or missing permissionError document
404 Not FoundUPID unknown or not accessible to youOperationOutcome, "no Group resource matches the requested upid"
409 ConflictAn export is already running for this patient and builderPoll the job identified by the header: Content-Location.
501 Not ImplementedBuilder not enrolled in the betaOperationOutcome, code: not-supported
500Could not create or queue the jobOperationOutcome. Retry the request.
📘

Do not use _type=Binary to request documents.

The export does not emit Binary resources; document content is delivered through DocumentReference. Use includeAssociatedData=_inlineAttachmentData instead. We accept _type=Binary, but it produces no output and returns an informational notice.

4. Poll: 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": []
}
  • transactionTime marks when the job finished.
  • request is the original start URL without its query string.
  • requiresAccessToken is 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=…

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 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)

{"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)

{"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"}}]}
  • data is the base64-encoded document - the same bytes returned by GET /Binary/{id}.
  • size is the decoded byte count. It may be absent, so do not depend on it.
  • url remains 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.

SituationWhat you seeWhat to do
The stored document is larger than 64 MBNo data; url presentGET url
The job has already attempted to inline 5,000 documentsNo data; url presentGET url
The document is restricted by access controlsNo data; url presentGET url; it applies the same access rules
The document could not be retrieved at export timeNo data; url presentGET url; retry later if that fails
The stored DocumentReference already carried inline dataIts stored data, unchangedDecode 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

LimitValueNote
Concurrent exports per patient1 per builderA second request returns 409 with the running job's URL
Inline documents per job5,000Additional attachments include url only
Inline document size64 MB stored (≈48 MB decoded)Larger documents include url only
Job runtime15 minutes maximumMost exports complete within 5 minutes. If a job exceeds the limit, it fails with code transient; start a new export.
Download link validity15 minutesCall the output URL again for a new link
Output retention7 daysStart a new export after this period
Poll intervalRetry-After, 5–60 sUse Retry-After
Request rate4,000 requests / minute / IPEdge 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


Did this page help you?