HL7 Bridge agent
A small local binary that exposes MLLP TCP listeners for your integration engine. Each message it receives is validated against one of your specs and replied to with a proper HL7 ACK. No HL7 content ever touches our database.
Overview
The agent is a single static binary you run on your own network. It needs a token to authenticate, which you grant by visiting a URL and clicking Authorize — the agent never asks you to paste a secret into a file. Once authorized, every run after that uses the saved credentials.
At steady state the agent:
- Authenticates with your Interoperall org using the token saved in
~/.interoperall/bridge.toml. - Fetches the list of HL7v2 specs in your org via
GET /api/v1/agent/specs. - Deterministically assigns a TCP port from the configured range (default
57100–57199) to each spec and starts an MLLP listener on each. - Persists the spec → port mapping to
bridge.state.jsonso assignments stay identical across restarts. - Starts the local admin dashboard at
http://localhost:9999.
Each incoming MLLP frame is forwarded to POST /api/v1/agent/validate/<spec_slug>over HTTPS. The JSON response is turned into an HL7 ACK (MSH/MSA/ERR…) and sent back through the same socket. A round trip is typically 80–200 ms.
Install
From Interoperall (recommended)
Go to Settings → HL7 Bridge agent, click the download button for your platform, and run the binary. On first run the agent walks you through authorization (see below) — no config file to download, no token to copy-paste.
Direct download
The binaries are also available individually. You'll need to log in to download them.
xattr -d com.apple.quarantine ./interoperall-bridge-*
First run & device authorization
The first time you run the agent it detects there's no config and starts the device authorization flow — the same OAuth-style pattern used by gh auth loginand the Stripe CLI. You don't handle any tokens directly.
$ ./interoperall-bridge-darwin-arm64
────────────────────────────────────────────────────────────────
Welcome to Interoperall Bridge
────────────────────────────────────────────────────────────────
Open this URL in your browser (any device):
https://interoperall.com/connect?code=K7T2-NP3X
Or visit:
https://interoperall.com/connect
and enter this code:
K7T2-NP3X
Waiting for authorization (expires in 10 minutes)…Open the URL on any device where you're logged into Interoperall — including a different machine if the agent is on a headless server. You'll see:
- A short page with the code pre-filled (from the
?code=…query) or an input box if you typed the short URL. - Two buttons: Authorize or Deny.
- Authorize mints a fresh agent token bound to your org, labelled with the agent's hostname.
A few seconds later, the agent finishes setup automatically:
✓ Authorized as "Acme Health" ✓ Saved to /Users/you/.interoperall/bridge.toml 2026-05-20T10:00:00.215Z INFO synced 3 specs from Interoperall 2026-05-20T10:00:00.220Z INFO assignments — point your integration engine at the listed ports: 2026-05-20T10:00:00.220Z INFO 127.0.0.1:57105 → adt-strict (v1.2.0) [inbound, ack=always] 2026-05-20T10:00:00.220Z INFO 127.0.0.1:57125 → oru-loinc (v0.3.0) [inbound, ack=always]
~/.interoperall/bridge.toml. You never saw or pasted it. You can revoke it any time from Settings → HL7 Bridge agent — the agent will start asking for re-authorization on its next run.On headless servers
The agent doesn't open a browser for you — it just prints the URL. That makes the flow work identically on a dev laptop and on a Linux server behind a firewall. Copy the URL or the 8-char code to any device that has a browser and you're logged into Interoperall on, approve, the agent finishes.
Denying or expiring
Codes are valid for 10 minutes. If you don't approve in time, or click Deny on the web page, the agent exits cleanly — just run it again to start a new authorization session. No token is ever created until you click Authorize.
Subsequent runs
After first-run setup, you just start the binary and the agent uses the saved config. By default it looks for bridge.toml in:
- The current directory (
./bridge.toml) - Then
~/.interoperall/bridge.toml(where first-run wrote it)
You can also pass an explicit path:
./interoperall-bridge # uses defaults above ./interoperall-bridge /etc/interoperall/bridge.toml # explicit path
Want to authorize a second agent on the same machine, or move the config to a non-default location? Just delete or move ~/.interoperall/bridge.toml and run the binary again — the device flow runs automatically when no config is found.
Auto-discovery & ports
By default the agent runs without any listener configuration. It fetches your org's HL7v2 specs and creates one listener per spec. Each spec is assigned a deterministic port using fnv1a(slug) % 100 + port_range_start — so the same spec always lands on the same port, even across machines.
The default range is 57100–57199. This range sits deep in IANA's dynamic/private band, so it never conflicts with common services (HTTP, SSH, Postgres, the HL7-default 2575, etc.). 100 ports is enough for any practical spec count.
If two specs hash to the same port, the second one is bumped to the next free slot. The assignment is then frozen in bridge.state.json so it survives subsequent restarts — even if new specs are added later.
Port state file
The agent writes its port-to-spec mapping to bridge.state.json in the same directory as the config (configurable via state_path). The file looks like this:
{
"version": 1,
"ports": {
"adt-strict": 57105,
"oru-loinc": 57125,
"siu-s12": 57160
}
}Keep this file alongside your bridge.toml in version control (or in your Ansible/Terraform secrets), so a fresh deployment of the agent picks the same ports as the old one.
bridge.toml reference
Minimum viable config:
org_token = "iob_…"
Every other field has a sensible default. Full reference:
Per-spec overrides
Sometimes you need a specific spec on a specific port — e.g. your existing integration engine is hard-wired to port 6661. Use [overrides.<slug>] blocks to tweak individual specs without leaving auto-discovery mode:
org_token = "iob_…" [overrides.adt-strict] port = 6661 # pin this spec to a specific port auto_ack = "only-on-pass" # only send an ACK when validation passes bind = "0.0.0.0" # listen on all interfaces (LAN access) version = "1.0.0" # pin to a specific published version (default: latest production)
Override fields:
Manual listener mode
Define explicit [[listeners]] entries when you want full control — for example to skip a spec entirely or run multiple listeners against the same spec on different ports.
Important: the moment any [[listeners]] block exists, auto-discovery is turned off — only the listeners you list will start. You become responsible for adding new specs to the config when they appear in your org.
org_token = "iob_…" [[listeners]] name = "ADT (prod inbound)" port = 6661 spec_slug = "adt-strict" direction = "inbound" auto_ack = "always" bind = "0.0.0.0" [[listeners]] name = "ADT (staging inbound)" port = 6671 spec_slug = "adt-strict" direction = "inbound" auto_ack = "always" bind = "127.0.0.1"
Admin dashboard
Each running agent serves a small dashboard at http://localhost:9999 (configurable via [admin]):
- Status cards — last heartbeat, last sync, spec count, listener count
- Listeners — port → spec mapping, ack policy, online state
- Recent traffic — last 500 frames (timestamp, source, status, ack code, response time)
- Logs — recent agent log lines
The dashboard polls the agent every 2 seconds. JSON endpoints are at /api/state, /api/traffic, /api/logs — useful for scripting health checks or monitoring.
How ACKs are built
For every incoming HL7v2 message the agent builds a response of the form:
MSH|^~\&|Interoperall|Bridge|<recv_app>|<recv_fac>|<now>||ACK^<trigger>^ACK|<id>|<P|T>|<ver> MSA|<AA|AE|AR>|<echo MSH-10>|<short summary> ERR||<location>|207&Application Internal Error&HL70357|<E|W|I>|||<rule message> ERR||... ← one per failed rule
Mapping:
Routing fields are swapped so the ACK is addressed back at the original sender (MSH-3 / 4 / 5 / 6 are flipped). MSH-10 in the ACK is a fresh control ID; MSA-2 echoes the sender's original control ID.
Up to 50 ERR segments are emitted per ACK. Beyond that, a final ERR notes the remaining truncation count.
Troubleshooting
First-run device flow doesn't finish
The agent prints a code, you open the URL — and nothing seems to happen when you click Authorize. Things to check:
- You must be logged into Interoperall on the device where you opened the URL. The page will redirect to sign-in if you're not.
- Your role in the org must be
adminor higher (owner/admin). Editors and viewers can't authorize agents. - The org plan must be
pro. Free plans see a clear error. - Codes expire after 10 minutes. If you took longer, just run the agent again to start a new session.
- The agent polls every ~5 seconds — there's a small delay between clicking Authorize and the agent finishing setup.
"heartbeat failed on startup"
Happens on every run AFTER the first one — meaning a config exists but the saved token doesn't work.
- The token was revoked from Settings. Delete
~/.interoperall/bridge.tomland run the agent again — it'll start a fresh device-authorization session. - The API is unreachable from the agent's network (firewall, DNS, proxies). Confirm:
curl -i -H "Authorization: Bearer iob_..." \ -X POST https://interoperall.com/api/v1/agent/heartbeat
"listener X references spec which does not exist"
The slug in the config or override doesn't match any spec in your org. Either fix the slug, create the spec in Interoperall, or remove the listener.
"EADDRINUSE: address already in use"
Another process is holding the same port. Easiest fix: bump port_range_start to something like 57500, delete bridge.state.json, restart. The agent will pick a fresh range.
My integration engine rejects the ACK
Different engines have different opinions about ACK content. Common sticking points:
- Some engines insist on a specific
MSH-3(sending app) value. Use[overrides.<slug>].auto_ackto disable ACKs (never) and have your engine treat the message as fire-and-forget instead. - MSH-11 (processing ID) and MSH-12 (version) are echoed from the sender by default. If the sender doesn't set them, the ACK falls back to
Pand2.5.1.
Updating the agent
Replace the binary in place, then restart. Configuration and state files are forward-compatible across patch and minor releases.
On startup the agent receives a min_versionfrom the server. If your binary is older than the server's minimum, it logs a warning so you know to upgrade. We'll never break agents in the field for breaking changes — those always come with a deprecation window.