LabClinicAPI-Dokumentation für PraxissystemeZum Portal

Überblick

Mit der API verbindet Ihre Praxissoftware sich direkt mit LabClinic: Aufträge anlegen, Status und Befunde abrufen und sofort erfahren, wenn sich etwas ändert — ohne dass jemand im Portal nachsehen muss.

  • Basis-URL: https://ldb-connector.roenfeldt.dev/v1
  • Maschinenlesbare Beschreibung (OpenAPI 3.1): https://ldb-connector.roenfeldt.dev/v1/openapi.json — daraus lassen sich Clients für die meisten Sprachen generieren.
  • Format: JSON (UTF-8), Zeitangaben nach ISO 8601, Befunde als PDF.
  • Fehler einheitlich als application/problem+json (RFC 9457), siehe Fehler.

Die API arbeitet immer mit den Daten Ihrer Praxis: Welche Praxis gemeint ist, ergibt sich allein aus dem API-Key. Aufträge anderer Einsender sind weder sichtbar noch erreichbar.

Zugang

API-Keys erzeugt der Administrator Ihrer Praxis im Portal unter API. Ein Key wird nur einmal angezeigt — speichern Sie ihn sofort an einem sicheren Ort (Passwort-Tresor, Secret-Store der Software). Für jedes System einen eigenen Key anlegen: dann lässt sich einer gezielt widerrufen.

Jede Anfrage trägt den Key im Header:

Authorization: Bearer ldbp_3f9a1c2b4d5e_…

Berechtigungen (beim Anlegen des Keys gewählt):

BerechtigungErlaubt
orders:readAufträge lesen
orders:writeAufträge anlegen und ändern
reports:readBefunde abrufen
events:readEreignisse abrufen
webhooks:manageWebhooks verwalten

Wird ein Key nicht mehr gebraucht oder ist er in falsche Hände geraten: im Portal widerrufen. Das wirkt ab der nächsten Anfrage; danach mit einem neuen Key weiterarbeiten.

Schnellstart

Die neuesten Aufträge Ihrer Praxis:

curl "https://ldb-connector.roenfeldt.dev/v1/orders?limit=5" \
  -H "Authorization: Bearer $LDBP_KEY"
{
  "data": [
    {
      "orderId": "4711",
      "orderNumber": "A-2026-0815",
      "barcode": "43048265",
      "status": "done",
      "receivedAt": "2026-10-23T09:14:00.000Z",
      "sampleTaken": "2026-10-23T06:30:00.000Z",
      "patient": { "firstname": "Max", "lastname": "Mustermann", "birthdate": "1980-04-12" },
      "reportsAvailable": true
    }
  ],
  "nextCursor": "eyJ2IjoyLCJkIjoxNzkyNzQ…"
}

Ab jetzt über Änderungen informiert werden (Position merken, siehe Ereignisstrom):

curl "https://ldb-connector.roenfeldt.dev/v1/events?after=latest" -H "Authorization: Bearer $LDBP_KEY"
# → { "data": [], "next": "18342", "hasMore": false }

Aufträge anlegen

POST /v1/orders mit Berechtigung orders:write. Der Header Idempotency-Key ist Pflicht (z. B. eine UUID je Auftrag, siehe unten).

curl -X POST https://ldb-connector.roenfeldt.dev/v1/orders \
  -H "Authorization: Bearer $LDBP_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a9e-3b0d-4c51-9f0e-2d7b8a1c4e10" \
  -d '{
    "barcode": "43048265",
    "patient": {
      "firstname": "Max",
      "lastname": "Mustermann",
      "birthdate": "1980-04-12",
      "gender": "m",
      "email": "max@example.com"
    },
    "sample": { "takenAt": "2026-10-23T08:30:00+02:00", "type": "serum" },
    "analyses": { "parameters": ["p1"], "templates": ["t1"] }
  }'

# 201 Created
{ "orderId": "4711", "barcode": "43048265" }

Felder

  • barcode — Pflicht, vom Probenröhrchen; sichtbare ASCII-Zeichen ohne Leerzeichen, max. 64.
  • patient.gender — f, m oder d; email optional.
  • sample.takenAt — Entnahmezeitpunkt. Mit Zeitzone (…+02:00 oder …Z) wird umgerechnet, ohne Zeitzone (2026-10-23T08:30) gilt die Ortszeit des Labors (Europe/Vienna).
  • sample.type — Probenart aus dem Katalog (sample_types).
  • analyses — mindestens ein Parameter oder Profil; IDs aus dem Katalog.

Barcodes

SituationErgebnis
Barcode ist neuAuftrag wird angelegt (201).
Barcode wurde vom Labor für Ihre Praxis bereitgestellt (Status unregistered)Auftrag wird registriert: Patient und Analysen werden ergänzt (201).
Barcode existiert bereits in jedem anderen Fall409 barcode-taken — nichts wird verändert.

Ein bestehender Auftrag wird beim Anlegen nie überschrieben. Änderungen laufen ausdrücklich über PATCH.

Idempotency-Key: sicher wiederholen

Erzeugen Sie für jeden Auftrag einmalig einen Key und speichern Sie ihn zusammen mit dem Auftrag, bevor Sie senden. Geht die Antwort verloren (Timeout, Verbindungsabbruch), senden Sie denselben Auftrag mit demselben Key erneut:

  • Wurde der Auftrag schon angelegt, kommt dasselbe Ergebnis zurück (200, Header Idempotent-Replayed: true).
  • Nach 504 lab-timeout wird ein dabei schon entstandener Auftrag vervollständigt, nicht verdoppelt.
  • Läuft die erste Anfrage noch: 409 request-in-progress, kurz warten und wiederholen.
  • Anderer Inhalt mit gleichem Key: 422 idempotency-key-reused. Nach einer Ablehnung (z. B. barcode-taken) den korrigierten Auftrag mit einem neuen Key senden.

Aufträge und Befunde lesen

Liste

GET /v1/orders?status=&cursor=&limit= (orders:read) — neueste zuerst, 1–50 je Seite (Standard 50). Für die nächste Seite cursor=<nextCursor> anhängen; nextCursor: null heißt: keine weiteren Aufträge. status filtert, mehrere kommagetrennt (status=received,in_progress).

Die Liste ist zum Anzeigen und für den ersten Abgleich gedacht. Um Änderungen zu bemerken, nicht die Liste wiederholt abrufen, sondern den Ereignisstrom oder Webhooks nutzen.

Status

StatusIm PortalBedeutung
unregisteredNicht registriertVom Labor bereitgestellter Barcode, Ihrer Praxis zugeordnet, noch ohne Patientendaten und Analysen.
registeredRegistriertAuftrag angelegt, Probe noch nicht im Labor.
receivedEingegangenProbe im Labor eingegangen.
on_holdZurückgestelltZurückgestellt, z. B. wegen einer Rückfrage.
in_progressIn BearbeitungIn Bearbeitung — auch wenn ein Auftrag nach einem Befund wieder geöffnet wird.
partially_reportedTeilbefundTeilbefund freigegeben, weitere Ergebnisse folgen.
doneFertigBefund freigegeben.
cancelledStorniertAuftrag storniert bzw. im Labor gelöscht (nur in Ereignissen).

Ob Ihre Praxis Teilbefunde erhält, stellt Ihr Praxis-Administrator im Portal ein. Ohne Teilbefunde erscheinen teilbefundete Aufträge als in_progress, und Teilbefunde sind über die API nicht abrufbar.

Einzelner Auftrag

curl https://ldb-connector.roenfeldt.dev/v1/orders/4711 -H "Authorization: Bearer $LDBP_KEY"

{
  "orderId": "4711",
  "barcode": "43048265",
  "status": "done",
  "receivedAt": "2026-10-23T09:14:00.000Z",
  "sampleTaken": "2026-10-23T06:30:00.000Z",
  "patient": { "firstname": "Max", "lastname": "Mustermann", "birthdate": "1980-04-12", "gender": "m" },
  "analyses": [{ "id": "p1", "name": "Analyse A" }],
  "reports": [
    { "reportId": "9871", "issuedAt": "2026-10-23T11:02:00.000Z", "kind": "partial" },
    { "reportId": "9902", "issuedAt": "2026-10-23T15:40:00.000Z", "kind": "final" }
  ]
}

kind ist partial (Teilbefund) oder final (Befund); bei sehr alten Befunden kann es null sein. issuedAt ist der Zeitpunkt der Freigabe; wo das Laborsystem ihn nicht verzeichnet, steht dort der Tag der Erstellung (0:00 Uhr). Alle Daten kommen bei jedem Aufruf frisch aus dem Laborsystem.

Befund-PDF

curl https://ldb-connector.roenfeldt.dev/v1/orders/4711/reports/9902 \
  -H "Authorization: Bearer $LDBP_KEY" -o befund-9902.pdf

Berechtigung reports:read. Antwort application/pdf.

Aufträge ändern

PATCH /v1/orders/{orderId} (orders:write) ersetzt Patient, Probe und Analysen vollständig — alle drei Teile sind Pflicht, gesendete Analysen ersetzen die bisherigen. Der Barcode bleibt.

curl -X PATCH https://ldb-connector.roenfeldt.dev/v1/orders/4711 \
  -H "Authorization: Bearer $LDBP_KEY" -H "Content-Type: application/json" \
  -d '{
    "patient": { "firstname": "Max", "lastname": "Mustermann", "birthdate": "1980-04-12", "gender": "m" },
    "sample": { "takenAt": "2026-10-23T08:30:00+02:00", "type": "serum" },
    "analyses": { "parameters": ["p1", "p2"] }
  }'

Möglich, solange die Probe noch nicht im Labor ist (unregistered, registered). Danach antwortet die API mit 409 order-locked — Änderungen dann bitte direkt mit dem Labor klären.

Änderungen erfahren

Zwei Wege mit denselben Ereignissen — wählen Sie einen oder kombinieren Sie beide:

Ereignisstrom (Pull)Webhooks (Push)
Funktioniert ohne öffentliche Adressejanein — Ihr Server muss per HTTPS erreichbar sein
VerzögerungIhr Abfrageintervall (z. B. 30–60 s)Sekunden
Nach Ausfall Ihres Systemsdort weiterlesen, wo Sie aufgehört haben (30 Tage)Wiederholung bis zu 3 Tage, danach Ereignisstrom

Empfehlung: Der Ereignisstrom ist der verlässliche Weg und reicht für die meisten Praxissysteme. Webhooks lohnen sich, wenn Sie Befunde ohne Verzögerung brauchen — auch dann den Ereignisstrom als Sicherheitsnetz nach Ausfällen nutzen.

Ereignistypen

TypBedeutungFelder in data
order.status_changedStatusänderungorderId, orderNumber, externalId, status, previousStatus, version
report.availableBefund verfügbarorderId, reportId, kind (partial | final), replacesReportId, version
{
  "id": "evt_5b0e…",
  "type": "report.available",
  "occurredAt": "2026-10-23T15:40:12.000Z",
  "data": {
    "orderId": "4711",
    "reportId": "9902",
    "kind": "final",
    "replacesReportId": null,
    "version": 6
  }
}
  • Ereignisse enthalten keine Patientendaten. Details und PDF holen Sie bei Bedarf über die API.
  • version steigt je Auftrag streng an. Speichern Sie die zuletzt verarbeitete Version je Auftrag und verwerfen Sie Ereignisse mit kleinerer oder gleicher Version — so stimmt die Reihenfolge immer.
  • id ist eindeutig; ein Ereignis kann (bei Webhooks) mehrfach ankommen — über die id entdoppeln.
  • Wird ein Befund korrigiert, folgt report.available mit replacesReportId; der alte Befund gilt dann als ersetzt.
  • Wird ein Auftrag erstmals sichtbar, ist previousStatus null. Vom Labor bereitgestellte Barcodes erscheinen so mit status: "unregistered".

Ereignisstrom

GET /v1/events?after=&limit= (events:read), limit 1–500 (Standard 100).

{ "data": [ …Ereignisse… ], "next": "18397", "hasMore": false }
  1. Einmalig: Aufträge über die Liste übernehmen und GET /v1/events?after=latest aufrufen; next speichern.
  2. Regelmäßig (z. B. alle 30–60 Sekunden): GET /v1/events?after=<next>, Ereignisse verarbeiten, danach das neue next speichern. Bei hasMore: true sofort weiterlesen.
  3. Antwortet die API mit 410 events-expired (länger als 30 Tage nicht abgefragt): Schritt 1 wiederholen.
// Beispiel (JavaScript, Node 20+)
let next = await store.get("ldbp_next") ?? (await api("/v1/events?after=latest")).next;
for (;;) {
  const page = await api(`/v1/events?after=${next}`);
  for (const event of page.data) await handle(event);   // idempotent, version prüfen
  next = page.next;
  await store.set("ldbp_next", next);
  if (!page.hasMore) await sleep(30_000);
}

next rückt auch vor, wenn es für Ihre Praxis nichts Neues gab — speichern Sie es trotzdem, dann wird Ihre Position nie zu alt.

Webhooks

Das Portal sendet jedes Ereignis per POST an Ihre HTTPS-Adresse. Einrichten im Portal unter API → Webhooks oder über die API (webhooks:manage):

curl -X POST https://ldb-connector.roenfeldt.dev/v1/webhooks \
  -H "Authorization: Bearer $LDBP_KEY" -H "Content-Type: application/json" \
  -d '{ "url": "https://pvs.praxis.example/webhooks/labor", "eventTypes": ["report.available"] }'

# 201 Created — das Secret wird nur jetzt angezeigt
{ "id": "whe_…", "url": "https://pvs.praxis.example/webhooks/labor", "eventTypes": ["report.available"],
  "enabled": true, "secret": "whsec_kZ3o…" }

eventTypes leer lassen für alle Ereignisse. Höchstens 10 Endpunkte je Praxis. Weitere Aufrufe: GET/PATCH/DELETE /v1/webhooks/{id}, POST …/test (sendet sofort eine signierte Testnachricht webhook.test und meldet die Antwort Ihres Servers), POST …/rotate-secret, GET …/deliveries (letzte 50 Zustellungen).

Bearer-Token (optional)

Verlangt Ihr Server oder ein vorgeschaltetes API-Gateway einen Token, geben Sie ihn als authToken an (im Portal: „Bearer-Token“). Jede Zustellung und jede Testnachricht trägt ihn dann als Authorization: Bearer …. Das Portal speichert ihn verschlüsselt und gibt ihn nie zurück; die Antworten zeigen nur hasAuthToken. Wechseln mit PATCH und neuem authToken, entfernen mit "authToken": null. Die Signatur gilt zusätzlich und bleibt der Nachweis, dass die Nachricht vom Portal kommt.

curl -X PATCH https://ldb-connector.roenfeldt.dev/v1/webhooks/whe_… \
  -H "Authorization: Bearer $LDBP_KEY" -H "Content-Type: application/json" \
  -d '{ "authToken": "eyJhbGciOi…" }'

Was ankommt

POST /webhooks/labor HTTP/1.1
Content-Type: application/json
webhook-id: evt_5b0e…
webhook-timestamp: 1792768812
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
Authorization: Bearer eyJhbGciOi…   (nur mit authToken)

{ "id": "evt_5b0e…", "type": "report.available", "occurredAt": "…", "data": { … } }

Signatur prüfen

Signiert nach Standard Webhooks. Am einfachsten mit einer der offiziellen Bibliotheken (z. B. standardwebhooks für JavaScript, Python, Go, Java, PHP, C#). Von Hand:

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: der unveränderte Request-Body als String (nicht neu serialisieren!)
function verify(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // max. 5 Minuten alt
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest("base64");
  return headers["webhook-signature"].split(" ").some((entry) => {
    const [version, signature] = entry.split(",");
    return version === "v1" && signature.length === expected.length &&
      timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  });
}

Nach einem Secret-Wechsel (rotate-secret) signiert das Portal 24 Stunden lang mit altem und neuem Secret; der Header enthält dann zwei Signaturen, eine gültige reicht.

Antworten und Wiederholung

  • Antworten Sie innerhalb von 10 Sekunden mit einem 2xx-Status und verarbeiten Sie danach — lange Arbeit nicht im Request erledigen. Weiterleitungen werden nicht verfolgt.
  • Sonst wird wiederholt: nach 5 s, 1 min, 5 min, 30 min, 1 h, 3 h, 6 h, dann alle 12 h — insgesamt 13 Versuche über rund 3 Tage.
  • Antwortet Ihr Server mit 410 Gone, wird der Webhook sofort deaktiviert; nach 5 Tagen ohne erfolgreiche Zustellung ebenfalls. Ihr Praxis-Administrator sieht das im Portal und kann ihn wieder aktivieren — ausstehende Zustellungen werden dann nachgeholt.
  • Ereignisse können mehrfach ankommen und in anderer Reihenfolge: über webhook-id entdoppeln, über version ordnen.

Analysenkatalog

GET /v1/catalog (orders:read oder orders:write) liefert die für Ihre Praxis bestellbaren Parameter (parameters) und Profile (templates) mit ihren IDs und Probenarten.

Die Antwort trägt einen ETag. Senden Sie ihn als If-None-Match mit; ohne Änderung kommt 304 Not Modified ohne Inhalt. Einmal täglich abfragen genügt.

curl https://ldb-connector.roenfeldt.dev/v1/catalog -H "Authorization: Bearer $LDBP_KEY" -H 'If-None-Match: "XIoFLI4Otb…"'

Fehler

HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "urn:ldbp:problem:barcode-taken",
  "title": "Barcode bereits vergeben",
  "status": 409,
  "detail": "Barcode bereits vergeben.",
  "requestId": "3be449b2-bfca-48ed-8754-59770a3eb160"
}

Werten Sie type aus (stabil), nicht title oder detail (Texte können sich ändern). Bei Rückfragen an das Labor die requestId angeben (auch im Header X-Request-Id).

typeStatusBedeutungWas tun
invalid-parameter400Ein Parameter oder Header ist ungültig.Anfrage korrigieren; detail nennt den Grund.
invalid-body400Der Inhalt ist ungültig.Felder aus detail korrigieren.
unauthorized401API-Key fehlt, ist ungültig oder widerrufen.Key prüfen; nicht wiederholen.
forbidden403Dem Key fehlt die nötige Berechtigung.Berechtigung beim Org-Admin anfragen.
not-found404Unbekannt oder für diesen Einsender nicht sichtbar.ID prüfen.
limit-reached409Höchstzahl erreicht (z. B. Webhook-Endpunkte).Bestehende Einträge aufräumen.
barcode-taken409Der Barcode ist bereits vergeben.Anderen Barcode verwenden; einen bestehenden Auftrag mit PATCH ändern.
request-in-progress409Eine Anfrage mit diesem Idempotency-Key läuft noch.Nach Retry-After mit demselben Key wiederholen.
order-locked409Die Probe ist im Labor; der Auftrag ist nicht mehr änderbar.Änderung mit dem Labor klären.
events-expired410Die Position im Ereignisstrom ist älter als 30 Tage.Aufträge über die Liste abgleichen, dann mit after=latest neu beginnen.
idempotency-key-reused422Gleicher Idempotency-Key, anderer Inhalt.Für neuen Inhalt einen neuen Key erzeugen.
analysis-not-orderable422Ein Profil oder eine Analyse ist für Ihre Praxis nicht freigegeben.Nur Analysen aus GET /v1/catalog bestellen; detail nennt die IDs. Freigaben vergibt das Labor.
lab-rejected422Das Laborsystem hat die Anfrage abgelehnt.Grund aus detail beheben (z. B. unbekannte Analyse).
rate-limited429Zu viele Anfragen.Nach Retry-After Sekunden fortfahren.
internal500Interner Fehler im Portal.Später wiederholen; bei Dauer das Labor informieren (mit requestId).
lab-error502Fehler im Laborsystem.Später wiederholen; bei Dauer das Labor informieren (mit requestId).
lab-unavailable503Das Laborsystem ist nicht erreichbar.Nach Retry-After wiederholen.
lab-timeout504Das Laborsystem hat nicht rechtzeitig geantwortet.Mit demselben Idempotency-Key wiederholen — es entsteht kein Doppelauftrag.

Grenzen

Grenze
Anfragen120 pro Minute und Key. Header RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset; bei Überschreitung 429 mit Retry-After.
Auftragsliste1–50 Aufträge je Seite
Ereignisstrom1–500 Ereignisse je Abruf; Ereignisse bleiben 30 Tage abrufbar
Webhooks10 Endpunkte je Praxis, Antwort innerhalb von 10 s, Wiederholung über rund 3 Tage
API-Keys20 aktive Keys je Praxis

Datenschutz und Sicherheit

  • Das Portal speichert keine Patientendaten, Ergebnisse oder Befunde. Aufträge und PDFs kommen bei jedem Abruf direkt aus dem Laborsystem.
  • Ereignisse und Webhooks enthalten nur IDs und Status, keine Namen oder Ergebnisse.
  • API-Keys und Webhook-Secrets sind Zugangsdaten: nie in Quellcode, Logs, URLs oder E-Mails. Ein Key gewährt Zugriff auf alle Aufträge Ihrer Praxis.
  • Nur HTTPS. Webhook-Ziele müssen öffentlich per HTTPS erreichbar sein; interne Adressen sind gesperrt.
  • Befund-PDFs enthalten Gesundheitsdaten: in Ihrem System nur so lange und so geschützt speichern, wie Sie es auch sonst tun müssen.

Checkliste für die Integration

  1. Key mit den nötigen Berechtigungen beim Praxis-Administrator anfordern; sicher speichern.
  2. Katalog laden, Parameter- und Profil-IDs zuordnen.
  3. Aufträge mit Barcode und je Auftrag gespeichertem Idempotency-Key anlegen; bei Timeouts mit demselben Key wiederholen.
  4. Bestehende Aufträge einmal über die Liste übernehmen, dann Ereignisstrom ab after=latest lesen.
  5. Ereignisse entdoppeln (id) und je Auftrag nach version ordnen.
  6. Bei report.available den Befund als PDF abrufen; replacesReportId beachten.
  7. Optional Webhooks einrichten, Signatur prüfen, mit …/test ausprobieren.
  8. Fehler nach type behandeln, Retry-After respektieren, requestId loggen.

Fragen zur Anbindung beantwortet LabClinic. Technische Details: OpenAPI-Beschreibung.