API Reference
The API is served on two versions with different authentication:
/api/v1/*— Data endpoints authenticated with theX-API-KEYheader./api/v2/*— The same data endpoints authenticated with a HelseID access token (DPoP).
The health check (/api/internal/health) requires no authentication. See Authentication.
GET /api/v1/treatment-group
Returns treatment group data. Supports filtering and version-based diff queries.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
since-version |
long |
No | If provided, returns only items changed since this version. If omitted, returns all active items. |
name |
string |
No | Returns active treatment groups with matching name. |
disease-group |
string |
No | Returns active treatment groups that contains the disease group. |
jurisdiction |
string |
No | Returns active treatment groups that contains the jurisdiction code. |
indication |
string |
No | Returns active treatment groups that contains the indication code. |
vare-nr |
string |
No | Returns active treatment groups that contains the FEST/Varenummer. |
effective-date |
string YYYY-MM-DD |
No | Returns treatment groups that is active on the given date. |
ignore-fest-validation |
bool |
No | Default false. When false, products without a resolved FEST match are dropped from each treatmentAlternatives[].products array. A group that had at least one product before filtering but is left with zero across every alternative is omitted entirely; a group with no products to begin with is unaffected. When true, returns all products unfiltered and adds isInFest to each product. |
Response
{
"currentVersion": 5,
"entries": [
{
"data": { ... },
"version": 5,
"lastChanged": "2025-04-10T08:30:00Z",
"isDeleted": false
}
]
}
Without since-version: Returns all non-deleted items. isDeleted is always false.
With since-version: Returns items with version > N, including deleted items. isDeleted
may be true either because the item was removed from the source system or because all of its
products lost their FEST match and the item is no longer visible in a default full response. A
later FEST match can return the item again with isDeleted: false.
Example
# Get all
curl -H "X-API-KEY: key" \
https://api.example.com/api/v1/treatment-group
# Get changes since version 3
curl -H "X-API-KEY: key" \
https://api.example.com/api/v1/treatment-group?since-version=3
# Filter on indication code
curl -H "X-API-KEY: key" \
https://api.example.com/api/v1/treatment-group?indication=G35
GET /api/v1/treatment-group/
Returns a treatment group.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Returns an active treatment group with matching id. |
ignore-fest-validation |
bool |
No | Default false. When false, products without a resolved FEST match are dropped. 404 Not Found is returned only if the group had at least one product before filtering but is left with zero across every alternative afterward; a group with no products to begin with is returned unaffected. When true, returns all products unfiltered and adds isInFest to each product. |
Response
{
"data": {
"id": "...",
"name": "...",
"description": "...",
"diseaseGroup": "...",
"jurisdictions": [ { "system": "...", "code": "100022", "display": "Helse Nord RHF" } ],
"validityPeriod": { "start": "...", "end": "..." },
"reviewDate": "...",
"indications": [ { "coding": [ {"system": "...", "code": "C500","display": "..."}], "text": null } ],
"treatmentAlternatives": [
{
"rank": 1,
"treatmentId": "...",
"name": "Atezolizumab (Tecentriq)",
"description": "...",
"products": [
{
"identifiers": [
{ "system": "FEST/LegemiddelPakning/Varenummer", "value": "448992" },
{ "system": "FEST/LegemiddelPakning/Id", "value": "ID_3F2504E0-4F89-11D3-9A0C-0305E82C3301" },
{ "system": "FEST/LegemiddelMerkevare/Id", "value": "ID_7C9E6679-7425-40DE-944B-E07FC1F90AE7" },
{ "system": "FEST/LegemiddelVirkestoff/Id", "value": "ID_E4EAAAF2-D142-11E1-B3E4-080027620CDD" }
]
}
]
}
]
},
"version": 1,
"lastChanged": "2026-06-23T09:17:13.113Z",
"isDeleted": false
}
Explanation of content of indetifiers:
| System | Description |
|---|---|
FEST/LegemiddelPakning/Varenummer |
The Norwegian pharmacy article number (varenummer) for the drug package, as registered in FEST. |
FEST/LegemiddelPakning/Id |
The FEST internal identifier for the drug package (LegemiddelPakning). |
FEST/LegemiddelMerkevare/Id |
The FEST internal identifier for the drug's brand/proprietary product (Legemiddel Merkevare). |
FEST/LegemiddelVirkestoff/Id |
The FEST internal identifier for the drug's active substance (Legemiddel Virkestoff). There may be multiple FEST/LegemiddelVirkestoff/Id for each product. |
GET /api/v1/reimbursement-group
Returns reimbursement group data (legal basis, indications, covered products). Supports filtering and version-based diff queries.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
since-version |
long |
No | If provided, returns only items changed since this version. If omitted, returns all active items. |
indication |
string |
No | Returns active reimbursement groups that contains the indication code. |
ignore-fest-validation |
bool |
No | Default false. When false, products without a resolved FEST match are dropped from each group's products array. A group that had at least one product before filtering but is left with zero afterward is omitted entirely; a group with no products to begin with is unaffected. When true, returns all products unfiltered and adds isInFest to each product. |
Response
{
"currentVersion": 5,
"entries": [
{
"data": {
"id": "...",
"legalBasis": { "coding": [ { "system": "...", "code": "950", "display": "H-resept" } ] },
"indications": [ { "coding": [ { "system": "...", "code": "G35", "display": "Multiple sclerosis" } ] } ],
"products": [
{
"identifiers": [
{ "system": "FEST/LegemiddelPakning/Varenummer", "value": "166028" },
{ "system": "FEST/LegemiddelPakning/Id", "value": "ID_3F2504E0-4F89-11D3-9A0C-0305E82C3301" },
{ "system": "FEST/LegemiddelMerkevare/Id", "value": "ID_7C9E6679-7425-40DE-944B-E07FC1F90AE7" },
{ "system": "FEST/LegemiddelVirkestoff/Id", "value": "ID_E4EAAAF2-D142-11E1-B3E4-080027620CDD" }
]
}
]
},
"version": 5,
"lastChanged": "2025-04-10T08:30:00Z",
"isDeleted": false
}
]
}
Behavior is identical to the treatment-group endpoint. The products array is empty if upstream provides no article number for the authorization.
Example
# Get all
curl -H "X-API-KEY: key" \
https://api.example.com/api/v1/reimbursement-group
# Get changes since version 3
curl -H "X-API-KEY: key" \
https://api.example.com/api/v1/reimbursement-group?since-version=3
# Filter on indication code
curl -H "X-API-KEY: key" \
https://api.example.com/api/v1/reimbursement-group?indication=G35
GET /api/v1/reimbursement-group/
Returns a reimbursement group (legal basis, indications, covered products).
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Returns an active reimbursement group with matching id. |
ignore-fest-validation |
bool |
No | Default false. When false, products without a resolved FEST match are dropped. 404 Not Found is returned only if the group had at least one product before filtering but is left with zero afterward; a group with no products to begin with is returned unaffected. When true, returns all products unfiltered and adds isInFest to each product. |
Response
{
"data": {
"id": "...",
"legalBasis": { "coding": [ { "system": "...", "code": "950", "display": "H-resept" } ] },
"indications": [ { "coding": [ { "system": "...", "code": "G35", "display": "Multiple sclerosis" } ] } ],
"products": [
{
"identifiers": [
{ "system": "FEST/LegemiddelPakning/Varenummer", "value": "166028" }
]
}
]
},
"version": 5,
"lastChanged": "2025-04-10T08:30:00Z",
"isDeleted": false
}
HelseID (v2) endpoints
The v2 endpoints expose the same data and behave identically to their v1 counterparts. The differences are:
- Authentication — a HelseID access token sent with a DPoP proof, instead of the
X-API-KEYheader. See HelseID (DPoP). - No rate limiting — v2 endpoints are not part of the per-API-key rate limiter described below.
Response shapes, query parameters, and versioned-diff semantics are identical to the v1 endpoints above.
For both API versions, isInFest is omitted from products by default. It is present as a boolean
only when the request includes ignore-fest-validation=true:
{
"identifiers": [
{ "system": "FEST/LegemiddelPakning/Varenummer", "value": "166028" }
],
"isInFest": false
}
GET /api/v2/treatment-group
Returns treatment group data. Supports the same filtering and version-based diff queries as GET /api/v1/treatment-group.
Parameters
Identical to GET /api/v1/treatment-group (since-version, name, disease-group, jurisdiction, indication, vare-nr, effective-date, ignore-fest-validation).
Response
Same envelope and entry shape as GET /api/v1/treatment-group.
Example
# Get all (authenticated with a HelseID DPoP access token + proof)
curl -H "Authorization: DPoP <access-token>" \
-H "DPoP: <proof-jwt>" \
https://api.example.com/api/v2/treatment-group
# Get changes since version 3
curl -H "Authorization: DPoP <access-token>" \
-H "DPoP: <proof-jwt>" \
https://api.example.com/api/v2/treatment-group?since-version=3
GET /api/v2/treatment-group/
Returns a single treatment group. Same id parameter and response shape as GET /api/v1/treatment-group/{id}.
GET /api/v2/reimbursement-group
Returns reimbursement group data. Supports the same filtering and version-based diff queries as GET /api/v1/reimbursement-group.
Parameters
Identical to GET /api/v1/reimbursement-group (since-version, indication, ignore-fest-validation).
Response
Same envelope and entry shape as GET /api/v1/reimbursement-group.
Example
curl -H "Authorization: DPoP <access-token>" \
-H "DPoP: <proof-jwt>" \
https://api.example.com/api/v2/reimbursement-group?indication=G35
GET /api/v2/reimbursement-group/
Returns a single reimbursement group. Same id parameter and response shape as GET /api/v1/reimbursement-group/{id}.
Response Envelope
All endpoints return data in the same envelope:
{
currentVersion: number; // The server's current data version
entries: Array<{
data: T; // The actual data object
version: number; // Version when this entry last changed
lastChanged: string; // ISO-8601 timestamp of last change
isDeleted: boolean; // Whether this entry has been removed
}>;
}
GET /api/internal/health
Returns the health status of the API and its dependencies. This endpoint does not require authentication and is intended for load balancer and Kubernetes liveness/readiness probes.
Response
{
"status": "healthy",
"timestamp": "2025-04-10T08:30:00Z",
"components": {
"postgres": "healthy"
}
}
When a dependency is unhealthy:
{
"status": "unhealthy",
"timestamp": "2025-04-10T08:30:00Z",
"components": {
"postgres": {
"status": "unhealthy",
"error": "Connection refused"
}
}
}
Status Codes
| Code | Meaning |
|---|---|
200 OK |
All dependencies are healthy |
503 ServiceUnavailable |
One or more dependencies are unhealthy |
Example
curl http://localhost:5069/api/internal/health
GET /api/internal/sync/logs
Returns a combined, normalized, paginated list of past sync runs from the FEST/M30 sync
(FestSyncLog) and both sync pipelines' entries in SyncHistory (distinguished by its Source
column — see below), sorted by completion/start time descending. Read-only — it does not trigger a
sync itself (see POST /api/internal/sync and POST /api/internal/fest/sync for that).
Authenticated with the X-API-KEY header, same as the other /api/internal/* endpoints, and rate
limited per API key (see Rate Limiting below). Hidden from Swagger.
SyncHistory rows can now originate from either sync pipeline: the full SHI/FHIR sync always
writes one row per run (Source: Shi), and a FEST/M30 sync additionally writes a row (Source: Fest) whenever it bumped the Version/ContentHash of at least one TreatmentGroup/
ReimbursementGroup as a result of a product identifier change (a FEST run that changed nothing
writes no row). source=fest returns both FestSyncLog entries and Source: Fest SyncHistory
entries; source=shi returns only Source: Shi SyncHistory entries.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
source |
string (fest | shi, case-insensitive) |
No | Filters to a single sync source. Omit to return both. An unrecognized value returns 400 Bad Request. |
take |
int |
No | Max number of entries to return. Clamped to 1-200. Defaults to 50. |
skip |
int |
No | Number of entries to skip, for paging. Defaults to 0. A negative value returns 400 Bad Request. |
Response
{
"total": 12,
"take": 50,
"skip": 0,
"items": [
{
"source": "Fest",
"startedAt": "2026-01-01T00:00:00Z",
"completedAt": "2026-01-01T00:05:00Z",
"success": true,
"durationMs": 300000,
"details": {
"mode": "Delta",
"force": false,
"incrementalDateUsed": "2025-12-31T23:00:00Z",
"refreshedProductCount": 3,
"errorMessage": null,
"categoryStats": { "...": "per-category added/updated/unchanged/deactivated counts" }
}
},
{
"source": "Shi",
"startedAt": "2026-01-01T00:00:00Z",
"completedAt": "2026-01-01T00:02:00Z",
"success": true,
"durationMs": 120000,
"details": {
"version": 42,
"planDefinitions": { "...": "added/updated/unchanged/deleted counts" },
"activityDefinitions": { "...": "..." },
"regulatedAuthorizations": { "...": "..." },
"treatmentGroups": { "...": "..." },
"reimbursementGroups": { "...": "..." }
}
},
{
"source": "Fest",
"startedAt": "2025-12-31T23:58:00Z",
"completedAt": "2025-12-31T23:59:00Z",
"success": true,
"durationMs": 60000,
"details": {
"version": 43,
"planDefinitions": { "addedCount": 0, "updatedCount": 0, "unchangedCount": 0, "deletedCount": 0, "addedKeys": [], "updatedKeys": [], "deletedKeys": [] },
"activityDefinitions": { "...": "empty stats — FEST never touches this category" },
"regulatedAuthorizations": { "...": "empty stats — FEST never touches this category" },
"treatmentGroups": { "updatedCount": 1, "updatedKeys": ["tg-123"] },
"reimbursementGroups": { "updatedCount": 0, "updatedKeys": [] }
}
}
]
}
total is the total number of matching entries (before take/skip are applied), for paging.
items is ordered by completion/start time descending. details is source-specific: FEST entries
(FestSyncLog) report the sync mode (Full/Delta), whether force was requested, the
incrementalDate used, the count of products whose identifiers were refreshed, an errorMessage
on failure, and per-category stats; SyncHistory entries — from either sync pipeline — report the
publication version and per-resource-type added/updated/unchanged/deleted counts. A FEST-sourced
SyncHistory entry (source: "Fest", same shape as SHI entries) only ever populates
treatmentGroups/reimbursementGroups; the FHIR-only categories are always empty stats.
Known limitation: SyncHistory has no persisted failure/error state today — a row is
only ever written after a successful run, so success is always true for every SyncHistory
entry (both Source: Shi and Source: Fest), and a run that fails (or, for FEST, changes nothing)
currently leaves no row at all (invisible to this endpoint). FestSyncLog tracks failures
explicitly, so success can be false with a populated errorMessage for entries sourced from
it. For SyncHistory entries, startedAt is derived (SyncedAt - DurationMs) since SyncHistory
only stores a single completion timestamp; for FestSyncLog entries, startedAt/completedAt
come directly from the log row.
Example
# Get the 50 most recent runs from both sources
curl -H "X-API-KEY: key" \
http://localhost:5069/api/internal/sync/logs
# Get only FEST runs, paging 20 at a time
curl -H "X-API-KEY: key" \
"http://localhost:5069/api/internal/sync/logs?source=fest&take=20&skip=20"
GET /api/internal/fest/missing-products
Lists every persisted ReimbursementGroupProduct/TreatmentAlternativeProduct (latest
non-deleted version of each group only) whose article number (varenummer) currently has no
resolved FEST match, i.e. it would be filtered out of the public v1/v2 endpoints by the default
ignore-fest-validation=false behavior. Intended for internal data-quality monitoring rather than
consumption by external clients. Authenticated with the X-API-KEY header, same as the other
/api/internal/* endpoints, and rate limited per API key. Hidden from Swagger.
Response
[
{
"articleNumber": "448992",
"groupType": "ReimbursementGroup",
"groupBusinessKey": "rg-123",
"treatmentAlternativeId": null
},
{
"articleNumber": "166028",
"groupType": "TreatmentGroup",
"groupBusinessKey": "tg-456",
"treatmentAlternativeId": "atezolizumab-tecentriq"
}
]
groupType is either "ReimbursementGroup" or "TreatmentGroup". treatmentAlternativeId is
only set for TreatmentGroup entries, identifying which alternative within the group the product
belongs to. articleNumber may be null if the product has no FEST/LegemiddelPakning/Varenummer
identifier at all.
Example
curl -H "X-API-KEY: key" \
http://localhost:5069/api/internal/fest/missing-products
HTTP Status Codes
| Code | Meaning |
|---|---|
200 OK |
Success |
400 Bad Request |
Missing or invalid parameter |
401 Unauthorized |
Missing or invalid API key, or invalid/expired/replayed HelseID token or DPoP proof |
403 Forbidden |
Authenticated via HelseID, but the access token is missing the required nhn:legemiddelgrunndata/api scope |
404 Not found |
Data not found in DB |
429 Too Many Requests |
Rate limit exceeded (v1 endpoints only, see Rate Limiting) |
500 Internal Server Error |
Server error |
503 ServiceUnavailable |
Health check detected unhealthy dependency |
Error Format
Unhandled server errors return 500 Internal Server Error with the following JSON body:
{
"correlationId": "3f2b1c9a-4d5e-6f70-8192-a3b4c5d6e7f8"
}
In the Development environment, the body also includes message and stackTrace.
Every response — successful or error — includes an X-Correlation-Id response header. On 500 responses the same value is included in the body as correlationId. Clients may pass their own correlation ID via the X-Correlation-Id request header; it will be reused in the response and in server logs.
If the client aborts the request, no error response is written.
Rate Limiting
The data endpoints (/api/v1/treatment-group, /api/v1/reimbursement-group, /api/internal/sync, /api/internal/sync/logs) are rate limited to 120 requests per minute per API key (sliding window). The limit is configurable via the RateLimiting__PermitLimit, RateLimiting__WindowSeconds, and RateLimiting__QueueLimit environment variables. Rate limiting can be disabled entirely with RateLimiting__Enabled=false (default true).
The v2 (HelseID) endpoints are not rate limited.
When the limit is exceeded, the API responds with 429 Too Many Requests:
{ "error": "Rate limit exceeded. Try again later." }
The response includes a Retry-After header with the length of the rate-limit window in seconds. Waiting that long guarantees the request budget has fully replenished.
Notes:
- Requests without a valid API key (missing or unknown) share a single
anonymousbucket, which also prevents API-key brute-forcing. - The limit is enforced per replica (in-memory). With 3 replicas in production, the effective fleet-wide limit is ~3x the configured value.
RateLimiting__Enabled=falsedisables rate limiting on the data endpoints (requests pass through unthrottled). The flag is read at startup.GET /api/internal/healthand the Swagger UI are not rate limited.