In a hurry? eClinicalWorks (eCW) is one of the largest US ambulatory EHRs, used in tens of thousands of physician practices. Under the ONC 21st Century Cures Act, eCW is required to expose a FHIR R4 Bulk Data $export endpoint that lets authorized clients pull full-population data (patients, encounters, observations, conditions, medications, etc.) as NDJSON files. This is the single most efficient way to ingest large volumes of eCW data for analytics, ACO/value-based care reporting, AI training, and population health workflows. The flow is async: kick off the export, poll for completion, download NDJSON files, parse them. This guide walks through the full implementation pattern using Mirth Connect as the integration layer, the production gotchas that trip teams up, and the operational discipline that keeps multi-tenant exports reliable.
If you're integrating with eClinicalWorks for analytics or population health, FHIR Bulk Data $export is almost certainly the right path — and it's measurably underused. Most healthtech teams default to per-patient FHIR API calls (slow, expensive, rate-limited), miss the existence of $export, or attempt it without understanding the async pattern. The result: integrations that would take a weekend with $export end up taking weeks of looped per-patient calls.
This guide is the practitioner playbook we use with clients building analytics, ACO, or AI workloads on top of eCW data. It covers the FHIR Bulk Data IG pattern, eCW-specific behavior, a complete Mirth Connect support implementation, and the operational practices that keep the system reliable at scale. For broader context, pair this with our FHIR R4 integration complete guide, the HL7 vs FHIR comparison, our FHIR Bulk Data implementation guide, and our productized eClinicalWorks FHIR Export and FHIR R4 adapter services.
1. What FHIR Bulk Data Export Is and Why It Exists
The FHIR R4 Bulk Data Access Implementation Guide (also called Flat FHIR) defines a standardized way to extract large amounts of FHIR data from an EHR or other FHIR server. Instead of asking for one Patient at a time via individual GET /Patient/{id} calls — which is fine for a UI rendering one chart but disastrous when you need 50,000 patients — you ask for an export of the whole population at once.
The mechanism is async by design:
- Client requests
$exportwith parameters (which resource types, since when, etc.). - Server returns a
202 Acceptedwith a polling URL. - Client polls the URL periodically; server responds
202while the export is being prepared. - When ready, server responds
200 OKwith a manifest listing one or more NDJSON file URLs. - Client downloads the NDJSON files; each line is one FHIR resource.
This pattern was mandated by the ONC 21st Century Cures Act — every certified US EHR has to support $export for at least the system-level case to satisfy interoperability requirements. eClinicalWorks supports it as part of its certified FHIR R4 implementation.
For a deeper explanation of FHIR Bulk Data including the spec details, see our FHIR Bulk Data implementation guide. This article focuses on the eCW-specific application.
2. eClinicalWorks's FHIR Bulk Data Surface
eClinicalWorks exposes FHIR R4 endpoints per US Core Implementation Guide, with $export available for authorized client applications.
What you'll find at the eCW FHIR endpoint
- FHIR R4 base URL per practice / tenant. The exact URL pattern is published on eCW's FHIR developer portal — registration is required.
- Capability statement at
[base]/metadatalisting supported resources, operations, and search parameters. $exportoperation at three levels (system, group, patient — see Section 3).- OAuth 2.0 / SMART Backend Services authentication.
- Sandbox environment for development and testing — eCW provides this; verify the current URL and registration steps on the developer portal.
Supported resource types
eCW's $export typically supports the major US Core resources:
Patient,Practitioner,Organization,LocationEncounter,AppointmentCondition,Observation,Procedure,DiagnosticReportAllergyIntolerance,ImmunizationMedication,MedicationRequest,MedicationDispense,MedicationStatementDocumentReference,CarePlan,CareTeam,Goal
The exact list at any specific eCW practice depends on what that practice has enabled, the eCW version they're running, and any custom configurations. Always check the practice's actual capability statement rather than assuming.
Rate limits and quotas
eCW enforces rate limits on FHIR requests including $export calls. The specific limits are not consistently documented and can change. Plan for:
- Polling intervals of 30+ seconds to avoid tripping rate limits during long-running exports.
- Concurrent export ceilings — running multiple exports for the same tenant simultaneously may be throttled.
- Practice-level quotas — high-volume integrations should coordinate with eCW for elevated limits.
For commercial deployments at scale, engage eCW's developer relations / vendor services team early — quotas suitable for small pilots may not work for production analytics workloads.
3. The Three Export Levels: Patient, Group, System
The FHIR Bulk Data spec defines three $export levels, and eCW supports them with different availability per practice.
3.1 System-level export
GET [base]/$exportExports all data the authorized client has access to— typically the entire practice's patient population. This is the most common level for analytics and population-health use cases.
3.2 Group-level export
GET [base]/Group/[group-id]/$exportExports data for a specific patient group defined in the EHR — for example, a cohort of patients with diabetes, a specific care-plan population, or an ACO attribution list. eCW supports group-level export with practices that have configured groups.
3.3 Patient-level export
GET [base]/Patient/$exportExports data for all patients in the eCW system, scoped by what the authorized client has access to. Subtle difference from system-level: typically narrower in some implementations, especially for what non-clinical resources are included.
Which to use
- For analytics across an entire practice: System-level (
$export). - For ACO or specific cohort workflows: Group-level (when groups are configured).
- For patient-only data without practice-wide non-clinical data: Patient-level.
In practice, most healthtech teams use system-levelfor first integrations, then move to group-level once they've identified specific cohorts that don't need the full population data flow.
4. The Complete Async Workflow
The full request/response sequence:
Step 1 — Kickoff request
GET https://[ecw-fhir-base]/$export
Accept: application/fhir+json
Prefer: respond-async
Authorization: Bearer [access-token]Optional query parameters:
_type=Patient,Encounter,Condition,Observation— limit to specific resource types_since=2026-01-01T00:00:00Z— only resources modified since this time_outputFormat=application/fhir+ndjson— NDJSON is the standard format
Step 2 — Server response (kickoff accepted)
HTTP/1.1 202 Accepted
Content-Location: https://[ecw-fhir-base]/$export/status/[job-id]The Content-Location header gives you the polling URLfor this specific export job. Save it; it's how you check progress.
Step 3 — Poll for completion
GET https://[ecw-fhir-base]/$export/status/[job-id]
Authorization: Bearer [access-token]While the export is processing:
HTTP/1.1 202 Accepted
X-Progress: in-progress
Retry-After: 60Retry-Aftertells you how long to wait before polling again. Respect it — failing to do so produces rate-limit errors and doesn't make the export complete faster.
Step 4 — Server response (export complete)
When the export is ready:
HTTP/1.1 200 OK
Content-Type: application/json
{
"transactionTime": "2026-04-30T10:00:00Z",
"request": "https://[ecw-fhir-base]/$export?_type=Patient,Observation",
"requiresAccessToken": true,
"output": [
{
"type": "Patient",
"url": "https://[ecw-fhir-base]/files/[job-id]/Patient.ndjson",
"count": 12450
},
{
"type": "Observation",
"url": "https://[ecw-fhir-base]/files/[job-id]/Observation_part1.ndjson",
"count": 500000
},
{
"type": "Observation",
"url": "https://[ecw-fhir-base]/files/[job-id]/Observation_part2.ndjson",
"count": 487231
}
],
"error": []
}The manifest lists one or more NDJSON files per resource type. Large resource types (like Observation, which can have millions of rows) are split across multiple files.
Step 5 — Download the NDJSON files
GET https://[ecw-fhir-base]/files/[job-id]/Patient.ndjson
Authorization: Bearer [access-token]
Accept: application/fhir+ndjsonEach line of the file is a complete FHIR resource as JSON. Streaming parse rather than loading the whole file in memory — Section 7 covers this.
Step 6 — (Optional) Cleanup
DELETE https://[ecw-fhir-base]/$export/status/[job-id]
Authorization: Bearer [access-token]Tells the server you're done with this export job; it can free the temporary storage. Some servers do this automatically after a TTL.
5. Authentication: SMART Backend Services
Bulk data exports are typically server-to-server, not user-driven, so they use the SMART Backend Services Authorization profile rather than user-launch SMART on FHIR.
Setup
- Generate an RSA or EC key pair for your application.
- Register the public key with eCW's FHIR developer portal — you'll get a
client_id. - In your client, sign a JWT with the private key when requesting access tokens.
- Exchange the signed JWT for an access token via the OAuth token endpoint.
Token request
POST https://[ecw-auth-base]/token
Content-Type: application/x-www-form-urlencoded
scope=system/*.read
&grant_type=client_credentials
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=[signed-jwt]Scopes
For bulk export workflows, you'll typically request:
system/*.read— read access to all resource types (most permissive)system/Patient.read system/Encounter.read system/Observation.read ...— granular per-resource
eCW may restrict which scopes are available depending on your application's profile and the practice's configuration. Always start with the minimum scope needed; broader scopes face stricter review.
Token lifetime and refresh
Access tokens are typically valid for 5 minutes to 1 hour depending on the implementation. Long-running exports may require refresh — when polling and downloading takes longer than the token lifetime, request a fresh token rather than failing the operation.
For FHIR authentication patterns more broadly, see our FHIR R4 integration complete guide.
6. Implementing the Export in Mirth Connect
Mirth Connect is well-suited as the integration engine for FHIR $export workflows because it handles asynchronous patterns, multi-tenant routing, and reliable file ingestion in production-grade ways. The high-level architecture:
[Trigger] <- scheduled or external
|
[Channel A: Kickoff] <- initiates $export, captures job URL
| writes to globalChannelMap or external queue
[Channel B: Poller] <- polls job URL on schedule
| when complete
[Channel C: Downloader] <- fetches NDJSON files
|
[Channel D: Parser] <- line-by-line NDJSON -> individual FHIR resources
|
[Storage / Downstream] <- analytics warehouse, FHIR server, or downstream consumersBelow is the channel-by-channel pattern. Adapt connector specifics to your version of Mirth Connect — the example uses HTTP Sender connectors with JavaScript transformers.
6.1 Channel A — Export Kickoff
Source: Database Reader (polls a scheduled-jobs table) or HTTP Listener (triggered by an external scheduler).
Destination: HTTP Sender to the eCW $export endpoint.
Source transformer prepares the request:
// Build $export kickoff request
var since = sourceMap.get('since') || '2026-01-01T00:00:00Z';
var resourceTypes = sourceMap.get('resourceTypes') ||
'Patient,Encounter,Observation,Condition';
var url = configurationMap.get('ecw.fhir.base') +
'/$export?_type=' + encodeURIComponent(resourceTypes) +
'&_since=' + encodeURIComponent(since) +
'&_outputFormat=application/fhir+ndjson';
channelMap.put('exportUrl', url);In the destination's HTTP Sender:
- Method: GET
- URL: Use
}exportUrl{from channelMap - Headers:
Accept: application/fhir+jsonPrefer: respond-asyncAuthorization: Bearer }accessToken{
Destination response transformer captures the polling URL:
// Server returns 202 with Content-Location header
var responseHeaders = connectorMessage.getResponseMap().get('http_response_headers');
var contentLocation = responseHeaders.get('Content-Location');
if (!contentLocation) {
throw new Error('Kickoff did not return Content-Location header. Actual status: ' +
connectorMessage.getResponseMap().get('responseStatus'));
}
// Persist the job state - typically to a database table or external queue
var jobRecord = {
jobId: java.util.UUID.randomUUID().toString(),
pollingUrl: contentLocation.toString(),
state: 'POLLING',
kickoffTime: new Date().toISOString(),
tenantId: sourceMap.get('tenantId')
};
// Route to a downstream channel that picks up jobs to poll
VMRouter().routeMessage('Job_Track_Export', JSON.stringify(jobRecord));6.2 Channel B — Poller
Source: Database Reader on the export-jobs table, polling for jobs in POLLING state. Set the polling interval to roughly the Retry-After typical value (e.g., 60 seconds).
Destination: HTTP Sender to the polling URL stored on each job.
Polling logic (in destination response transformer):
var statusCode = connectorMessage.getResponseMap().get('responseStatusCode');
if (statusCode == '202') {
// Still processing - leave job in POLLING state, will retry next interval
var retryAfter = connectorMessage.getResponseMap()
.get('http_response_headers').get('Retry-After');
logger.info('Export still in progress; Retry-After: ' + retryAfter);
} else if (statusCode == '200') {
// Export complete - parse manifest
var manifest = JSON.parse(connectorMessage.getEncodedData());
var jobId = sourceMap.get('jobId');
// Update job state to READY, store manifest
updateJobState(jobId, 'READY', manifest);
// Trigger downloader
VMRouter().routeMessage('Channel_Download_Files',
JSON.stringify({jobId: jobId, manifest: manifest}));
} else {
// Error - escalate
logger.error('Export polling returned unexpected status: ' + statusCode);
updateJobState(sourceMap.get('jobId'), 'ERROR',
{statusCode: statusCode, body: connectorMessage.getEncodedData()});
}6.3 Channel C — Downloader
Source: Triggered by Channel B when status is READY. Receives the manifest.
Destination(s): One HTTP Sender per file in the manifest, downloading to a local working directory or directly to object storage (S3 / Azure Blob / GCS).
For very large NDJSON files (multiple GB), stream the download rather than loading into memory:
// Download large files in streaming fashion (Java/Groovy or destination-level streaming)
// Specifics depend on Mirth version - this is the pattern, not the only API:
var url = sourceMap.get('fileUrl');
var localPath = '/var/lib/mirth/ecw-exports/' + sourceMap.get('jobId') + '/'
+ sourceMap.get('resourceType') + '.ndjson';
// Use streaming download via Java HTTP client - example pattern
var fileWriter = new java.io.FileOutputStream(localPath);
// (full implementation specifics vary by Mirth version)For most production deployments, downloading large NDJSON files to object storage rather than the Mirth host's local disk is the right pattern — it decouples download throughput from Mirth's heap and disk.
6.4 Channel D — NDJSON Parser
Source: File Reader watching the directory or object-storage path where Channel C writes files. Set Process Batch: true so each file is processed line-by-line.
Destination: Whatever your downstream is — a FHIR server, an analytics warehouse, a downstream Mirth channel, or a Kafka topic.
Source transformer for batch parsing:
// Mirth Batch Mode produces one message per line (per FHIR resource)
// 'msg' is the individual NDJSON line
var resource;
try {
resource = JSON.parse(msg.toString());
} catch (e) {
logger.error('Failed to parse NDJSON line: ' + e.message);
return; // Skip this line; channel continues
}
// Resource type and ID
var resourceType = resource.resourceType;
var resourceId = resource.id;
if (!resourceType || !resourceId) {
logger.warn('NDJSON line missing resourceType or id; skipping');
return;
}
// Persist or forward
channelMap.put('resource', JSON.stringify(resource));
channelMap.put('resourceType', resourceType);
channelMap.put('resourceId', resourceId);The destination then writes the resource to the chosen downstream — see Section 8 for storage patterns.
For broader Mirth Connect operational patterns, see our Mirth Connect performance tuning, Mirth Connect database configuration, and the practitioner walk-through in How to build an HL7 interface in Mirth Connect.
7. Parsing NDJSON Files
NDJSON (Newline-Delimited JSON) is one resource per line, no enclosing array. This format is specifically chosen for streaming — you can parse files larger than memory.
Why streaming matters
A single eCW practice's Observation export can be 5 GB+. Loading it into memory parses the entire file before processing the first record. Streaming processes records as they arrive — constant memory regardless of file size.
Streaming pattern (Java / Mirth)
// Pattern using Java's BufferedReader for line-by-line processing
var fileReader = new java.io.FileReader('/path/to/Observation.ndjson');
var bufferedReader = new java.io.BufferedReader(fileReader);
var line;
var lineCount = 0;
try {
while ((line = bufferedReader.readLine()) !== null) {
lineCount++;
try {
var resource = JSON.parse(line);
// Process resource - write to downstream, queue, transform, etc.
processResource(resource);
} catch (e) {
logger.warn('Skipping malformed NDJSON line ' + lineCount + ': ' + e.message);
}
}
} finally {
bufferedReader.close();
fileReader.close();
}
logger.info('Processed ' + lineCount + ' lines from NDJSON');In Mirth's File Reader with Batch Mode enabled, this is handled automatically — Mirth produces one message per line.
Parallelization
For very large files, you can split a single NDJSON file across multiple Mirth instances or processing threads. The pattern:
- Pre-split the file into chunks (e.g., 10,000 lines each) using
split -l 10000. - Each chunk becomes a separate input file to the parser channel.
- Multiple parser-channel threads consume chunks concurrently.
This trades some operational complexity for throughput.
Resource validation
Before trusting an NDJSON resource, validate it:
- Has
resourceTypeandidat minimum. resourceTypematches the file's declared type in the manifest.- Required fields per US Core profile are present.
Resources that fail validation should be logged and quarantined rather than silently dropped or written to downstream stores.
8. Resource Mapping and Storage
Where the bulk-exported FHIR data ends up depends on your use case.
8.1 Direct to FHIR server (replication pattern)
Use case: build a FHIR-native data lake mirroring eCW data.
- Each NDJSON resource is
PUTorPOST-ed to your own FHIR server. - Resources retain their original IDs (or are remapped to your tenant scheme).
- Useful for downstream FHIR-aware applications.
For Mirth-side FHIR server work, see our Mirth FHIR Server offering.
8.2 Direct to analytics warehouse (denormalized pattern)
Use case: BI dashboards, ML training, ACO reporting.
- Each NDJSON resource is flattened to row format.
- Written to Snowflake, BigQuery, Redshift, or similar.
- Often combined across resource types into denormalized analytical tables.
This is the most common use case for eCW bulk export. The flattening logic is non-trivial — FHIR resources have nested structures, repeating elements, references — but the analytics value is high.
8.3 Hybrid (operational + analytical)
Use case: real-time operational queries plus analytical reporting.
- Resources written to both a FHIR server (for operational queries) and a warehouse (for analytics).
- Mirth fans out from the parser channel to both destinations.
This pattern is operationally heavier but supports the broadest range of downstream consumers.
Tenant ID handling
In multi-practice deployments, always tag every resource with a tenant identifierin your downstream storage — even if FHIR's native tenant scoping handles isolation. Practical reasons:
- BAA boundaries between practices must be respected; cross-practice queries must be explicitly authorized.
- Re-running exports for one tenant must not affect others.
- Audit logs need per-tenant attribution.
The standard pattern is to add a tenant_id column or meta.tag element to every record at parse time.
9. Scaling to Multi-Tenant and Multi-Practice
A healthtech platform integrating with eCW typically connects to dozens or hundreds of practices, not just one. The architecture changes meaningfully at scale.
9.1 Per-tenant export jobs
Each practice has its own:
- eCW FHIR base URL (often per-practice subdomain or path).
- OAuth client credentials (separate
client_idper registration). - Schedule and resource scope (some practices want daily exports; others weekly).
- BAA and contractual limits on what data can flow.
Track jobs per tenant in a database table with columns like:
tenant_id, last_export_time, last_export_status, fhir_base_url,
client_id, polling_url, retry_count, error_stateThe Mirth channels (kickoff, poller, downloader, parser) all read from this table; tenant identity flows through every channel via sourceMap or channelMap.
9.2 Concurrency limits
Don't run all tenant exports concurrently. Practical limits:
- Per-tenant: typically one export job at a time (eCW may not allow multiple).
- Across all tenants: limit to N concurrent exports based on Mirth host capacity, your downstream's ingestion rate, and the practice-level concurrency limits.
Use Mirth's source connector thread limits and a job-state machine to enforce this.
9.3 Failure isolation
A failed export for one tenant should not affect others. Pattern:
- Each tenant's job runs in its own logical workflow.
- Errors are recorded against the tenant, not the system.
- Alerts are tenant-specific so the right account team can address them.
- Retries are scheduled per-tenant with appropriate backoff.
For broader operational patterns, see Mirth Connect performance tuning.
9.4 Incremental exports
The _since parameter enables incremental exports — fetch only resources modified since the last successful export. This is dramatically faster than re-pulling the full population:
- First export: full population (large, expensive).
- Subsequent exports: incremental (small, fast), using
_sinceset to the previous export'stransactionTime.
Track the transactionTime from each manifest and use it for the next export's _since. This is the difference between daily exports being "expensive" or "cheap."
9.5 Operational monitoring at scale
For multi-tenant deployments, the operational concerns multiply:
- Per-tenant export status dashboards.
- Alerts on tenants whose exports haven't succeeded in N days.
- Token expiration monitoring — every tenant's keys/tokens have lifecycles.
- Quota usage monitoring — eCW may enforce monthly quotas per practice.
We cover these patterns in our Mirth helpdesk and services engagements for clients running multi-tenant FHIR programs.
10. Common Production Gotchas
The recurring patterns we see in client engagements specifically around eCW $export.
10.1 Treating $export as synchronous
Teams new to bulk data sometimes write the kickoff as a synchronous request and wait. The kickoff returns 202immediately — there's no synchronous response with data. Build the async pattern from day one.
10.2 Polling too aggressively
Polling every second exhausts rate limits and doesn't make the export faster. Respect Retry-After. If the server says wait 60 seconds, wait 60 seconds.
10.3 Missing the access token requirement on file downloads
Even after the manifest returns NDJSON URLs, the file downloads still require the bearer token (when requiresAccessToken: truein the manifest). Forgetting this returns 401s and looks like a permissions issue when it's an API misuse issue.
10.4 Token expiration mid-export
A long-running export can outlast the access token lifetime. Refresh the token before each polling call and each file download — don't assume a single token will last the whole job.
10.5 Memory exhaustion on large files
Loading a 5 GB Observation.ndjson into memory crashes Mirth with OutOfMemoryError. Use streaming (Section 7). For broader heap-tuning context, see Mirth Connect Java heap space error and performance tuning.
10.6 Naïve cross-tenant resource collisions
Two practices may both have a Patient/123. If your downstream uses just the FHIR id as the primary key, the second tenant overwrites the first. Always namespace by tenant — tenant_a/Patient/123 and tenant_b/Patient/123 are different records.
10.7 Ignoring the error array in the manifest
The manifest can include an error array listing files containing OperationOutcome resources for items that failed during export. Many implementations only check output and miss meaningful failures.
10.8 Re-pulling full populations every export
After the initial export, all subsequent exports should use _since to pull only deltas. Re-pulling the full population every time wastes bandwidth, time, and quota. Track transactionTime properly.
10.9 Not handling export job failures gracefully
The polling URL can return errors at any time — practice took the FHIR endpoint down for maintenance, quota exhausted, internal error. Build retry-with-backoff for transient failures and clear escalation for persistent ones.
10.10 Skipping the BAA / contractual review
Bulk data is bulk PHI. Before pulling production data from any practice, verify:
- BAA is in place between your organization and the practice.
- The export use case is within the BAA's permitted purposes.
- The data flows downstream are also covered (your warehouse, your processors, etc.).
For broader compliance context, see our HIPAA compliance guide for integration engineers and the healthcare interoperability and compliance guide.
11. When $export Is Not the Right Choice
Bulk export is the right tool for population-scale data flows. It's the wrong tool for:
Real-time clinical workflows
If you need a notification within seconds of a clinical event (admission, lab result, prescription), bulk export's batch latency is unacceptable. Use HL7 v2 over MLLP or FHIR Subscriptions instead. See our HL7 ADT messages reference and HL7 ORM/ORU lab workflow for v2 patterns.
Single-patient lookups
If you need data for one patient at a time (e.g., a chart-loading SMART app), use the standard FHIR REST API endpoints — GET /Patient/{id}, GET /Encounter?patient={id}. Bulk export is overkill and incurs latency.
Sub-population queries
For "all patients with diabetes seen in the last 30 days," group-level export may work, but standard FHIR search (GET /Condition?code=...) often handles it more directly.
Write-back to eCW
$export is read-only. Writing back to eCW requires the standard FHIR REST API or specific eCW APIs — and write capabilities vary by resource type and practice configuration.
For the broader EHR + FHIR landscape and which integration pattern fits which use case, see our EHR integration complete guide, the FHIR R4 integration complete guide, and our HL7 integration services for production engagements.
12. Frequently Asked Questions
What is FHIR $export in eClinicalWorks?
$export is the FHIR R4 Bulk Data Access operation — a standardized way to extract large volumes of patient data from eClinicalWorks (or any certified FHIR R4 EHR) as NDJSON files. eCW supports $export at the system, group, and patient levels under the ONC 21st Century Cures Act mandate. It's the most efficient way to ingest population-scale data from eCW for analytics, ACO reporting, AI training, and population health workflows.
How long does an eClinicalWorks $export take?
It varies by population size and resource scope. Small practices with limited resource types: 5–30 minutes. Large multi-site practices with full resource types: 1–6 hours. Very large datasets can take 12+ hours. Always poll respecting Retry-After rather than assuming a fixed duration.
Do I need a BAA with eClinicalWorks to use $export?
You need a BAA with the practice (the eCW customer) whose data you're accessing — not directly with eClinicalWorks. The practice authorizes your client application; the BAA covers the data-sharing relationship between your organization and the practice. eCW itself enforces practice-level access controls.
What's the difference between system, group, and patient $export?
System (/$export) returns all data your client has access to across the practice. Group (/Group/[id]/$export) returns data for a defined patient group (cohort). Patient (/Patient/$export) returns data for all patients but typically narrower in non-clinical resources. Most analytics use cases use system-level.
How do I authenticate to eClinicalWorks for bulk export?
Use the SMART Backend Services Authorization profile — register your application's public key with eCW's developer portal, sign a JWT with your private key, exchange it for an access token via OAuth, and include the token on every API call. This is server-to-server auth, not user-launched SMART on FHIR.
Can I use Mirth Connect for eClinicalWorks $export?
Yes. Mirth Connect is well-suited to FHIR $export workflows because it handles the asynchronous pattern (kickoff, poll, download), supports streaming NDJSON parsing, and handles multi-tenant routing reliably. The pattern uses 4 channels: kickoff, poller, downloader, parser.
What are eCW's rate limits for $export?
eCW enforces rate limits but the specific values aren't consistently documented and can change. Practical defaults: poll no more frequently than Retry-After (typically 60 seconds), don't run multiple concurrent exports for the same tenant, and engage eCW's vendor services team for elevated quotas at production scale.
How do I do incremental exports rather than re-pulling everything?
Use the _since query parameter set to the previous export's transactionTime (returned in the manifest). This pulls only resources modified since that time. Track transactionTime per tenant in your job-tracking database.
What if a $export job fails partway?
Check the manifest's error array — it lists files containing OperationOutcome resources for items that failed. Persistent failures (quota exhausted, server error, malformed request) should be logged, alerted, and retried with backoff. Don't silently retry indefinitely.
Can I use $export for real-time workflows?
No. Bulk export is batch-oriented with latency from minutes to hours. For real-time clinical workflows (admissions, lab results, prescription events), use HL7 v2 over MLLP or FHIR Subscriptions.
What's the difference between $export and the standard FHIR REST API?
The standard REST API (GET /Patient/123, GET /Encounter?patient=123) returns one resource or a small page at a time — appropriate for chart-loading apps and per-patient workflows. $export returns the entire authorized population in NDJSON files — appropriate for analytics and population health. Use whichever fits the volume and latency requirements.
Related Reading
- FHIR R4 Integration: The Complete Guide
- EHR Integration: The Complete Guide
- HL7 Integration: The Complete Guide
- Mirth Connect: The Complete Guide
- FHIR Bulk Data Implementation Guide 2026
- HL7 v2 vs v3 vs FHIR
- Healthcare Interoperability & Compliance Guide
- Mirth Connect Performance Tuning
- Mirth Connect Database Configuration
- Mirth Connect Java Heap Space Error
- HIPAA Compliance for Integration Engineers
- eClinicalWorks Integration
- eClinicalWorks FHIR Export — Productized Service
- FHIR R4 Adapter — Productized Service
- Mirth FHIR Server
- Mirth Helpdesk