Skip to main content

Bulk Export

The Bulk Export system implements the FHIR Bulk Data Access specification, enabling you to export large datasets asynchronously in NDJSON format. It is ideal for data migrations, analytics pipelines, compliance reporting, and population health management.

How Bulk Export Works

Bulk exports are asynchronous. The flow is:

  1. Kick off an export — receive a 202 Accepted with a polling URL
  2. Poll the status URL until the export is complete (200 OK)
  3. Download the output files from the provided URLs
  4. Delete the export job when done

System-Level Export

Export all resources across your organization:

curl "https://api.esus.health/fhir/\$export" \
  -H "Authorization: Bearer TOKEN" \
  -H "Accept: application/fhir+json" \
  -H "Prefer: respond-async"

Response: 202 Accepted

Content-Location: https://api.esus.health/bulk-export/aa11bb22-cc33-dd44-ee55-ff6677889900

Filter by Resource Type

curl "https://api.esus.health/fhir/\$export?_type=Patient,Observation,Condition" \
  -H "Authorization: Bearer TOKEN" \
  -H "Prefer: respond-async"

Filter by Date

# Only resources modified since a specific date
curl "https://api.esus.health/fhir/\$export?_since=2024-01-01T00:00:00Z" \
  -H "Authorization: Bearer TOKEN" \
  -H "Prefer: respond-async"

Patient-Level Export

Export all resources for all patients in your organization:

curl "https://api.esus.health/fhir/Patient/\$export" \
  -H "Authorization: Bearer TOKEN" \
  -H "Prefer: respond-async"

Check Export Status

Poll the Content-Location URL to check progress:

curl https://api.esus.health/bulk-export/aa11bb22-cc33-dd44-ee55-ff6677889900 \
  -H "Authorization: Bearer TOKEN"

In Progress — 202 Accepted

{
  "status": "in-progress",
  "progress": "Export is being processed"
}

The progress field is a descriptive status string (not a percentage). The response also carries the same message in the X-Progress header.

Complete — 200 OK

{
  "transactionTime": "2024-01-15T10:30:00Z",
  "request": "https://api.esus.health/fhir/$export",
  "requiresAccessToken": true,
  "output": [
    {
      "type": "Patient",
      "url": "/bulk-export/aa11bb22-cc33-dd44-ee55-ff6677889900/download/Patient",
      "count": 1240
    },
    {
      "type": "Observation",
      "url": "/bulk-export/aa11bb22-cc33-dd44-ee55-ff6677889900/download/Observation",
      "count": 5820
    },
    {
      "type": "Condition",
      "url": "/bulk-export/aa11bb22-cc33-dd44-ee55-ff6677889900/download/Condition",
      "count": 980
    }
  ],
  "error": []
}

Download Export Files

Each manifest output entry exposes a URL of the form /bulk-export/<id>/download/<ResourceType>. Calling it returns a short-lived presigned URL (valid for 900 seconds) pointing at the NDJSON file — raw storage keys are never surfaced:

curl "https://api.esus.health/bulk-export/aa11bb22-cc33-dd44-ee55-ff6677889900/download/Patient" \
  -H "Authorization: Bearer TOKEN"

Response:

{
  "url": "https://storage.esus.health/bulk-exports/...?signature=...",
  "count": 1240,
  "expiresInSeconds": 900
}

Then download the NDJSON from the presigned URL:

curl "https://storage.esus.health/bulk-exports/...?signature=..." -o patients.ndjson

Output files are in NDJSON (newline-delimited JSON) format — one FHIR resource per line.

Sample patients.ndjson:

{"resourceType":"Patient","id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","meta":{"versionId":"3","lastUpdated":"2024-01-10T08:00:00Z"},"name":[{"family":"Smith","given":["John"]}],"birthDate":"1985-03-15"}
{"resourceType":"Patient","id":"bb22cc33-dd44-ee55-ff66-778899001122","meta":{"versionId":"1","lastUpdated":"2024-01-12T14:00:00Z"},"name":[{"family":"Doe","given":["Jane"]}],"birthDate":"1990-07-22"}

Cancel an Export

If you no longer need an export or want to free up resources:

curl -X DELETE https://api.esus.health/bulk-export/aa11bb22-cc33-dd44-ee55-ff6677889900 \
  -H "Authorization: Bearer TOKEN"

Returns 204 No Content on success.

Processing NDJSON in Common Languages

Node.js

import { createReadStream } from 'fs'
import { createInterface } from 'readline'

const rl = createInterface({ input: createReadStream('patients.ndjson') })

rl.on('line', (line) => {
  const patient = JSON.parse(line)
  console.log(patient.id, patient.name?.[0]?.family)
})

Python

import json

with open('patients.ndjson') as f:
    for line in f:
        patient = json.loads(line)
        print(patient['id'], patient.get('name', [{}])[0].get('family'))

Use Cases

Use CaseRecommended Export
Full data migrationSystem-level $export without filters
Incremental syncSystem-level $export?_since=<last-sync-date>
Analytics on clinical data$export?_type=Patient,Observation,Condition
Compliance audit export$export?_type=AuditEvent&_since=<period-start>
Population healthPatient-level Patient/$export