Authentication
Every Transactional API request is authenticated with a short-lived JWT sent as a bearer token:
Authorization: Bearer <jwt>
Your application signs each token with its own private key (RS256). Medicus verifies it against your application's public keys, which it fetches from the JWKS URL registered for your application. Medicus never holds your private key.
The token
Tokens are signed with RS256 and must include a kid header identifying the
signing key. Claims:
| Claim | Required | Description |
|---|---|---|
iss | yes | Your applicationIdentifier. |
sub | yes | The subject. Equal to iss for application-restricted calls; the staff member's identifier for user-restricted calls. |
iat | yes | Issued-at (Unix time). |
exp | yes | Expiry (Unix time). Must be no more than 60 seconds after iat. Mint a fresh token per request or short burst. |
azp | conditional | Authorised party: your applicationIdentifier. Required when sub ≠ iss (user-restricted). |
act | conditional | Actor. Permitted only for application-restricted calls (sub = iss). |
Medicus allows up to 60 seconds of clock skew when validating iat and exp,
but tokens are intended to be short-lived.
Application-restricted vs user-restricted
- Application-restricted (
sub = iss): the application acts as itself. - User-restricted (
sub ≠ iss): the application acts on behalf of a named staff member. This additionally requires that the staff member has authorised your application (authorisations expire after 12 hours), andazpmust be set.
Acting on behalf of a practitioner (act)
An application-restricted call can name the human and organisation it is acting
for by including an act claim. This is used for audit when your application is
the trusted caller:
"act": {
"practitioner": {
"name": "Dr Test Jones",
"email": "gppartner@medicus.health",
"gmc_number": "G123123"
},
"organisation": {
"name": "UCL Hospital",
"ods_code": "R125"
}
}
Required: act.practitioner.name, act.practitioner.email, and
act.organisation.name. The gmc_number (7-digit GMC number, e.g. 1234567)
and ods_code fields are optional.
User-restricted authorisation
Before your application can act as a given staff member, that staff member must authorise it. This is a browser redirect, not a token exchange:
-
Redirect the user to the Medicus authorisation page for their tenant, passing your application id (
app) and a callback URL (redirect):https://england.medicus.health/{tenantId}/staff/authorise?app={applicationId}&redirect={yourCallbackUrl}On Staging the host is
https://staging.england.medicus.health. The callback may use a custom URI scheme. -
Medicus prompts the user to sign in (if needed) and approve the request.
-
On approval, Medicus records the authorisation for 12 hours and redirects the browser to your callback URL, with two query parameters appended:
username(the authorising staff member's Medicus username, the same value to use assubon subsequent user-restricted tokens) andexpiresAt(an ISO 8601 timestamp for when the 12-hour authorisation expires). Your original query parameters, if any, are preserved. There is no further token exchange. -
While that authorisation is valid, your user-restricted tokens for that user (
subis the user,azpis your application id) are accepted. Once the 12 hours lapse, requests return the "not currently authorised" 403 below. Catch that response and redirect the user through the authorisation flow again.
Some endpoints (for example retrieve care record) additionally check that the named user is allowed to access the specific patient in the request.
Endpoint access
Each endpoint your application can call is granted individually, per
authentication mode, in the form <endpoint>:<mode>, e.g.:
create-note:application-restricted
retrieve-care-record:user-restricted
Calling an endpoint your application has not been granted (in the mode you are using) is rejected. Endpoint access is configured by Medicus when your application is onboarded.
Enablement
Beyond endpoint access, your application must be enabled for the specific tenant (practice) you are integrating with. A practice administrator enables (and can disable) your application through Medicus. You do not need to contact Medicus each time a new practice goes live. Disabled or un-enabled applications are rejected.
Authentication and authorisation failures
Requests that fail authentication or authorisation are rejected with HTTP 403
before the endpoint runs, with a body of the form
{ "errors": [{ "code": "unauthorized", "description": "<message>" }] }.
Some examples, by category:
Missing or malformed request
{ "errors": [{ "code": "unauthorized", "description": "Missing Authorization header" }] }
{ "errors": [{ "code": "unauthorized", "description": "Invalid Authorization format" }] }
Invalid or expired token
{ "errors": [{ "code": "unauthorized", "description": "Invalid JWT: Expired token" }] }
{ "errors": [{ "code": "unauthorized", "description": "Invalid JWT: Signature verification failed" }] }
Missing required claim
{ "errors": [{ "code": "unauthorized", "description": "JWT is missing exp (expiration time) claim" }] }
{ "errors": [{ "code": "unauthorized", "description": "JWT is missing azp (authorizing party) claim if sub is not iss" }] }
Your JWKS could not be read
Your JWKS URL did not respond, did not return JSON, or returned a document with no key Medicus can use. The description carries the underlying reason and the URL that was fetched.
{ "errors": [{ "code": "unauthorized", "description": "Unable to obtain JWKS: JWK Set did not contain any keys (https://example.com/.well-known/jwks.json)" }] }
{ "errors": [{ "code": "unauthorized", "description": "Unable to obtain JWKS: Failed to fetch JWKS from https://example.com/.well-known/jwks.json, HTTP 404" }] }
Application not enabled for this tenant
{ "errors": [{ "code": "unauthorized", "description": "This app has not been granted access to this tenant." }] }
Endpoint not granted
{ "errors": [{ "code": "unauthorized", "description": "This app has not got permission to call the create-note API endpoint (application-restricted)." }] }
User-restricted authorisation expired or missing
{ "errors": [{ "code": "unauthorized", "description": "This app is not currently authorised to request as the specified user." }] }
Keys and registration
- Your application is registered once (a single JWKS URL and its granted endpoints); individual practices then enable it. There is no per-practice registration.
- A single JWKS may hold multiple keys with different
kidvalues, which is how you rotate keys (and how several engineers can each use their own key). - The
kidhas no required format (a name, a UUID, a timestamp all work); it only has to match akidin your JWKS exactly. - Medicus caches your JWKS for about an hour, so allow for that lag when rotating keys. Publish the new key alongside the old one and keep both served until the lag has passed, rather than replacing one with the other.
- Only a key set Medicus can read is cached. If your JWKS URL is briefly down, or serves an empty or unreadable key set, requests are refused for as long as that lasts and start succeeding again on the first request after you fix it. You do not have to wait out the cache.
Signing a token
Mint the token with any RS256 JWT library, signing with your private key and
setting the kid header. For example:
// Node.js (jsonwebtoken)
import jwt from 'jsonwebtoken';
import { readFileSync } from 'node:fs';
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{ iss: 'your-app', sub: 'your-app', iat: now, exp: now + 60 },
readFileSync('private-key.pem'),
{ algorithm: 'RS256', header: { kid: 'your-key-id' } },
);
Example
Header:
{ "alg": "RS256", "typ": "JWT", "kid": "your-key-id" }
Payload (application-restricted):
{
"iss": "your-app",
"sub": "your-app",
"iat": <unix-timestamp>,
"exp": <unix-timestamp + 60>
}
Then send it:
curl https://{tenantId}.api.england.medicus.health/transactional-api/ping \
-H "Authorization: Bearer $MEDICUS_JWT"