Skip to main content
Guide

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:

ClaimRequiredDescription
issyesYour applicationIdentifier.
subyesThe subject. Equal to iss for application-restricted calls; the staff member's identifier for user-restricted calls.
iatyesIssued-at (Unix time).
expyesExpiry (Unix time). Must be no more than 60 seconds after iat. Mint a fresh token per request or short burst.
azpconditionalAuthorised party: your applicationIdentifier. Required when sub ≠ iss (user-restricted).
actconditionalActor. 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 an active authorisation (see User-restricted authorisation below for how one is established and how long it lasts), and azp must 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, an authorisation for that staff member and your application must exist. There are two ways this can happen: the standard interactive flow below, which any integration uses, and a patient-scoped authorisation that Medicus sets up for specific integrations. Both result in the same thing, a username to sign into sub on your tokens, so the tokens themselves are identical either way; what differs is how the authorisation is established and, for the patient-scoped kind, which patient your calls are allowed to touch.

Standard path: interactive authorisation

This is a browser redirect, not a token exchange:

  1. 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.

    For example, on Staging for Dr MA Baird's Practice:

    https://staging.england.medicus.health/a30005/staff/authorise?app=your-application-identifier&redirect=https://your-app.example/callback
  2. Medicus prompts the user to sign in (if needed) and approve the request.

  3. On approval, Medicus records a standing authorisation valid 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 as sub on subsequent user-restricted tokens) and expiresAt (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.

  4. While that authorisation is valid, your user-restricted tokens for that user (sub is the user, azp is 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.

Patient-scoped authorisation

For some integrations, the action that should trigger authorisation happens inside Medicus rather than in your application, so the interactive redirect above doesn't apply. In that case Medicus can instead mint an authorisation scoped to one specific patient: it lasts 12 hours, the same as the standard flow, and is reusable for any number of calls within that window. The difference is scope, not lifetime or reusability: your user-restricted calls for that user are only accepted while they act on the one patient the authorisation names. A call naming any other patient is rejected, even while the authorisation is otherwise valid:

{ "errors": [{ "code": "patient-not-authorised", "description": "This app is not currently authorised to request as the specified user for this patient." }] }

Medicus delivers the resulting username to your application through whatever channel fits that integration; there is no Transactional API endpoint to request one yourself, and this only exists for integrations Medicus has specifically set up this way.

A patient-scoped authorisation and a standing authorisation for the same user and application are tracked independently. Having one doesn't consume or shorten the other, so a standing authorisation from the interactive flow keeps working normally, with no patient restriction, regardless of any patient-scoped authorisation also in play.

Some endpoints (for example retrieve care record) additionally check that the named user is allowed to access the specific patient in the request, on top of the patient-scope check above.

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 kid values, which is how you rotate keys (and how several engineers can each use their own key).
  • The kid has no required format (a name, a UUID, a timestamp all work); it only has to match a kid in 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"