Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
31625d9
[docs] Add SMART Health Cards ($health-cards-issue) page
spicyfalafel Jul 24, 2026
99a8f79
[docs] OrgBAC: document $health-cards-issue org-scoped route
spicyfalafel Aug 3, 2026
e3d5787
[docs] SMART Health Cards: available since 2608
spicyfalafel Aug 3, 2026
01c9547
[docs] OrgBAC $health-cards-issue: available since 2608
spicyfalafel Aug 3, 2026
91375cb
[docs] Move SMART Health Cards to the end of the Other list
spicyfalafel Aug 3, 2026
dc4c177
[docs] SMART Health Cards: trim the Configuration paragraph
spicyfalafel Aug 3, 2026
ae3afe7
[docs] SMART Health Cards: trim intro, add env export, drop repeatabl…
spicyfalafel Aug 3, 2026
f990c62
[docs] SMART Health Cards: set the key via deployment env, not shell …
spicyfalafel Aug 3, 2026
8a884f4
[docs] SMART Health Cards: clarify AccessPolicy under OrgBAC
spicyfalafel Aug 3, 2026
72f3195
[docs] SMART Health Cards: correct OrgBAC access (policies still requ…
spicyfalafel Aug 3, 2026
4ff4063
[docs] SMART Health Cards: shorten OrgBAC section, add org AccessPoli…
spicyfalafel Aug 3, 2026
5dd5540
[docs] SMART Health Cards: clearer access-control (what to configure)…
spicyfalafel Aug 3, 2026
97b227e
[docs] SMART Health Cards: drop broad client policy, rewrite credenti…
spicyfalafel Aug 3, 2026
261c2b1
[docs] SMART Health Cards: add a credentialValueSet example request
spicyfalafel Aug 3, 2026
3784627
[docs] SMART Health Cards: drop the tightening note and examples link
spicyfalafel Aug 3, 2026
c7dcc7e
[docs] SMART Health Cards: list common credentialValueSets + spec link
spicyfalafel Aug 3, 2026
1ac0c55
[docs] SMART Health Cards: env-var block in Configuration, wording fixes
spicyfalafel Aug 3, 2026
8727a7e
Fix SMART Health Cards access-control policies and credentialValueSet…
spicyfalafel Aug 3, 2026
c2ea270
Document where to get SMART Health Cards value sets and how to resolv…
spicyfalafel Aug 3, 2026
e7c632d
Trim credentialValueSet source list to the IG plus the hybrid-engine …
spicyfalafel Aug 3, 2026
67935db
Set SMART Health Cards introduced version back to 2607
spicyfalafel Aug 3, 2026
13a5a11
Remove the legacy credential types section (content-based model only)
spicyfalafel Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
* [$meta](api/rest-api/other/meta.md)
* [$meta-add](api/rest-api/other/meta-add.md)
* [$meta-delete](api/rest-api/other/meta-delete.md)
* [SMART Health Cards](api/rest-api/other/smart-health-cards.md)
* [Bulk API](api/bulk-api/README.md)
* [Configure Access Policies for Bulk API](api/bulk-api/configure-access-policies-for-bulk-api.md)
* [$dump](api/bulk-api/dump.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,16 @@ GET <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/Composition/$document

See also [$document endpoint](../../../api/rest-api/other/document.md)

### $health-cards-issue

```
POST <AIDBOX_BASE_URL>/Organization/<org-id>/fhir/Patient/<patient-id>/$health-cards-issue
```

Issues a SMART Health Card scoped to the organization: the target `Patient` and every internal read run inside the organization's compartment. Available starting from version 2607.

See also [SMART Health Cards](../../../api/rest-api/other/smart-health-cards.md)

### History

Resource full history
Expand Down
317 changes: 317 additions & 0 deletions docs/api/rest-api/other/smart-health-cards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
---
description: >-
Issue signed SMART Health Cards from a patient's clinical data with the
$health-cards-issue operation.
---

# SMART Health Cards

Since the 2607 version, Aidbox can issue [SMART Health Cards](https://smarthealth.cards/): verifiable health credentials packaged as a signed, compact JSON Web Signature (JWS) that a patient presents as a QR code or file. The [`$health-cards-issue`](https://spec.smarthealth.cards/#via-fhir-health-cards-issue-operation) operation issues a signed card from the patient's clinical resources.

Aidbox publishes the verification public key at a JWKS endpoint, so any SMART Health Cards verifier can validate the cards.

## Configuration

The operation signs cards with an issuer **EC P-256 private key**, set as a PEM in `module.health-cards-links.issuer-private-key`; the public key and JWKS are derived from it.

Generate a key with OpenSSL and set it as the issuer key:

```bash
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out issuer.pem
```

```
BOX_MODULE_HEALTH_CARDS_LINKS_ISSUER_PRIVATE_KEY=<your-key>
```

{% hint style="warning" %}
Keep the private key secret and back it up. Rotating it changes the `kid`, so cards signed with the previous key stop verifying against the published JWKS.
{% endhint %}

## Issuing a card

```http
POST /fhir/Patient/<patient-id>/$health-cards-issue
```

The body is a `Parameters` resource. `credentialType` is required; the other parameters are optional.

| Parameter | Type | Description |
| ---------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `credentialType` | uri / string | Resource types the card should carry. Required, repeatable. See [credentialType](smart-health-cards.md#credentialtype). |
| `credentialValueSet` | uri / string | Restrict included resources to members of a ValueSet. Repeatable. See [credentialValueSet](smart-health-cards.md#credentialvalueset). |
| `includeIdentityClaim` | string / boolean | Which `Patient` identity fields to include. See [includeIdentityClaim](smart-health-cards.md#includeidentityclaim). |
| `_since` | dateTime / instant | Only include resources modified at or after this instant (`_lastUpdated ge`). |

### Example request

```http
POST /fhir/Patient/pt-1/$health-cards-issue
```

```json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "credentialType",
"valueUri": "Immunization"
}
]
}
```

### Example response

```json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "verifiableCredential",
"valueString": "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6Ii4uLiJ9.<deflated-payload>.<signature>"
},
{
"name": "resourceLink",
"part": [
{ "name": "vcIndex", "valueInteger": 0 },
{ "name": "bundledResource", "valueUri": "resource:0" },
{
"name": "hostedResource",
"valueUri": "https://<base>/fhir/Patient/pt-1"
}
]
},
{
"name": "resourceLink",
"part": [
{ "name": "vcIndex", "valueInteger": 0 },
{ "name": "bundledResource", "valueUri": "resource:1" },
{
"name": "hostedResource",
"valueUri": "https://<base>/fhir/Immunization/imm-1"
}
]
}
]
}
```

`verifiableCredential.valueString` is the signed SMART Health Card (a compact JWS). Each `resourceLink` maps a bundled resource (`resource:N`) to its source Aidbox resource (`hostedResource`).

## Input parameters

### credentialType

The operation accepts any FHIR resource type in the Patient compartment, such as `Immunization`, `Observation`, or `Condition`. It searches the patient's compartment for that type and adds the matching resources to the card.

`credentialType` is required (at least one). Aidbox combines multiple values with logical AND: the card carries every requested type the patient has. Aidbox rejects a missing or unsupported `credentialType` with `400`.

### credentialValueSet

Keeps only resources whose clinical code is a member of the given ValueSet. For example, supply a ValueSet of COVID-19 vaccine codes to include only COVID-19 immunizations. Aidbox validates the resource's coded fields (`vaccineCode`, `code`, `medicationCodeableConcept`, and `type`) against the ValueSet with [`$validate-code`](validate.md), so the ValueSet must be loaded into Aidbox's terminology. An unresolvable ValueSet fails the request with `400`.

For example, a COVID-19-only immunization card:

```json
{
"resourceType": "Parameters",
"parameter": [
{ "name": "credentialType", "valueUri": "Immunization" },
{
"name": "credentialValueSet",
"valueUri": "https://terminology.smarthealth.cards/ValueSet/immunization-covid-cvx"
}
]
}
```

Passing several `credentialValueSet` parameters combines them as a card-level logical AND: a resource is kept if it matches at least one, and the card is issued only when every ValueSet matched some resource (otherwise `404`). So passing a COVID-19 vaccine ValueSet and an mpox vaccine ValueSet requests a card that carries a vaccine from each.

Any ValueSet that Aidbox's terminology can resolve works. The standard SMART Health Cards value sets come from the [SMART Health Cards terminology IG](https://terminology.smarthealth.cards/artifacts.html); load the ones you need into Aidbox as `ValueSet` resources. Their codes come from external code systems (CVX, SNOMED CT, LOINC), so also run the [terminology engine](../../../terminology-module/aidbox-terminology-module/hybrid.md) in `hybrid` mode against a terminology server that provides those code systems, otherwise the codes will not validate.

The common value sets, under `https://terminology.smarthealth.cards/ValueSet/`:

| ValueSet | Contents |
| --- | --- |
| `immunization-covid-all` | COVID-19 vaccines (CVX, SNOMED CT, ICD-11) |
| `immunization-covid-cvx` | COVID-19 vaccines (CVX) |
| `immunization-orthopoxvirus-all` | mpox / orthopoxvirus vaccines |
| `immunization-all-cvx` | All CVX vaccine codes |
| `lab-qualitative-test-covid` | Qualitative COVID-19 lab tests (LOINC) |
| `lab-qualitative-result` | Qualitative infectious-disease lab results |

### includeIdentityClaim

Controls which `Patient` identity fields the card carries:

| Value | Effect |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------- |
| omitted or `true` | Default claims: `Patient.name` (family and given only) and `Patient.birthDate`. |
| `Patient.name`, `Patient.gender` (one `valueString` each) | Only the named fields. |
| `false` | Omit the `Patient` from the card entirely. |

### \_since

The operation includes only resources with `_lastUpdated` at or after the given instant, for example `2024-01-01T00:00:00Z`.

## Verifying cards (JWKS)

Aidbox publishes the issuer's public key as a JWK Set at a public endpoint:

```http
GET /health-cards/.well-known/jwks.json
```

```json
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "vbo4liL4JMgrGcYgAhVWreZPFMisG65EhM0p3Vdgzi8",
"y": "U3_QRvRc12uGpFiwBlX43vXbqYoVwGQ2obGZgF2gCYc",
"kid": "jDfy60_p2Gyvcl_sXayw0FcSECzZtrC7WUKrVaigVYI",
"use": "sig",
"alg": "ES256"
}
]
}
```

A verifier picks the right key by the `kid` in the card's JWS header. The card's issuer (`iss`) is `<base-url>/health-cards`, and its key set lives at `<iss>/.well-known/jwks.json`. Returns `404` until the issuer key is configured.

## Access control

A caller needs both permission to **invoke the operation** and permission for the **internal reads** it issues. The operation re-dispatches those reads through the regular pipeline, so your [Access Policies](../../../access-control/authorization/access-policies.md) gate them; a read the caller may not perform stops the card (for example `403`). The `credentialValueSet` terminology check (`$validate-code`) runs in-process and needs no policy.

For a single call the operation issues these authorized requests:

- `POST /fhir/Patient/{id}/$health-cards-issue`, the operation itself (`fhir-patient-health-cards-issue`).
- `GET /fhir/Patient/{id}?_elements=name,birthDate` reads the identity claims as `FhirRead` (skipped when `includeIdentityClaim` is `false`; `_elements` lists the requested claim fields).
- `GET /fhir/{type}?{compartment-param}=Patient/{id}` searches each requested `credentialType` as `FhirSearch`, using the type's Patient-compartment parameter (`patient` for `Immunization`, `subject` for `Observation` and `Condition`, and so on) and following `next` links to page through every match. Adds `&_lastUpdated=ge{_since}` when `_since` is set.

Each policy links to an operation. First, allow invoking the operation itself:

```json
{
"resourceType": "AccessPolicy",
"id": "health-cards-issuer-op",
"engine": "matcho",
"link": [{ "reference": "Operation/fhir-patient-health-cards-issue" }],
"matcho": { "client": { "id": "my-client-id" } }
}
```

Next, allow the type searches. Link to `FhirSearch` and, with `$one-of`, match the compartment parameter each requested type uses so the policy only permits patient-scoped searches:

```json
{
"resourceType": "AccessPolicy",
"id": "health-cards-issuer-search",
"engine": "matcho",
"link": [{ "reference": "Operation/FhirSearch" }],
"matcho": {
"client": { "id": "my-client-id" },
"params": {
"$one-of": [
{ "resource/type": "Immunization", "patient": "present?" },
{ "resource/type": { "$enum": ["Observation", "Condition"] }, "subject": "present?" }
]
}
}
}
```

List the resource types you request under the compartment parameter each one uses (`patient` for `Immunization`, `subject` for `Observation` and `Condition`). Finally, allow the `Patient` read:

```json
{
"resourceType": "AccessPolicy",
"id": "health-cards-issuer-read-patient",
"engine": "matcho",
"link": [{ "reference": "Operation/FhirRead" }],
"matcho": {
"client": { "id": "my-client-id" },
"params": { "resource/type": "Patient" }
}
}
```

### Organization scope (OrgBAC)

Under [organization-based access control](../../../access-control/authorization/scoped-api/organization-based-hierarchical-access-control.md), the operation is also served at:

```http
POST /Organization/<orgid>/fhir/Patient/<patient-id>/$health-cards-issue
```

It issues a card only from the organization's data: the target `Patient` and every internal read run inside the organization's compartment, so a `Patient` from another organization returns `403`. `hostedResource` URLs use the same `/Organization/<orgid>/fhir` base.

Access Policies still apply (the compartment restricts data but does not grant access). Under OrgBAC the operation and its reads run as the org-scoped operations `orgbac-fhir-health-cards-issue`, `orgbac-fhir-read`, and `orgbac-fhir-search` (kebab-case route ids, not the camel-case `FhirRead` / `FhirSearch`), so link the org policies to those. The three policies mirror the ones above with the ids swapped:

```json
{
"resourceType": "AccessPolicy",
"id": "health-cards-org-issuer-op",
"engine": "matcho",
"link": [{ "reference": "Operation/orgbac-fhir-health-cards-issue" }],
"matcho": { "client": { "id": "my-client-id" } }
}
```

```json
{
"resourceType": "AccessPolicy",
"id": "health-cards-org-issuer-search",
"engine": "matcho",
"link": [{ "reference": "Operation/orgbac-fhir-search" }],
"matcho": {
"client": { "id": "my-client-id" },
"params": {
"$one-of": [
{ "resource/type": "Immunization", "patient": "present?" },
{ "resource/type": { "$enum": ["Observation", "Condition"] }, "subject": "present?" }
]
}
}
}
```

```json
{
"resourceType": "AccessPolicy",
"id": "health-cards-org-issuer-read",
"engine": "matcho",
"link": [{ "reference": "Operation/orgbac-fhir-read" }],
"matcho": {
"client": { "id": "my-client-id" },
"params": { "resource/type": "Patient" }
}
}
```

The response is the same `Parameters`, with each `hostedResource` under the organization:

```json
{
"name": "resourceLink",
"part": [
{ "name": "vcIndex", "valueInteger": 0 },
{ "name": "bundledResource", "valueUri": "resource:1" },
{ "name": "hostedResource", "valueUri": "https://<base>/Organization/<orgid>/fhir/Immunization/imm-1" }
]
}
```

## Errors

The operation returns every error as an `OperationOutcome`.

| Status | When |
| ------ | --------------------------------------------------------------- |
| `422` | The issuer private key is not configured. |
| `400` | `credentialType` is missing or unsupported. |
| `400` | A requested `credentialValueSet` could not be resolved. |
| `404` | No resources match the requested credential type or value sets. |
| `403` | An Access Policy denies reading the patient's data. |
Loading