Authentication
Authenticate to the EDFlow Integration API: OAuth 2.0 client credentials with private_key_jwt, client secret or mTLS, short-lived JWT access tokens, scopes, network binding, HMAC source keys for the signed ingest profile, clock skew, lockouts and signing at send time.
There are two credential families:
- API credentials for the REST API (
/v1/*): OAuth 2.0 client credentials that return a 5-minute JWT access token, or (in limited cases) a static API key. - HMAC source keys for the signed ingest profile (
/ehr/v1/*), for engines already built to it.
Methods
| Method | How | Recommended for | Live policy |
|---|---|---|---|
private_key_jwt | Sign a JWT assertion with your private key; register a JWKS URL or public keys | EMR vendors, Epic and Oracle system accounts, anyone who can sign JWTs | Always allowed. Recommended |
client_secret_basic / client_secret_post | Client id and secret | Engines that can't sign JWTs | Only with an enforced IP allowlist or mTLS, and never for sensitive scopes |
mTLS (tls_client_auth, self_signed_tls_client_auth) | Client certificate on the mTLS host; the token is bound to it | Hospitals that mandate mutual TLS | Always allowed |
| Static API key | Authorization: Bearer edf_{env}_sk_… | Prototypes, simple reads, FHIR subscription rest-hooks | Off by default; IP allowlist and ≤ 90-day expiry when allowed |
| HTTP Basic with an API key | client_id:api_key | HL7-over-HTTP senders that only do Basic | As static keys, /v1/hl7v2 only |
| HMAC source key | Per-request signature over the raw body | Engines built to /ehr/v1/* | IP allowlist required; key versions expire within 365 days |
Get a token
POST /oauth/token with grant_type=client_credentials. Optional scope narrows the token.
private_key_jwt
The assertion: header alg RS384, ES384, RS256 or ES256 and a kid from your registered keys; iss and sub equal your client id; aud is exactly the token endpoint URL (https://api.edflow.space/oauth/token in live); exp at most 5 minutes ahead; a unique jti.
import { importPKCS8, SignJWT } from "jose";
const tokenUrl = `${process.env.EDFLOW_API_BASE_URL}/oauth/token`;
const key = await importPKCS8(process.env.EDFLOW_PRIVATE_KEY_PEM ?? "", "ES384");
const assertion = await new SignJWT({})
.setProtectedHeader({ alg: "ES384", kid: process.env.EDFLOW_KEY_ID ?? "", typ: "JWT" })
.setIssuer(process.env.EDFLOW_CLIENT_ID ?? "")
.setSubject(process.env.EDFLOW_CLIENT_ID ?? "")
.setAudience(tokenUrl)
.setJti(crypto.randomUUID())
.setIssuedAt()
.setExpirationTime("4m")
.sign(key);
const res = await fetch(tokenUrl, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: assertion,
scope: "requests:read requests:write",
}),
});
const { access_token, expires_in } = await res.json();The portal can generate a key pair in your browser and download the private key; only the public key is sent. Prefer a JWKS URL: you rotate by publishing a new kid.
Access tokens
JWTs (RFC 9068) signed by EDFlow, valid 60 to 300 s (default 300). They carry your organization, sites, scopes, environment, the credential used and your pinned API version. Every request re-checks the app, the credential and the site, so revocations and scope reductions apply immediately, not at expiry. Cache the token and refresh 30 s before it expires.
Discovery: /.well-known/oauth-authorization-server (RFC 8414), /oauth/jwks.json, and /v1/fhir/.well-known/smart-configuration for SMART Backend Services clients.
Scopes
| Scope | Grants | Sensitive |
|---|---|---|
events:write, fhir:write, hl7v2:write | Ingest | |
events:validate | Dry-run validation | |
events:read | Ingest results (metadata only) | |
encounters:read | Encounter workflow state, lookup by visit number | ✓ |
careteam:read | Current care team | ✓ |
requests:read, requests:write | Patient requests feed; acknowledge and resolve | ✓ |
tasks:read | Patient tasks | ✓ |
codes:read | Department code alerts | |
documents:read | Visit documents | ✓ |
webhooks:read, webhooks:write | Webhook management | |
sandbox:write | Sandbox simulation and reset | Sandbox only |
Sensitive scopes need private_key_jwt or mTLS, a four-eyes approval and the privacy office's PIA attestation in live, and the site's capability switch. A token obtained with a client secret never carries them.
Network binding
In customer test and live every app must have at least one of: private_key_jwt with registered keys, mTLS, or an IP allowlist. A leaked client secret is useless from any address outside the allowlist.
The signed ingest profile
Requests to /ehr/v1/* carry EDFlow-Source, EDFlow-Delivery-Id (new for each attempt), EDFlow-Timestamp and EDFlow-Signature:
signedPayload = "v2." + timestamp + "." + deliveryId + "." + METHOD + " " + path + "."
+ (siteCode or "-") + "." + (input or "-") + "."
+ lowercase_hex(SHA-256(raw body))
signature = lowercase_hex(HMAC-SHA256(sourceKey, signedPayload))
header = "EDFlow-Signature: v2=" + signatureThe key is the 32 raw bytes of the 64-hex key revealed once in the admin console, not the ASCII of the hex. Sign the exact bytes you send. During a rotation you may send two signatures separated by a comma. Verify your implementation against the conformance test vector.
Sign and authenticate at send time
Engines retry from queues that can hold hours of messages. Compute the signature and fetch or refresh the token in the step that sends the request (a Mirth/OIE JavaScript Writer or destination pre-send script; a Rhapsody or Cloverleaf send-time script), never in the transformer, whose output queued retries replay unchanged.
| Response | What it means | Do this |
|---|---|---|
401 token_expired (retryAction: reauthenticate) | Token expired while queued | Fetch a new token, resend the same request |
401 timestamp_out_of_window, delivery_replayed (retryAction: resign) | Signed too long ago, or delivery id reused | Re-sign with a new timestamp and delivery id; event ids stay the same |
401 invalid_client, invalid_token | Wrong credential | Fix it; don't loop |
An exact replay of a signed delivery that was already processed returns the stored result, so a client that timed out learns what happened.
Clock skew
Timestamps more than 5 minutes from EDFlow's clock are refused. Keep engine clocks on NTP and compare with GET /ehr/v1/health at startup.
Lockouts
Ten failed authentications in 5 minutes from one (client id, IP) pair lock that pair for 15 minutes; 50 from one IP lock that IP. A lockout never blocks a valid credential, an allowlisted address or a registered mTLS certificate, so nobody can lock your integration out by guessing.
Related
- Credentials: lifecycle, rotation and what to do if a key leaks
- Errors
Dernière mise à jour le
Cette page vous a-t-elle été utile?
Concepts
How the EDFlow integration model works: sites, sources, encounters and events, facts and derived stages, projections and minimum necessary data, idempotency, ordering and precedence, and the early-arrival buffer.
Credentials
Manage EDFlow API credentials: self-serve in the developer portal or issued by the hospital or the vendor, shown once, rotation with overlap, expiry reminders, revocation, and what to do if a key leaks.