Quick start

The Helios grouping API lets your own systems send patient contacts and receive the DRG grouping back as JSON — using exactly the same grouping engine as Grouper in the Helios portal.

  1. Ask Helios for an API key. You will receive it once, as a text starting with hgk_. Keep it secret, like a password.
  2. Send your contacts to https://heliosgroup.dk/api/v1/group with the key in an Authorization header.
  3. Read the DRG code, name, tariff, trim point and length of stay from each result.
curl -X POST https://heliosgroup.dk/api/v1/group \
  -H "Authorization: Bearer hgk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"primaryDiagnosis":"DJ189","ageYears":72,"sex":"F"}'

Base URL for everything below: https://heliosgroup.dk/api/v1

Authentication

Every endpoint except /health needs your API key. Send it in either header:

Authorization: Bearer hgk_YOUR_KEY
X-API-Key: hgk_YOUR_KEY
  • Always use HTTPS. Never put the key in a web page, a mobile app or a URL — call the API from your own server or batch job.
  • A key can have an expiry date and can be limited to certain logics. Helios can revoke it at any time; if you think it has leaked, tell us and we issue a new one.
  • Use one key per system, so a single key can be switched off without stopping the others.
  • On request Helios can limit a key to your servers' IP addresses. A request from any other address is refused with 403 ip_not_allowed.

Endpoints

Method and pathWhat it does
GET /healthChecks that the API is up. No key needed.
GET /logicsLists the logics your key may group against (id, name, year, whether it is the active one).
GET /logics/{id}One logic, including the coded values it accepts for admission priority, discharge disposition, contact reason and patient type.
POST /groupGroups one contact or a batch of up to 500.

POST /group

Send either a single contact as the body, or a batch:

{
  "logic": "active",
  "contacts": [
    { "id": "A-1001", "primaryDiagnosis": "DJ189", "ageYears": 72, "sex": "F" },
    { "id": "A-1002", "primaryDiagnosis": "DS720", "procedures": ["KNFB20"], "ageYears": 81, "sex": "M" }
  ]
}

logic is optional. Leave it out (or use "active") to use whichever logic is active at Helios right now. To group against a specific year, use the id from GET /logics. Only logics that have been fully approved by Helios are available.

Grouping against the default logic means your results change when Helios sets a new year's logic as the default. If you need results that never change, name a logic by id and store that id with your results.

The reply:

{
  "apiVersion": "1",
  "logic": { "id": "dk-drg-2026", "name": "DK-DRG 2026", "year": 2026, "isActive": true },
  "count": 2,
  "results": [
    { "index": 0, "id": "A-1001", "matched": true, "drgCode": "04MA13", "drgName": "…",
      "price": 31250, "trimPoint": 9, "los": 4.2, "regel": 1234, "rtc": 0, "rtcMeaning": null,
      "mdc": "04", "dgcat": "…", "icdValid": true, "effectivePrimaryDiagnosis": "DJ189" },
    { "index": 1, "id": "A-1002", "matched": true, "…": "…" }
  ]
}

The values above are illustrative. Results come back in the same order as the contacts you sent, and carry your own id back so you can match them without relying on position.

The contact object

All fields are optional in the sense that the API will always answer, but a contact with no primary diagnosis cannot be grouped to a meaningful DRG.

FieldTypeMeaning
idstring or numberYour own reference for the contact. Returned unchanged in the result. Not stored by Helios.
primaryDiagnosisstringAction diagnosis (aktionsdiagnose), SKS code with the leading letter, e.g. DJ189.
secondaryDiagnoseslist of stringsBi-diagnoses (bidiagnoser), SKS codes. Up to 100.
procedureslist of stringsProcedure codes, e.g. KNFB20. Up to 100.
supplementCodeslist of stringsSupplementary (tillægs-) codes belonging to the procedures. Up to 100.
ageYears / ageDaysnumberAge at admission. Send one of them; ageDays is more exact for newborns and wins if both are sent.
sexstring"M" or "F" (also accepted: "K", "male", "female", 1, 2). Empty if unknown.
admissionPrioritystringAdmission priority code (INDM).
dischargeDispositionstringDischarge disposition code (DISCDISP).
contactReasonstringContact reason code (KONTAARS).
contactDaysnumberLength of the contact in days.
pattypestringPatient type code (PATTYPE).

The accepted values for the four coded fields differ slightly between logic years. Look them up with GET /logics/{id} — the reply contains an allowedValues object with admissionPriority, dischargeDisposition, contactReason and pattype.

Fields the API does not know are ignored and reported in a warnings list on that contact's result.

The result object

FieldMeaning
indexPosition of the contact in your request (starting at 0).
idYour own id for the contact, or null if you sent none.
matchedtrue when a DRG rule matched. When false the other grouping fields are empty.
drgCode, drgNameThe DRG the contact was grouped to.
priceThe DRG tariff, as given in the logic's tariff table.
trimPoint, losTrim point and average length of stay for the DRG.
regelNumber of the rule that matched (the first matching rule wins) — useful when you want to look the decision up in Atlas.
rtc, rtcMeaningReturn-to-caller code from the engine, with a plain-language meaning when there is one.
mdc, dgcatMajor diagnostic category and diagnosis category derived from the primary diagnosis.
icdValidWhether the primary diagnosis exists in the logic's diagnosis table.
effectivePrimaryDiagnosisThe diagnosis the grouping actually used (it can differ from the one you sent when the logic replaces it).
warningsOnly present when something was ignored, e.g. an unknown field.
errorOnly present when this one contact was rejected (invalid_contact) — the rest of the batch is still grouped.

Converting your data

Most hospital extracts come as several flat files: one row per contact (the stam file), one row per diagnosis (diag) and one row per procedure (proc), linked by a contact number. The API wants one JSON object per contact with the diagnoses and procedures gathered into lists. The steps are always the same:

  1. Read the contact file. One JSON object per row. Map your contact number to id, the action diagnosis to primaryDiagnosis, age and sex to ageYears/ageDays and sex, and the admission, discharge and reason codes to admissionPriority, dischargeDisposition and contactReason.
  2. Read the diagnosis file. Group the rows by contact number. The row marked as the action diagnosis becomes primaryDiagnosis (if it is not already in the contact file); all others go into secondaryDiagnoses.
  3. Read the procedure file. Group by contact number; the procedure codes go into procedures, and any supplementary codes into supplementCodes.
  4. Send in batches of up to 500 contacts and match the results back by id.

Check three things in your data before you blame the grouping:

  • Codes must be the SKS codes including their leading letter (DJ189, not J189), with no dots, spaces or lower-case letters. Trim and upper-case them first.
  • Dates are not sent. Compute age and length of contact yourself (age at admission, in days or years).
  • Coded fields (admission, discharge, reason, patient type) must use the codes listed by GET /logics/{id}. An empty value is accepted and simply does not match rules that need it.

The Python example below does the whole conversion and sends the batches. Change the column names in the COLUMNS block to match your own files.

Code examples

curl — look up the logics your key can use

curl https://heliosgroup.dk/api/v1/logics \
  -H "Authorization: Bearer hgk_YOUR_KEY"

curl https://heliosgroup.dk/api/v1/logics/dk-drg-2026 \
  -H "Authorization: Bearer hgk_YOUR_KEY"

Python — group a stam / diag / proc extract

import csv, json, os, sys, urllib.request
from collections import defaultdict

API  = "https://heliosgroup.dk/api/v1"
KEY  = os.environ["HELIOS_API_KEY"]          # never hard-code the key
LOGIC = "active"                              # or an id from GET /logics

# ---- adjust to your own files --------------------------------------
COLUMNS = {
    "stam": {"id": "kontaktnr", "age": "alder_aar", "sex": "koen",
             "admission": "indm", "discharge": "discdisp", "reason": "kontaars",
             "primary": "aktionsdiagnose"},
    "diag": {"id": "kontaktnr", "code": "diagnose"},
    "proc": {"id": "kontaktnr", "code": "procedure"},
}
DELIM, ENC = ";", "utf-8"
# ---------------------------------------------------------------------

def rows(path):
    with open(path, newline="", encoding=ENC) as f:
        yield from csv.DictReader(f, delimiter=DELIM)

def clean(code):
    return (code or "").strip().upper().replace(".", "").replace(" ", "")

def build(stam, diag, proc):
    c = COLUMNS
    contacts = {}
    for r in rows(stam):
        cid = r[c["stam"]["id"]]
        contacts[cid] = {
            "id": cid,
            "primaryDiagnosis": clean(r[c["stam"]["primary"]]),
            "ageYears": float(r[c["stam"]["age"]] or 0) if r[c["stam"]["age"]] else None,
            "sex": r[c["stam"]["sex"]],
            "admissionPriority": r[c["stam"]["admission"]],
            "dischargeDisposition": r[c["stam"]["discharge"]],
            "contactReason": r[c["stam"]["reason"]],
            "secondaryDiagnoses": [], "procedures": [],
        }
    for r in rows(diag):
        k = contacts.get(r[c["diag"]["id"]])
        code = clean(r[c["diag"]["code"]])
        if k and code and code != k["primaryDiagnosis"]:
            k["secondaryDiagnoses"].append(code)
    for r in rows(proc):
        k = contacts.get(r[c["proc"]["id"]])
        code = clean(r[c["proc"]["code"]])
        if k and code:
            k["procedures"].append(code)
    return list(contacts.values())

def post(batch):
    req = urllib.request.Request(
        API + "/group",
        data=json.dumps({"logic": LOGIC, "contacts": batch}).encode(),
        headers={"Authorization": "Bearer " + KEY, "Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=120) as resp:
        return json.load(resp)

if __name__ == "__main__":
    stam, diag, proc, out = sys.argv[1:5]
    contacts = build(stam, diag, proc)
    fields = ["id", "matched", "drgCode", "drgName", "price", "trimPoint", "los", "regel"]
    with open(out, "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=fields, delimiter=";", extrasaction="ignore")
        w.writeheader()
        for i in range(0, len(contacts), 500):
            reply = post(contacts[i:i + 500])
            for res in reply["results"]:
                w.writerow(res)
    print("Grouped", len(contacts), "contacts with", reply["logic"]["name"])

Run it with HELIOS_API_KEY=hgk_… python group.py stam.csv diag.csv proc.csv result.csv.

JavaScript (Node 18+) — one batch

const res = await fetch("https://heliosgroup.dk/api/v1/group", {
  method: "POST",
  headers: {
    "Authorization": "Bearer " + process.env.HELIOS_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    contacts: [
      { id: "A-1001", primaryDiagnosis: "DJ189", ageYears: 72, sex: "F" },
    ],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.code + ": " + data.error.message);
for (const r of data.results) console.log(r.id, r.drgCode, r.price);

Errors

Errors always have the same shape and a matching HTTP status:

{ "error": { "code": "invalid_key", "message": "The API key is not valid." } }
StatusCodeMeaning and what to do
400bad_json, bad_requestThe body is not valid JSON, is not an object, or contacts is empty or not a list.
401missing_keyNo key was sent. Add the Authorization header.
401invalid_keyThe key is mistyped or unknown. Check for stray spaces or line breaks.
401revoked_keyHelios has switched this key off. Ask for a new one.
401expired_keyThe key has passed its expiry date. Ask for a new one.
403ip_not_allowedThe key is limited to certain IP addresses and this request came from another one. Ask Helios to add the address, or call from your registered server.
403logic_not_allowedYour key may not use the current default logic. Name an allowed logic in logic.
404logic_not_foundThat logic id does not exist, is not approved yet, or is not allowed for your key. List the available ones with GET /logics.
404not_foundUnknown endpoint.
405method_not_allowedWrong HTTP method (POST for /group, GET for the others).
409no_active_logicNo logic is active at the moment. Name one by id, or try later.
413too_many_contactsMore than 500 contacts in one request. Split the batch.
503logic_unavailableThe logic's data is temporarily incomplete. Retry in a minute; contact Helios if it persists.

A single bad contact does not fail the request: it comes back with matched: false and an error object (invalid_contact, with a message such as sex must be "M" or "F"), while the others are grouped normally. Always check each result.

Limits and good practice

  • Up to 500 contacts per request, and up to 100 codes in each code list.
  • Group large files in sequential batches of a few hundred. Parallel requests are fine in moderation; if you see HTTP 5xx or 429 responses, wait a few seconds and retry with a pause between attempts.
  • Each key may send up to 120 requests per minute. Above that you get HTTP 429 with a Retry-After header (seconds to wait). Send contacts in batches rather than one request per contact.
  • The first request after a quiet period can take a few seconds while the logic is loaded; later requests are fast.
  • Helios does not store the contacts you send. The API is meant for grouping, so send only what the fields above need — no names, no CPR numbers.
  • When Helios corrects a logic, the correction reaches the API within about a minute.
  • The response header X-Helios-Api-Version tells you the API version. Version 1 will not change in ways that break existing clients; new fields may be added, so ignore fields you do not know.

Status and alerts

  • The public status page shows whether the grouping API is answering and how often it has over the last 90 days. GET /api/v1/health (no key needed) is what it checks, and you can use it for your own monitoring.
  • If your errors for a day get unusually high, or a key is about to expire, your users get a notice in the bell of the client portal and, if email is set up, an email. Check API usage in the portal for details.
  • For files too big to send as requests (thousands of contacts), use Bulk grouping in the Casemix Engine instead: upload the file, and download the result when it is ready.

Support

Need a key, a new key, a limit changed, or help with a mapping? Contact your Helios consultant, or write via the contact page. Mention which key name it concerns — never send the key itself.