For AI agents: the documentation index is at /docs/payerbox/llms.txt. A Markdown version of this page is available at /docs/payerbox/api-reference/authentication.md or by requesting it with the Accept: text/markdown header.
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"]
}

Clients calling member-match or Payer-to-Payer operations also need the client-hl7B2b extension on the same resource.

HL7 B2B authorization extension (UDAP)

Member-match and Payer-to-Payer operations identify the calling organization from the UDAP HL7 B2B Authorization Extension, not from any Client field. Payerbox reads it from the access token JWT under extensions.hl7-b2b:

{
  "sub": "partner-payer",
  "extensions": {
    "hl7-b2b": {
      "version": "1",
      "organization_id": "http://hl7.org/fhir/sid/us-npi#1234567893",
      "organization_name": "Partner Payer, Inc.",
      "purpose_of_use": ["http://terminology.hl7.org/CodeSystem/v3-ActReason#HPAYMT"]
    }
  }
}

The claim is not emitted by default. Aidbox builds it from a client-hl7B2b extension on the Client plus the Organization it points to. All three must hold:

  1. Client.auth.<grant>.token_format is "jwt". Opaque tokens carry no claims.
  2. Client.extension carries client-hl7B2b with organization and organizationIdentifierSystem.
  3. The referenced Organization exists and has an identifier entry whose system equals organizationIdentifierSystem.

Otherwise the token is issued without extensions.hl7-b2b and the operations below reject the caller.

client-hl7B2b extension

URL: http://health-samurai.io/fhir/core/StructureDefinition/client-hl7B2b (context: Client).

Sub-extensionCardinalityTypeContributes to the token
organization1..1Reference(Organization)organization_id and organization_name. Must resolve to an Organization in this deployment.
organizationIdentifierSystem1..1uriPicks which Organization.identifier.system becomes organization_id, as <system>#<value>
purposeOfUse0..*CodingOne <system>#<code> string per entry in purpose_of_use

version is always "1". The Organization is read at token issuance, so identifier or name edits apply to the next token.

purposeOfUse is bound (extensible) to http://health-samurai.io/fhir/core/ValueSet/b2b-purpose-of-use: TREAT, HPAYMT, HOPERAT, PUBHLTH, HRESCH, PATRQT, ETREAT. UDAP requires purpose_of_use, and Aidbox emits it only when at least one purposeOfUse is registered. Payer-to-Payer typically uses HPAYMT, Provider Access TREAT.

Registering a B2B client

Create the Organization first. Its identifier is what the token carries.

PUT /fhir/Organization/partner-payer-org
Content-Type: application/fhir+json

{
  "resourceType": "Organization",
  "id": "partner-payer-org",
  "name": "Partner Payer, Inc.",
  "identifier": [
    {"system": "http://hl7.org/fhir/sid/us-npi", "value": "1234567893"}
  ]
}
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"],
  "extension": [
    {
      "url": "http://health-samurai.io/fhir/core/StructureDefinition/client-hl7B2b",
      "extension": [
        {"url": "organization", "valueReference": {"reference": "Organization/partner-payer-org"}},
        {"url": "organizationIdentifierSystem", "valueUri": "http://hl7.org/fhir/sid/us-npi"},
        {"url": "purposeOfUse", "valueCoding": {"system": "http://terminology.hl7.org/CodeSystem/v3-ActReason", "code": "HPAYMT"}}
      ]
    }
  ]
}

Swap jwks_uri and client_assertion_types for a secret to get the symmetric variant, for local stacks and sandboxes only. The extension sits at the top level of Client, next to auth. Client has no identifier element, so organization identity travels through this extension.

Write the Client through the Aidbox base endpoint (PUT /Client/<id> or a POST / transaction Bundle), not through /fhir. With BOX_FHIR_CORRECT_AIDBOX_FORMAT enabled (the Payerbox default), /fhir rewrites the extension's valueReference / valueUri / valueCoding into Aidbox's internal union format (value: {"Reference": …}), which the token builder does not read. The write succeeds and tokens are still issued, but without extensions.hl7-b2b.

Identifier systems

organization_id is a single string in <system-uri>#<value> form, split on the last #. Any identifier system works: NPI, NAIC, or CARIN BB payerid.

CARIN BB payerid is itself a CodeSystem#code pair, so the resulting claim carries two #:

http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType#payerid#PAYER-123

Splitting on the last # keeps payerid on the system side. Register the Organization.identifier.system as http://hl7.org/fhir/us/carin-bb/CodeSystem/C4BBIdentifierType#payerid and set organizationIdentifierSystem to the same string.

Operations that read the claim:

OperationUses organization_id for
$bulk-member-matchRequesting-payer identity; stamped onto MatchedMembers; matched against Consent provision.actor[role=IRCP]
$provider-member-matchCaller identity
$davinci-data-export (payertopayer)Gates the opt-in export and resolves the requesting payer

A non-admin caller without the claim gets 403 from the member-match operations and 401 from $davinci-data-export.

Claim missing from an issued token

Decode the access token and check:

  1. Token is not a JWT. The grant config lacks token_format: "jwt".
  2. Extension missing or reshaped. GET /Client/<id>: every sub-extension must still read valueReference / valueUri / valueCoding under the exact urls above. A value: {"Reference": …} spelling means it went in through /fhir; rewrite it through the Aidbox base endpoint.
  3. organization does not resolve. The referenced Organization is absent from this deployment.
  4. System mismatch. The Organization has no identifier whose system is exactly organizationIdentifierSystem. A trailing slash or an http/https difference drops the whole claim.

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: