External integration contract for RingCX to retrieve a patient's outbound-calling rules at call setup time.
| Item | Value |
|---|---|
| Method | POST |
| Path | /api-ext/patients/authenticate |
| Content-Type | application/json |
| Authentication | Shared API key (AUTHENTICATE_API_KEY) |
| Purpose | Validate a patient's PIN and return their wards, devices, allowed numbers, and kill-switch flags |
The enforcement system (RingCX) is the PEP (policy enforcement point). This API is the PDP (policy decision point). RingCX calls this endpoint during call setup and must act on the response (allow, block, record) according to its own scripting — that wiring is out of scope for this repository.
Every request must include the shared API key in the Authorization header.
Accepted formats (case-sensitive prefix, then the key):
| Header value | Accepted |
|---|---|
Bearer <api-key> |
Yes |
ApiKey <api-key> |
Yes |
APIKey <api-key> |
Yes |
<api-key> (raw, no prefix) |
Yes |
| Status | Condition | Response body |
|---|---|---|
401 |
Missing or invalid API key | { "error": "Invalid API key" } |
503 |
Server has no AUTHENTICATE_API_KEY configured |
{ "error": "Authenticate endpoint is not configured" } |
Provision the key out-of-band. In production it is stored as the AUTHENTICATE_API_KEY
environment variable (Azure Key Vault / app settings recommended).
{
"patient_id": "12345",
"pin": "0000"
}| Field | Type | Rules |
|---|---|---|
patient_id |
string | Required. Numeric string only (/^\d+$/). Maps to patients.external_patient_id in the database. |
pin |
string | Required. 4–8 digits (/^\d{4,8}$/). Compared to the stored patient PIN. |
{ "error": "patient_id must be a numeric string" }{ "error": "pin must be 4-8 digits" }Returned when:
- No patient exists with the given
patient_id, or - The PIN does not match
The response is intentionally generic — it does not reveal whether the patient ID or PIN was wrong.
{
"patient_id": "",
"any_calls_allowed": false
}RingCX should treat this as do not allow outbound calling for the caller.
{
"patient_name": "Jane Smith",
"patient_id": "12345",
"any_calls_allowed": true,
"ward": [
{
"ward_name": "Ward A",
"any_calls_allowed": true,
"device_dids": [
{
"device": {
"did": "+61290000001",
"any_calls_allowed": true
}
}
]
}
],
"allowed_callers": [
{
"phone_number": "+61290000099",
"name": "Jane Smith",
"relationship": "Mother",
"any_calls_allowed": true,
"allow_recording": false
},
{
"phone_number": "+61290000100",
"name": "John Smith",
"relationship": "Brother",
"any_calls_allowed": false,
"allow_recording": true
}
],
"global_whitelist": [
{
"phone_number": "+61131114",
"name": "Quit Smoking",
"any_calls_allowed": true
},
{
"phone_number": "+611800011511",
"name": "Mental Health hotlines",
"any_calls_allowed": true
}
]
}| Field | Type | Description |
|---|---|---|
patient_name |
string | Patient's display name (patients.full_name). |
patient_id |
string | External patient ID (same as request patient_id). |
any_calls_allowed |
boolean | Patient-level outbound kill switch. false = block all outbound calling for this patient. Also false when the global kill switch is on (see below). |
ward |
array | Wards the patient belongs to, with nested devices. |
allowed_callers |
array | Whitelisted outbound destinations for this patient. Every configured number is returned; use any_calls_allowed on each item to allow or block calls to that destination. |
global_whitelist |
array | Organisation-wide outbound destinations dialable by every patient. See global_whitelist[] items below. |
| Field | Type | Description |
|---|---|---|
ward_name |
string | Ward name. |
any_calls_allowed |
boolean | Ward-level kill switch. false = block outbound for this ward. |
device_dids |
array | Devices in this ward. May be empty if the group has no devices. |
| Field | Type | Description |
|---|---|---|
device.did |
string | Device phone number (E.164). |
device.any_calls_allowed |
boolean | Device-level kill switch. false = block outbound on this device. |
Each item is one whitelisted destination stored in allowed_numbers. All fields are always
present on success responses.
| Field | Type | Description |
|---|---|---|
phone_number |
string | E.164 destination on the patient's whitelist. |
name |
string | Contact name for this destination (may be empty). |
relationship |
string | Contact relationship to the patient (may be empty). |
any_calls_allowed |
boolean | Per-number outbound kill switch. true = patient may dial this number (subject to ward/device/global rules). false = block outbound to this number even though it remains on the whitelist. Defaults to true when an administrator adds a number. |
allow_recording |
boolean | Whether recording is allowed for calls to this number. Independent of any_calls_allowed. |
Enforcement notes for RingCX:
- Top-level
any_calls_allowedmust betruebefore any outbound call can be permitted for this patient (alongside ward, device, allowed-number, and global rules). - A destination must appear in
allowed_callersand haveany_calls_allowed: truebefore an outbound call to that number can be permitted. - Numbers with
any_calls_allowed: falseare still returned so the enforcement layer can distinguish "not whitelisted" from "whitelisted but blocked". allow_recordingonly affects recording behaviour; it does not grant dialling permission on its own.- A destination may also appear in
global_whitelist. Every patient may dial those numbers when top-levelany_calls_allowedistrueand the destination hasany_calls_allowed: true, without needing a matching entry inallowed_callers.
Each item is one organisation-wide destination stored in global_whitelist_numbers. All fields are
always present on success responses.
| Field | Type | Description |
|---|---|---|
phone_number |
string | E.164 destination on the global whitelist. |
name |
string | Display label for this destination (may be empty). |
any_calls_allowed |
boolean | Per-number outbound kill switch. true = patient may dial this number (subject to global rules). false = block outbound to this number even though it remains on the global whitelist. Defaults to true when an administrator adds a number. |
Enforcement notes for RingCX:
- When top-level
any_calls_allowedistrue, a patient may dial a number that appears inglobal_whitelistwithany_calls_allowed: trueeven if it is not inallowed_callers. - Numbers with
any_calls_allowed: falseare still returned so the enforcement layer can distinguish "not on global whitelist" from "on global whitelist but blocked". - When the global kill switch is enabled,
global_whitelistis returned unchanged (each item keeps itsany_calls_allowedvalue) but top-levelany_calls_allowedisfalse. Enforcement must treat this as no outbound allowed regardless ofglobal_whitelistflags — the same pattern asallowed_callers.
If the administrator has enabled the global outbound kill switch (system_settings.outbound_calling_blocked = true), the API still returns 200 on valid credentials but forces:
- top-level
any_calls_allowed→false - every
ward[].any_calls_allowed→false - every
ward[].device_dids[].device.any_calls_allowed→false
allowed_callers is returned unchanged (including each item's any_calls_allowed value).
global_whitelist is also returned unchanged. Enforcement logic must still respect the global block
(treat as no outbound allowed regardless of ward, device, allowed-number, or global-whitelist flags).
If the administrator has disabled outbound calling for a patient (patients.any_calls_allowed = false), the API still returns 200 on valid credentials but forces:
- top-level
any_calls_allowed→false - every
ward[].any_calls_allowed→false - every
ward[].device_dids[].device.any_calls_allowed→false
allowed_callers is returned unchanged. Enforcement logic must treat the patient as unable to
place outbound calls regardless of ward, device, or allowed-number flags.
When both the global and patient kill switches apply, the effective result is the same: no outbound calling is permitted.
{ "error": "Failed to authenticate patient" }| Status | Meaning |
|---|---|
200 |
Valid patient_id + PIN; rules payload returned. |
400 |
Malformed request (patient_id or pin validation failed). |
401 |
Invalid or missing API key. |
403 |
Valid request shape but authentication denied (unknown patient or wrong PIN). |
500 |
Unexpected server/database error. |
503 |
Authenticate endpoint not configured on the server. |
curl -s -X POST https://<host>/api-ext/patients/authenticate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <api-key>" \
-d '{"patient_id":"12345","pin":"0000"}'curl -s -o /dev/null -w "%{http_code}" -X POST https://<host>/api-ext/patients/authenticate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <api-key>" \
-d '{"patient_id":"12345","pin":"9999"}'
# → 403curl -s -X POST https://<host>/api-ext/patients/authenticate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wrong-key" \
-d '{"patient_id":"12345","pin":"0000"}'
# → 401 { "error": "Invalid API key" }This endpoint sits on the call path. RingCX will call it during call setup.
| Topic | Guidance |
|---|---|
| Latency | Target low, predictable response times. Avoid retries that delay call setup unnecessarily. |
| Availability | Outages affect whether calls can be placed. Fail-open vs fail-closed when this API is unreachable is a business decision — not defined in this repo. Document the chosen behaviour in the RingCX engagement. |
| TLS | Production must use HTTPS. |
| Idempotency | Read-only with respect to call policy (no side effects on success). Safe to retry on transport errors if RingCX deduplicates. |
| Rate limiting | Not implemented in the API today. Size RingCX concurrency accordingly. |
Every JSON property in this contract is snake_case, at every level of nesting, in both request and response bodies. Integrators must match the names exactly.
| Area | Properties |
|---|---|
| Request | patient_id, pin |
| Top-level success | patient_name, patient_id, any_calls_allowed, ward, allowed_callers, global_whitelist |
| Wards / devices | ward_name, any_calls_allowed, device_dids, device.did, device.any_calls_allowed |
| Allowed numbers | phone_number, name, relationship, any_calls_allowed, allow_recording |
| Global whitelist | phone_number, name, any_calls_allowed |
| Denied response | patient_id, any_calls_allowed |
| Errors | error |
Note:
any_calls_allowedappears at five levels — patient (top-level), ward, device, allowed-number, and global-whitelist. These are independent flags that happen to share a name. This document always qualifies which one is meant (for example "top-levelany_calls_allowed"); rely on the nesting, not the name alone.
Earlier revisions mixed camelCase, snake_case and one PascalCase field, carried over from a legacy consumer that is no longer supported. Those spellings are removed, not aliased — the old names are neither accepted nor returned.
| Old | New |
|---|---|
AnyCallsAllowed (denied response) |
any_calls_allowed |
anyCallsAllowed (patient, ward, device) |
any_calls_allowed |
wardName |
ward_name |
deviceDIDs |
device_dids |
Request fields (patient_id, pin), the allowed-number and global-whitelist items, and
the error body were already snake_case and are unchanged.
The following are not specified here and belong to the separate RingCX integration:
- When during call setup to call this endpoint
- How to map
ward/device_dids/allowed_callers(includingany_calls_allowedandallow_recording) to allow/block/record actions - Precedence when ward-level, device-level, and allowed-number kill switches conflict
- Behaviour when the API times out or returns
500/503 - Inbound call handling (this system governs outbound only)