Using the Ethiopia FHIR Validation Sandbox

Base URL: https://sandbox.fhir.et · validates against the Ethiopia Base IG (et.fhir.core) · ← dashboard

This sandbox checks whether FHIR resources conform to Ethiopia's official FHIR profiles. Send a resource (or a transaction bundle) and get back an OperationOutcome with any conformance errors. There are three ways in: a raw $validate call, a pass-through interceptor, and a batch test harness.

Which profile a resource is checked against comes from its own meta.profile. If a resource declares no profile, it's validated against base FHIR R4. Running locally instead of deployed? Use http://localhost:8095.

Running the sandbox (operators)

The whole stack — HAPI (the validation engine, with the ET IGs loaded), the always-on conformance interceptor, and the gateway (the single entrypoint) — comes up with one command:

cd hapi-sandbox
docker compose up -d        # start HAPI + interceptor + gateway
docker compose ps           # wait for hapi-sandbox + hapi-interceptor -> (healthy)

Then open http://localhost:8095/docs/ (this dashboard) or http://localhost:8095/sim/simulator.html (the simulator), and POST to http://localhost:8095/intercept. Locally the base URL is http://localhost:8095; deployed it is https://sandbox.fhir.et (TLS handled by the host's reverse proxy, which forwards to the gateway). After rebuilding an IG, force a fresh install with docker compose down && rm -rf data && docker compose up -d.

1 Validate a single resource

POST the resource to $validate for its type. Use ?profile= to assert a specific Ethiopian profile (or rely on the resource's meta.profile).

# Conformant ETPatient -> 200, no error/fatal issues
curl -X POST "https://sandbox.fhir.et/fhir/Patient/$validate?profile=http://fhir.et/core/StructureDefinition/ETPatient" \
  -H "Content-Type: application/fhir+json" \
  -d '{"resourceType":"Patient","meta":{"profile":["http://fhir.et/core/StructureDefinition/ETPatient"]},
       "identifier":[{"system":"http://id.gov.et/nationalidentifier","value":"ET-1"}],
       "name":[{"family":"Challa"}],"gender":"male"}'

A non-conformant resource (e.g. a marital status outside the required Ethiopian value set) comes back HTTP 422 with the failing issues listed in the OperationOutcome.

2 Validate a transaction bundle

POST the whole bundle to Bundle/$validate; each entry is checked against its own meta.profile.

curl -X POST "https://sandbox.fhir.et/fhir/Bundle/$validate" \
  -H "Content-Type: application/fhir+json" -d @bundle.json

3 Create with validation

The server validates on write. POSTing a resource that declares an Ethiopian meta.profile but doesn't conform is rejected (422); a conformant one is stored (201).

curl -X POST "https://sandbox.fhir.et/fhir/Patient" \
  -H "Content-Type: application/fhir+json" -d @patient.json

4 The conformance interceptor

POST any resource or bundle to /intercept. It validates against the Ethiopian profiles, returns the verdict in response headers, and forwards to the server — so an existing system can be pointed at it unchanged.

curl -i -X POST "https://sandbox.fhir.et/intercept" \
  -H "Content-Type: application/fhir+json" -d @bundle.json

# response headers:
X-ET-Validation: 0 error(s) in Bundle (3 entries)
X-ET-Validation-Report: <url-encoded JSON of the issues>

Prefer a UI? Open the actor simulator, pick a payload (e.g. the medication-dispenser bundle), and submit — you'll see the verdict and per-issue detail.

5 Endpoints at a glance

URLWhat it does
/fhir/*Full FHIR API (CRUD, search, $validate, /metadata)
/fhir/{Type}/$validateValidate a resource, get an OperationOutcome
/interceptValidate → return verdict headers → forward (conformance proxy)
/sim/simulator.htmlBrowser actor simulator
/docs/Dashboard · /docs/postman/… Postman collection

6 Postman

Import the collection and set the host variable to https://sandbox.fhir.et. It has ready-made $validate (good/bad), create-with-validation, and interceptor requests.

7 Run a conformance session (live test + report)

A session captures a window of traffic, scores every request, audits what was stored, and produces an HTML report — the format used for the showcase and for onboarding a real system.

Two test modes. This section is the live mode (real/simulated traffic through /intercept). The non-live battery instead replays a fixed set of curated good/bad fixtures with no live traffic — deterministic regression, runs from a laptop or CI against any server:
cd harness && SHR_URL=https://sandbox.fhir.et/fhir ./run-tests.sh
(fixtures come from the et.fhir.core.test IG; good → 0 errors, bad → fails; HTML report under harness/target/.)

Step 1 — start it (sandbox operators, in the repo):

cd harness
sdk env                 # Java 21 (per .sdkmanrc)
./test-session.sh       # or: LABEL=hiv-demo ./test-session.sh

It restarts the interceptor for a clean session and starts a live feed.

Step 2 — send traffic. Point a real system's FHIR base URL at https://sandbox.fhir.et/intercept and use it normally, or open the simulator and submit. Each request prints a live / line. The interceptor records every verdict to harness/target/sessions/validation-reports.json and the patients it saw to patients.txt.

Step 3 — finish and read the report. Press Ctrl+C. The session then produces:

OutputWhat it contains
terminal summary✓ N conformant / ✗ N with findings across the session
stored-data auditre-validates everything stored for each seen patient via instance-level $validate
harness/target/runs/<timestamp>/session.htmlthe report — auto-opens in your browser: a verdict table (click a row for its OperationOutcome issues) beside the embedded stored-data audit report

Already have session data and just want to rebuild the report? ./test-session.sh report.

One-off vs session. A single POST to /intercept returns its verdict inline in the X-ET-Validation / X-ET-Validation-Report response headers (what the simulator shows per submit). The session.html dashboard is the per-session rollup of all those calls plus the stored-data audit.

8 The IGs behind the rules

Validation rules come from the official Ethiopia IGs loaded into the sandbox: et.fhir.core (ETBase, the base), et.fhir.surveillance, and et.fhir.hiv — with more added as they're published. A resource's meta.profile selects which profile it's checked against (e.g. an HIV dispense → hiv-medication-dispense, which builds on surveillance → core). The et.fhir.core.test IG holds the curated valid fixtures the non-live battery validates; the intentionally-invalid (negative) fixtures live with the harness as raw JSON (they can't be IG examples — SUSHI fails the build on them).

Deployed sessions. The interceptor is a Karate mock server and the non-live battery + auditor are Karate test runs. Today a live session against sandbox.fhir.et is driven by an operator running test-session.sh on the host; a hosted /session view (watch a live session in the browser, no local tooling) is the planned next step.