Authentication
Every call to the NHIE interoperability layer (IOL) is authenticated in layers. Your system proves who it is with a client certificate, identifies the client or user behind the request with a service-specific header, and, in production, connects from a whitelisted IP address.
This page explains how those layers fit together at runtime. To obtain your certificate and credentials, follow the Onboarding SOP.
How NHIE authenticates your system
| Layer | What it proves | How you supply it | Issued by |
|---|---|---|---|
| Mutual TLS (mTLS) | Which system is connecting (node authentication, IHE ATNA ITI-19) | Client certificate and private key, presented during the TLS handshake | NDoH, from the CSR you submit |
| API key (MHFL) | Which registered client is calling MHFL | apikey request header | NDoH, when your system is approved on MHFL |
| Security header (HPRS) | Which application and user made the request | x-security: <requesting-application>$<user-id> request header | You set it on each request |
| IP whitelisting | That the call comes from your approved network | Calling from the IP addresses you registered | NDoH, before production access |
Node authentication does not replace user authentication. Your own system remains responsible for authenticating the clinicians and staff who trigger each request, and for recording them in the ATNA audit events you send to the ARR.
What you need
| Item | File or value | Where it comes from |
|---|---|---|
| Private key | client-private.key | You generate it. Never share it. |
| Signed client certificate | client.crt | Returned after NDoH approves your CSR |
| CA bundle | ca_bundle.crt | Returned with your signed certificate |
| API key | <your-api-key> | Issued per client when your system is approved on MHFL |
| Requesting application name | For example emr | Agreed with NDoH during onboarding |
Requirements by environment
| Environment | Base URL | mTLS | IP whitelisting |
|---|---|---|---|
Test, initial connectivity (/csd, /pdq, /pdqm) | https://test-hie.health.gov.za | Optional | Not required |
Test, mTLS endpoints (/mtlspdqm/Patient, /arr/AuditEvent) | https://test-hie.health.gov.za | Required | Not required |
| Production | https://hie.health.gov.za | Required | Required |
Service headers apply in every environment: apikey for MHFL and x-security for HPRS PDQm. See Service headers.
Start without mTLS to confirm your headers and payloads, then move to the mTLS endpoints before interoperability testing. This mirrors the HPRS integrator process.
Example authenticated request
This PDQm patient query presents a client certificate and the HPRS x-security header, the same combination required in production.
- cURL
- Node.js
- Python
curl --request GET \
--cert client.crt \
--key client-private.key \
--cacert ca_bundle.crt \
--header 'x-security: emr$<user-id>' \
--header 'Accept: application/fhir+json' \
--url 'https://test-hie.health.gov.za/mtlspdqm/Patient?identifier=<hprn>'
import fs from 'node:fs';
import https from 'node:https';
const userId = '<user-id>';
const options = {
hostname: 'test-hie.health.gov.za',
path: '/mtlspdqm/Patient?identifier=<hprn>',
method: 'GET',
cert: fs.readFileSync('client.crt'),
key: fs.readFileSync('client-private.key'),
ca: fs.readFileSync('ca_bundle.crt'),
headers: {
'x-security': `emr$${userId}`,
Accept: 'application/fhir+json',
},
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
if (res.statusCode !== 200) {
throw new Error(`Request failed with status ${res.statusCode}`);
}
console.log(JSON.parse(body));
});
});
req.on('error', (err) => console.error('TLS or network error:', err.message));
req.end();
import os
import requests
user_id = "<user-id>"
response = requests.get(
"https://test-hie.health.gov.za/mtlspdqm/Patient",
params={"identifier": "<hprn>"},
cert=("client.crt", "client-private.key"),
verify="ca_bundle.crt",
headers={
"x-security": f"emr${user_id}",
"Accept": "application/fhir+json",
},
timeout=30,
)
response.raise_for_status()
print(response.json())
Implementation guidance
- Protect your private key. Keep it on the integrating server or in a hardware or cloud key store. Never commit it, email it, or share it with NDoH.
- Keep the API key out of source control. Load it from a secret manager or environment variable, and never write it to logs.
- Send the real user in
x-security. Use the ID of the person who triggered the request, not a shared service account, so HPRS access can be traced to an individual. - Rotate certificates every 90 days, or as your certificate policy requires. Submit a new CSR before the current certificate expires.
- Verify the server. Always validate the NHIE certificate against
ca_bundle.crt. Do not disable TLS verification (for examplecurl -k), even in test. - Preserve the caller's address. If you call NHIE through your own proxy, set
X-Forwarded-Forcorrectly so IP whitelisting and audit records stay accurate. - Log every call on your side, and send the matching ATNA audit event to the ARR.
Common failure cases
| Symptom | Likely cause | Recommended action |
|---|---|---|
| SSL handshake failure | Certificate and private key do not match, or certificate has expired | Check the pair with openssl s_client -connect test-hie.health.gov.za:443 -cert client.crt -key client-private.key |
401 Unauthorized with {"message":"Invalid authentication credentials"} | Missing or wrong apikey header (MHFL) | Check the header name and value |
403 Forbidden with "Missing security header" or "Missing account id" | Missing or incomplete x-security header (HPRS) | Send x-security: <requesting-application>$<user-id> with both parts filled in |
403 Forbidden (other) | IP address not whitelisted, or your credentials do not cover this route | Contact the integration team through HIS Support |
404 Not Found on the base URL | Expected: you reached the gateway but did not call a service path | Your mTLS connection is working. Call a service endpoint. |
See Error Handling for service-level errors, and contact HIS Support if a problem persists.

