Zum Inhalt springen

Fortbildungen-API

Rufen Sie die buchbaren Fortbildungen einer Schule als JSON ab, stellen Sie sie im eigenen Design dar und legen Sie Buchungen direkt aus Ihrem System an. Die API ist REST/JSON und wird per Bearer-Token authentifiziert.

  • Basis-URL: https://app.puls-ora.de/api/fortbildungen/v1
  • Format: JSON, UTF-8. Alle Antworten und Fehler sind JSON.
  • Auth: Bearer-Token im Authorization-Header.
  • Server-zu-Server: Der Token gehört auf Ihren Server, nie in den Browser.

Jede Anfrage trägt den Token im Header:

Authorization: Bearer pob_live_xxxxxxxxxxxxxxxxxxxx

Tokens erstellt die Schul-Administration in PulsOra unter Fortbildungen, API-Tokens. Je Token werden Berechtigungen (Scopes) vergeben:

  • catalog:read: Fortbildungen lesen.
  • bookings:write: Buchungen anlegen und stornieren.

Es gibt Live- und Test-Tokens (Präfix pob_live_ bzw. pob_test_). Ein Test-Token sieht die echten Fortbildungen, seine Buchungen sind aber als Test markiert: es werden keine echten E-Mails verschickt und kein Platz belegt.

Pro Token gelten Ratenbegrenzungen (Richtwert: 120 Leseanfragen/Minute, 20 Buchungen/Minute). Bei Überschreitung antwortet die API mit 429 und einem Retry-After-Header. Fehler folgen immer diesem Schema:

{
"error": {
"code": "anmeldeschluss_vorbei",
"message": "Die Anmeldung ist geschlossen."
}
}

Mögliche Fehlercodes:

HTTP code Wann
400 idempotency_key_required Header Idempotency-Key fehlt oder ist kürzer als 8 Zeichen.
400 invalid_json Der Request-Body ist kein gültiges JSON.
401 unauthorized Bearer-Token fehlt oder ist ungültig bzw. widerrufen.
403 forbidden_scope Dem Token fehlt der nötige Scope (z. B. bookings:write).
403 feature_inactive Das Fortbildungen-Feature ist für die Schule nicht aktiv.
404 not_found Fortbildung (publicId) oder Buchung (buchungId) nicht gefunden.
409 conflict Für diese E-Mail liegt bereits eine Anmeldung vor.
422 validation Pflichtfelder fehlen oder sind ungültig (z. B. E-Mail).
422 consent_required datenschutzAkzeptiert ist nicht true.
422 forbidden Anmeldung geschlossen (Anmeldeschluss vorbei).
422 bad_request Ungültiger Upload (falscher Typ, zu groß, zu viele Dateien).
429 rate_limited Rate-Limit überschritten. Header Retry-After beachten.
500 internal Unerwarteter Serverfehler. Bitte später erneut versuchen.

Scope: catalog:read

Liste der öffentlich buchbaren Fortbildungen der Schule. Query-Parameter: limit, cursor (Paginierung), optional kategorie, von, bis.

Antwort

{
"data": [
{
"publicId": "ekg-auffrischung-2026",
"titel": "EKG-Auffrischung für Rettungssanitäter",
"beschreibung": "Kompakter Auffrischungstag zur EKG-Interpretation.",
"dauerText": "8 Unterrichtsstunden",
"preisCent": 14900,
"preisHinweis": "inkl. Verpflegung",
"voraussetzungen": "Abgeschlossene Ausbildung zum Rettungssanitäter.",
"weitereInfos": "Bitte eigene Verpflegung mitbringen.",
"anmeldeschluss": "2026-09-20",
"termine": [
{ "datum": "2026-09-24", "startTime": "09:00", "endTime": "16:00" }
],
"kapazitaet": { "max": 20, "belegt": 12, "freiePlaetze": 8, "status": "frei" },
"dokumente": { "anfordern": true, "pflicht": false, "hinweis": "RettSan-Zeugnis" }
}
],
"nextCursor": null
}

Scope: catalog:read

Detail einer einzelnen Fortbildung (gleiche Felder wie in der Liste).

{
"publicId": "ekg-auffrischung-2026",
"titel": "EKG-Auffrischung für Rettungssanitäter",
"beschreibung": "Kompakter Auffrischungstag zur EKG-Interpretation.",
"dauerText": "8 Unterrichtsstunden",
"preisCent": 14900,
"preisHinweis": "inkl. Verpflegung",
"voraussetzungen": "Abgeschlossene Ausbildung zum Rettungssanitäter.",
"weitereInfos": "Bitte eigene Verpflegung mitbringen.",
"anmeldeschluss": "2026-09-20",
"termine": [
{ "datum": "2026-09-24", "startTime": "09:00", "endTime": "16:00" }
],
"kapazitaet": { "max": 20, "belegt": 12, "freiePlaetze": 8, "status": "frei" },
"dokumente": { "anfordern": true, "pflicht": false, "hinweis": "RettSan-Zeugnis" }
}

Scope: bookings:write

Legt eine Buchung an. Der Header Idempotency-Key ist Pflicht: bei wiederholtem Senden desselben Keys entsteht keine Doppelbuchung. Es gilt Double-Opt-in, der Platz wird erst belegt, wenn der Bewerber die Bestätigungs-E-Mail anklickt.

Anfrage

POST https://app.puls-ora.de/api/fortbildungen/v1/fortbildungen/ekg-auffrischung-2026/buchungen
Authorization: Bearer pob_live_xxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: 9f1c2b7a-4e5d-4a10-9c33-2b1e6f0a7c8d
{
"vorname": "Max",
"nachname": "Muster",
"email": "max.muster@example.de",
"telefon": "+49 170 0000000",
"organisation": "Rettungswache Musterstadt",
"datenschutzAkzeptiert": true
}

Antwort

201 Created
{
"buchungId": "clx0a1b2c3d4",
"status": "pending",
"testmode": false,
"dokumente": {
"erforderlich": true,
"pflicht": false,
"uploadUrlEndpoint": "/api/fortbildungen/v1/buchungen/clx0a1b2c3d4/dokumente/upload-url"
}
}

datenschutzAkzeptiert: true ist Pflicht. Fehlercodes: 409 (E-Mail bereits gebucht oder Idempotency-Konflikt), 422 (Anmeldeschluss vorbei oder Einwilligung fehlt), 429 (Rate-Limit).

Scope: bookings:write

Zweistufig: zuerst ohne storageKey eine Upload-URL anfordern, die Datei per PUT dorthin laden, dann denselben Endpunkt mit storageKey aufrufen, um den Upload zu finalisieren. Erlaubt: PDF, JPG, PNG, max. 10 MB, bis 5 Dateien.

Anfrage (Schritt 1)

POST https://app.puls-ora.de/api/fortbildungen/v1/buchungen/clx0a1b2c3d4/dokumente/upload-url
Authorization: Bearer pob_live_xxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"filename": "zeugnis.pdf",
"mimeType": "application/pdf",
"sizeBytes": 348122
}

Antwort (Schritt 1)

{
"uploadUrl": "https://app.puls-ora.de/api/files/upload?token=…",
"storageKey": "schule/…/fortbildungen/buchung/clx0a1b2c3d4/1a2b…",
"expiresIn": 300
}

Finalisieren (Schritt 2)

// 1) Datei per PUT an uploadUrl laden, dann finalisieren:
POST https://app.puls-ora.de/api/fortbildungen/v1/buchungen/clx0a1b2c3d4/dokumente/upload-url
Authorization: Bearer pob_live_xxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"filename": "zeugnis.pdf",
"mimeType": "application/pdf",
"sizeBytes": 348122,
"storageKey": "schule/…/fortbildungen/buchung/clx0a1b2c3d4/1a2b…"
}
// → 201 { "id": "doc_…" }

Scope: bookings:write

Storniert eine Buchung. Ein belegter Platz wird frei und die Warteliste rückt nach.

{
"status": "storniert"
}

Scope: bookings:write

Fragt den Status einer Buchung ab (Polling), z. B. pending, bestaetigt, warteliste, storniert, abgelaufen. Es werden keine personenbezogenen Daten zurückgegeben.

{
"status": "bestaetigt",
"testmode": false
}

Was wir senden (Antworten) und was wir von Ihnen empfangen wollen (Anfragen), Feld für Feld.

Feld Typ Beschreibung
publicId string Stabile, lesbare ID der Fortbildung (für alle weiteren Aufrufe).
titel string Titel der Fortbildung.
beschreibung string | null Freitext-Beschreibung.
dauerText string | null Anzeige-Dauer, z. B. „8 Unterrichtsstunden“.
preisCent integer | null Preis in Cent. null = kostenlos bzw. auf Anfrage.
preisHinweis string | null Zusatz zum Preis, z. B. „inkl. Verpflegung“.
voraussetzungen string | null Voraussetzungen für die Teilnahme.
weitereInfos string | null Weitere Hinweise.
anmeldeschluss date | null Letzter Anmeldetag (YYYY-MM-DD).
termine[] array Termine mit datum (date), startTime, endTime („HH:MM“).
kapazitaet object max (int|null), belegt (int), freiePlaetze (int|null), status („frei“ | „voll“ | „geschlossen“).
dokumente object anfordern (bool), pflicht (bool), hinweis (string|null): ob und welche Nachweise der Bewerber hochladen soll.
Feld Typ Pflicht Beschreibung
vorname string ja Vorname des Bewerbers.
nachname string ja Nachname des Bewerbers.
email string ja E-Mail des Bewerbers (bekommt die Bestätigungs-Mail).
telefon string nein Telefonnummer.
organisation string nein Organisation/Rettungswache.
datenschutzAkzeptiert boolean ja Muss true sein. Die Einwilligung erheben Sie auf Ihrer Seite.

Zusätzlich Pflicht: der Header Idempotency-Key (frei wählbar, mind. 8 Zeichen, pro Buchung eindeutig). Derselbe Key liefert garantiert dieselbe Buchung zurück statt einer zweiten.

Feld Typ Beschreibung
buchungId string ID der Buchung, für Status-Abfrage und Storno.
status string Immer pending (Platz erst nach E-Mail-Bestätigung).
testmode boolean true bei einem Test-Token (keine echten Mails, kein Platzverbrauch).
dokumente object erforderlich/pflicht (bool) und uploadUrlEndpoint: wohin Sie Nachweise hochladen.
Feld Typ Beschreibung
status string pending | bestaetigt | warteliste | storniert | abgelaufen.
testmode boolean Ob es eine Test-Buchung ist.
  1. Ihr System sendet den Buchungs-POST. Die API antwortet mit status: "pending".
  2. PulsOra schickt dem Bewerber automatisch eine Bestätigungs-E-Mail.
  3. Klickt der Bewerber den Link, wird der Platz belegt (oder die Warteliste greift, wenn voll).
  4. Klickt er nicht, läuft die Buchung nach 24 Stunden ab.
  5. Ihr System erfährt den Ausgang über GET /buchungen/{buchungId}.
Terminal-Fenster
curl -X POST \
"https://app.puls-ora.de/api/fortbildungen/v1/fortbildungen/ekg-auffrischung-2026/buchungen" \
-H "Authorization: Bearer pob_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "vorname":"Max","nachname":"Muster","email":"max@example.de","datenschutzAkzeptiert":true }'

Erheben Sie die Bewerberdaten auf Ihrer Seite und leiten Sie sie an PulsOra weiter, sind Sie Mit-Verantwortlicher im Sinne der DSGVO. Dafür ist ein Auftragsverarbeitungsvertrag (AVV) nötig, und die Einwilligung des Bewerbers muss vorliegen (datenschutzAkzeptiert: true). Über die API verlassen keine personenbezogenen Daten das System außer denen, die Sie selbst erhoben und gesendet haben.

Fragen zur Integration? hallo@puls-ora.de