Payerbox Docs

Authentication

Payerbox supports three OAuth 2.0 flows:

  • SMART App Launch — member-authorized authorization code grant for third-party apps reading a single member's data (Patient Access).
  • SMART Backend Services — system-to-system asymmetric JWT (private_key_jwt) for Provider Access, Payer-to-Payer, PAS.
  • Client Credentials — symmetric client_id + client_secret grant for trusted internal services, sandboxes, and quick integrations.

SMART flows follow the HL7 SMART App Launch IG 2.2.0. The OAuth 2.0 RFC is RFC 6749.

Endpoint discovery

Fetch the SMART configuration document before configuring a client — it advertises the live token / authorization / JWKS URLs, supported scopes, signing algorithms, grant types, and capabilities.

GET <base>/.well-known/smart-configuration

Key fields Payerbox returns:

FieldValue
authorization_endpoint<base>/auth/authorize
token_endpoint<base>/auth/token
introspection_endpoint<base>/auth/introspect
jwks_uri<base>/.well-known/jwks.json
grant_types_supportedauthorization_code, client_credentials, password, implicit, urn:ietf:params:oauth:grant-type:token-exchange
token_endpoint_auth_methods_supportedclient_secret_basic, client_secret_post, private_key_jwt
token_endpoint_auth_signing_alg_values_supportedRS384, ES384
code_challenge_methods_supportedS256
capabilitieslaunch-standalone, launch-ehr, client-public, client-confidential-symmetric, client-confidential-asymmetric, permission-patient, permission-user, permission-v1, permission-v2, permission-offline, sso-openid-connect
scopes_supportedopenid, profile, email, launch, launch/patient, patient/*.cruds, system/*.cruds, user/*.cruds, fhirUser, offline_access, online_access

Endpoints

EndpointUsed by
<base>/auth/authorizeSMART App Launch — authorization redirect
<base>/auth/tokenToken issuance (all flows)
<base>/auth/introspectRFC 7662 token introspection
<base>/auth/userinfoOpenID Connect UserInfo
<base>/.well-known/jwks.jsonServer's public keys (used by clients to validate Payerbox-issued JWTs)
<base>/.well-known/smart-configurationSMART discovery document
<base>/fhir/metadataFHIR CapabilityStatement

SMART App Launch (member-authorized)

Used by third-party apps in the FHIR App Portal. The member discovers an app in the FHIR App Gallery, clicks Launch, signs in, and grants the requested scopes.

Client registration

Each SMART app is registered as an Aidbox Client resource with type: smart-app and a code grant type:

PUT /Client/my-public-smart-app
Content-Type: application/json

{
  "resourceType": "Client",
  "id": "my-public-smart-app",
  "type": "smart-app",
  "active": true,
  "grant_types": ["code"],
  "auth": {
    "authorization_code": {
      "redirect_uri": "https://app.example.com/redirect",
      "token_format": "jwt",
      "access_token_expiration": 3600,
      "refresh_token": true,
      "secret_required": false,
      "pkce": true
    }
  },
  "smart": {"launch_uri": "https://app.example.com/launch"}
}
PUT /Client/my-confidential-smart-app
Content-Type: application/json

{
  "resourceType": "Client",
  "id": "my-confidential-smart-app",
  "type": "smart-app",
  "active": true,
  "grant_types": ["code"],
  "secret": "<client-secret>",
  "auth": {
    "authorization_code": {
      "redirect_uri": "https://app.example.com/redirect",
      "token_format": "jwt",
      "access_token_expiration": 3600,
      "refresh_token": true,
      "secret_required": true,
      "pkce": true
    }
  },
  "smart": {"launch_uri": "https://app.example.com/launch"}
}

Public apps (single-page apps, native apps without a server-side component) must use PKCE; confidential apps may opt in to PKCE as defence-in-depth.

Authorization request

GET <base>/auth/authorize?<params>
ParameterDescription
response_typeFixed value code.
client_idClient resource id.
redirect_uriMust match the registered Client.auth.authorization_code.redirect_uri.
scopeSpace-separated SMART scopes. Include launch/patient for standalone or launch for EHR launch, plus openid fhirUser for identity and offline_access to receive a refresh token.
audFHIR base URL the app intends to call (typically <base>/fhir).
stateOpaque value used to prevent CSRF; the server echoes it on the redirect back.
code_challenge + code_challenge_methodRequired for public apps (PKCE). code_challenge_method is always S256.
launchEHR launch only — the JWT identifier the EHR passed to the app.

Standalone launch

The user picks the app from outside the EHR. The app goes straight to /auth/authorize with scope=launch/patient ..., then exchanges the returned code at /auth/token.

EHR launch

The EHR initiates a launch by opening the app at Client.smart.launch_uri with iss=<base>/fhir and launch=<launch-jwt>. The app then performs the same authorization code flow, passing the launch value back to /auth/authorize.

The launch JWT is signed by Aidbox and carries the launch context:

ClaimValue
clientSMART client id
userAidbox user id
ctx.patientPatient id
expExpiration (seconds since epoch)

A helper RPC builds a complete launch URL:

POST /rpc
Content-Type: application/json

{
  "method": "aidbox.smart/get-launch-uri",
  "params": {
    "client": "my-public-smart-app",
    "user": "<user-id>",
    "iss": "<base>/fhir",
    "ctx": {"patient": "<patient-id>"}
  }
}

Token request

After receiving the authorization code, exchange it for an access token:

POST /auth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<code>
&redirect_uri=https://app.example.com/redirect
&client_id=my-public-smart-app
&code_verifier=<verifier>

Confidential clients omit client_id from the body and authenticate with Authorization: Basic <base64(client_id:client_secret)> (or send client_id + client_secret in the body).

{
  "token_type": "Bearer",
  "access_token": "<jwt>",
  "refresh_token": "<jwt>",
  "id_token": "<jwt>",
  "scope": "launch/patient openid fhirUser offline_access patient/*.read",
  "patient": "<patient-id>",
  "expires_in": 3600,
  "need_patient_banner": true
}

Refresh token

If the app requested offline_access it can refresh the access token without user interaction:

POST /auth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=<refresh-token>
&client_id=my-public-smart-app

SMART Backend Services (system-to-system)

Used by Provider Access, Payer-to-Payer, and PAS. The client signs a JWT with its private key; Payerbox verifies it against the client's JWKS endpoint registered at onboarding. No client secret is shared.

Client registration

Each backend partner is registered as an Aidbox Client resource with grant_types: ["client_credentials"], auth.client_credentials.client_assertion_types: ["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"], and a jwks_uri (or inline jwks) carrying the partner's public keys:

PUT /Client/partner-payer
Content-Type: application/json

{
  "resourceType": "Client",
  "id": "partner-payer",
  "active": true,
  "grant_types": ["client_credentials"],
  "auth": {
    "client_credentials": {
      "client_assertion_types": ["urn:ietf:params:oauth:client-assertion-type:jwt-bearer"],
      "token_format": "jwt",
      "access_token_expiration": 300
    }
  },
  "jwks_uri": "https://partner.example.com/.well-known/jwks.json",
  "scope": ["system/*.read"],
  "details": {
    "identifier": [{"system": "http://hl7.org/fhir/sid/us-npi", "value": "<NPI>"}]
  }
}

For operations that require the caller's NPI ($bulk-member-match, $provider-member-match), the NPI must be present on Client.details.identifier[system=http://hl7.org/fhir/sid/us-npi]. Aidbox rejects top-level Client.identifier, so the NPI lives under details.

Client assertion JWT

The client signs a short-lived JWT with its private key. Payerbox verifies the signature against the client's JWKS.

Header

FieldValue
algRS384 or ES384
kidKey id matching one entry in the client's JWKS
typJWT
jku (optional)TLS-protected URL of the client's JWK Set — must match Client.jwks_uri when present

Claims

ClaimValue
issClient id
subClient id (same as iss)
audToken endpoint URL — <base>/auth/token
expExpiration (≤ 5 minutes from now)
jtiUnique nonce — prevents replay

Token request

POST /auth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&scope=system/Patient.read+system/ExplanationOfBenefit.read
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<signed-JWT>
{
  "token_type": "Bearer",
  "access_token": "<jwt>",
  "scope": "system/Patient.read system/ExplanationOfBenefit.read",
  "expires_in": 300
}

Client JWKS

At onboarding, the partner registers either a jwks_uri (preferred — Payerbox refreshes the cached keys per the Cache-Control header) or an inline jwks array. Each JWK must include kty and kid; RSA keys also need n and e, EC keys need crv, x, and y. Key rotation is non-breaking — publish the new key, keep the old one until clients have rotated, then retire the old key.

Client Credentials

Plain OAuth 2.0 client_credentials grant with a pre-shared client_id + client_secret. Intended for trusted internal services, local stacks, and the quickstart — not for production payer-to-payer or provider integrations, which must use SMART Backend Services (asymmetric).

Client registration

PUT /Client/my-service
Content-Type: application/json

{
  "resourceType": "Client",
  "id": "my-service",
  "active": true,
  "grant_types": ["client_credentials"],
  "secret": "<client-secret>"
}

An access policy must grant the client access to whatever resources it will read or write.

Token request

POST /auth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=my-service
&client_secret=<client-secret>
POST /auth/token
Authorization: Basic <base64(my-service:<client-secret>)>
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
{
  "token_type": "Bearer",
  "access_token": "<token>",
  "need_patient_banner": true
}

Use the returned access_token as Authorization: Bearer <token> on any FHIR request.

Scopes

Payerbox supports both SMART v1 and v2 scope syntax (capabilities permission-v1 and permission-v2 are both advertised).

ContextUse
patient/Member-scoped (SMART App Launch). The access token's context.patient claim identifies which member.
system/System-level (Backend Services / Client Credentials). No per-member filter; access is governed by access policies, attribution, and consent.
user/User-scoped — used when a logged-in clinician launches an app.

Permissions:

  • v1 — read, write, * (e.g. patient/*.read, system/Claim.write).
  • v2 — granular CRUDS letters: c (create), r (read), u (update), d (delete), s (search). Wildcard * allowed (e.g. patient/*.cruds, system/Patient.rs).

Scopes with search parameters (v2)

Append a FHIR search query to a v2 read/search scope to narrow what the token can pull:

patient/Observation.rs?status=final

Aidbox auto-applies the filter to every search and read under that scope. Disallowed inside c / u / d permissions and disallowed with _include, _revinclude, _has, _assoc, _with.

Common scope sets

SurfaceRecommended scopes
Patient Access app (read-only)openid fhirUser patient/*.read offline_access
Provider Access (Backend Services)system/*.read
Payer-to-Payer (Backend Services)system/*.read
PAS (Backend Services)system/Claim.cu system/ClaimResponse.r

Full inventory of what a given deployment exposes is in the SMART configuration document's scopes_supported.

Common errors

HTTPOAuth errorCause
400invalid_requestMissing or malformed parameter
400invalid_grantAuthorization code expired, already used, or refresh token invalid
400invalid_clientUnknown client_id, wrong secret, or client assertion signature did not verify against the registered JWKS
400invalid_scopeRequested scope not allowed for this client
401invalid_tokenAccess token expired, revoked, or signature invalid
403insufficient_scopeToken lacks the required scope for the resource (returned as OperationOutcome from /fhir)

Last updated: