Hoppa till innehållet
Entwickler & API

Schnittstelle für Fremdsysteme

Kurse, Menschen, Kursbelegungen, Fortschritt und Nachweise dieser Einrichtung — lesbar und teilweise schreibbar über HTTP.

BUILT7 Endpunkte, Schlüssel je Einrichtung mit Takt je Minute, OpenAPI-Beschreibung, signierte Webhooks — gebaut und geprüft; erste externe Anbindung als Projektleistung.

SCORM 1.2 / 2004 — Import

LIVE

Fremde Kurspakete hochladen (Verwaltung → Kurse → SCORM), sie laufen im eigenen Player; Fortschritt und Abschluss werden übernommen.

LTI 1.3 — Tool-Rolle

BUILT

Login (OIDC), Deep Linking und Notenrückgabe (AGS) implementiert; die erste Anbindung an ein externes LMS erfolgt als Projektleistung.

SAML-SSO

BUILT

Anmeldung über den Identitätsanbieter Ihres Hauses (SAML 2.0); Einrichtung je Kunde als Projektleistung.

Datenausgang in offenen Formaten

LIVE

Personen als JSON (Art. 15/20 DSGVO), Kurse als PowerPoint und PDF, Rechnungen als CSV.

Schlüssel

Ein Schlüssel wird in der Verwaltung ausgestellt, unter /admin/api-schluessel. Er wird genau einmal angezeigt und lässt sich nicht wiederherstellen — auch nicht vom Betreiber. Geht er verloren, wird ein neuer ausgestellt und der alte gesperrt.

Jeder Schlüssel gehört genau einer Einrichtung und sieht ausschließlich deren Daten. Einen Schlüssel über mehrere Einrichtungen hinweg gibt es nicht.

curl -H "Authorization: Bearer uds_live_…" \
     https://uds-system.de/api/v1/courses

Nie in der Adresszeile (?api_key=…) — dort stünde er in jedem Zugriffsprotokoll, jedem Verlauf und jedem Verweis-Kopf. Diesen Weg gibt es deshalb nicht.

Rechte

Beim Ausstellen wird angekreuzt, was der Schlüssel darf. Ein Schlüssel ohne Kreuz darf nichts — die Liste ist eine weiße, keine schwarze.

  • courses:read
  • learners:read
  • learners:write
  • enrolments:read
  • enrolments:write
  • progress:read
  • credentials:read

Tempo

Jede Antwort trägt RateLimit-Limit, RateLimit-Remaining und RateLimit-Reset — auch die erfolgreiche. Damit lässt sich das eigene Tempo einstellen, bevor man gegen die Grenze läuft. Wird sie überschritten, kommt 429 mit Retry-After in Sekunden.

Seitenweise

Listen liefern limit (Vorgabe 50, höchstens 200) und offset. Die Antwort nennt total und has_more, damit man nicht bis zur ersten leeren Seite raten muss.

Endpunkte

GET/api/v1/coursescourses:read

List this organisation's courses

Returns the courses that belong to the organisation this key was issued for. `course_slug` is the value to use when enrolling someone — it is derived from the id and should be passed through rather than rebuilt.

Parameter

  • limit Page size, 1–200. Defaults to 50.
  • offset Rows to skip. Defaults to 0.
  • status Filter by course status, e.g. `published`.

Felder der Antwort

id · course_slug · title · description · status · program_key · semester · created_at · updated_at

GET/api/v1/learnerslearners:read

List this organisation's people

Deliberately a narrow selection. The underlying record carries 45 columns including a career profile, a phone number and marketing preferences; none of that is exposed here. `is_test_account` travels with it so an importing system can tell test records from real ones.

Parameter

  • limit Page size, 1–200. Defaults to 50.
  • offset Rows to skip. Defaults to 0.

Felder der Antwort

id · email · first_name · last_name · full_name · is_lecturer · is_test_account · created_at

PATCH/api/v1/learners/{id}learners:write

Correct a person's name

Only name fields can be changed. Role, organisation and lecturer status are permissions, not master data — a key that could set them would be a key that grants access. Creating people is not possible through this API; use the invitation flow. A person outside this organisation answers 404, the same as one that does not exist.

Parameter

  • id The person's id.

Rumpf

  • first_name string | null
  • last_name string | null
  • full_name string | null

Felder der Antwort

id · email · first_name · last_name · full_name · is_lecturer · is_test_account · created_at

GET/api/v1/enrolmentsenrolments:read

List who is taking which course

An enrolment is the record created when a person starts a course; it carries the coarse progress (`current_checkpoint`, `completed_modules`). Consumption and billing figures (avatar minutes, tokens, cost) are the operator's, not the learner's, and stay out.

Parameter

  • limit Page size, 1–200. Defaults to 50.
  • offset Rows to skip. Defaults to 0.
  • learner_id Only this person's enrolments.
  • course_slug Only this course's enrolments.

Felder der Antwort

id · user_id · course_slug · status · current_checkpoint · completed_modules · started_at · last_active_at · completed_at

POST/api/v1/enrolmentsenrolments:write

Enrol a person in a course

Both the person and the course must belong to this organisation; either one outside it answers 404. Only this organisation's own courses can be enrolled into — that is exactly the set `GET /api/v1/courses` returns. Enrolling somebody twice is not an error: the existing enrolment comes back with 200 and `created: false`, so a nightly sync does not fill its log with failures.

Rumpf

  • learner_id uuid, required
  • course_slug string, required — from GET /api/v1/courses

Felder der Antwort

id · user_id · course_slug · status · current_checkpoint · completed_modules · started_at · last_active_at · completed_at

GET/api/v1/progressprogress:read

Per-lesson progress

The FINE-grained progress, lesson by lesson. For how far somebody is in a course overall, read the enrolment instead — a system that confuses the two pulls a thousand rows for one number.

Parameter

  • limit Page size, 1–200. Defaults to 50.
  • offset Rows to skip. Defaults to 0.
  • learner_id Only this person's progress.

Felder der Antwort

id · user_id · lesson_id · steps_done · steps_total · correct_count · gradable_count · mastered · started_at · completed_at · updated_at

GET/api/v1/credentialscredentials:read

Issued certificates

The signed credential itself (`vc_jwt`) is NOT returned — it belongs to the holder, not to an integrating system. Proctoring flags are a behavioural observation and stay out for the same reason. `qr_verification_code` lets anyone confirm a certificate publicly without handling the credential.

Parameter

  • limit Page size, 1–200. Defaults to 50.
  • offset Rows to skip. Defaults to 0.
  • learner_id Only this person's certificates.

Felder der Antwort

id · user_id · certificate_number · holder_name · course_language · score_percent · issued_at · valid_until · qr_verification_code · issuing_institution

Fehler

StatusCodeWann
401NO_KEYNo `Authorization: Bearer uds_live_…` header.
401INVALID_KEYUnknown or revoked key. Deliberately indistinguishable — otherwise the error could be used to search for valid keys.
403MISSING_SCOPEThe key does not carry the scope this endpoint needs.
429RATE_LIMITEDMore than `RateLimit-Limit` calls in one minute. `Retry-After` says how long to wait.
503UPSTREAM_ERRORThe query did not run. Deliberately NOT an empty list — a nightly sync would otherwise delete your records.

Webhooks — die Gegenrichtung

Statt nachts zu fragen, ob jemand fertig ist, kann die Anlage Bescheid sagen. Ein Ziel wird in der Verwaltung angelegt (/admin/webhooks), mit einer https-Adresse und den Ereignissen, die es hören soll. Es bekommt ein Geheimnis, genau einmal angezeigt.

Ereignisse

  • enrolment.created A person was enrolled in a course — through the API or inside the platform.
  • enrolment.completed A person finished a course.
  • credential.issued A certificate was issued. The payload carries the public verification code, never the signed credential itself.
  • learner.updated A person's name was changed through the API.

Die Zustellung

POST <Ihre Adresse>
Content-Type: application/json
X-Universe-Event: enrolment.completed
X-Universe-Delivery: <Kennung, bei jeder Wiederholung dieselbe>
X-Universe-Signature: t=1725960000,v1=<hex>

{ "id": "…", "event": "enrolment.completed", "created_at": "…",
  "attempt": 1, "data": { "enrolment_id": "…", "learner_id": "…", … } }

Antworten Sie mit 2xx, sobald Sie das Ereignis abgelegt haben — nicht erst, wenn Sie es verarbeitet haben. Nach zehn Sekunden gilt der Aufruf als gescheitert. Dann wird mit wachsenden Abständen nachgeliefert, insgesamt 26 Stunden lang; die Kennung in X-Universe-Delivery bleibt dabei gleich, damit Sie Wiederholungen erkennen.

Die Unterschrift prüfen

Unterschrieben wird `${t}.${rumpf}` mit HMAC-SHA256 über Ihr Geheimnis; t ist der Zeitpunkt in Sekunden. Verwerfen Sie Zustellungen, deren t mehr als 300 Sekunden von Ihrer Uhr abweicht — sonst ließe sich eine mitgeschnittene Zustellung später noch einmal einspielen.

// Node.js
const [t, v1] = kopf.split(",").map((s) => s.split("=")[1]);
const erwartet = crypto.createHmac("sha256", GEHEIMNIS)
  .update(`${t}.${rohRumpf}`).digest("hex");
const gueltig =
  Math.abs(Date.now() / 1000 - Number(t)) <= 300 &&
  crypto.timingSafeEqual(Buffer.from(erwartet), Buffer.from(v1));

Wichtig: über den rohen Rumpf, nicht über das geparste und neu serialisierte JSON — jede Umformung ändert die Bytes.

Maschinenlesbar

Dieselbe Beschreibung als OpenAPI 3.1: /api/v1/openapi.json — ohne Schlüssel abrufbar, damit eine Anbindung gebaut werden kann, bevor einer ausgestellt ist.