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.
Überblick
Abschnitt betitelt „Überblick“- 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.
Authentifizierung
Abschnitt betitelt „Authentifizierung“Jede Anfrage trägt den Token im Header:
Authorization: Bearer pob_live_xxxxxxxxxxxxxxxxxxxxTokens 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.
Rate-Limits & Fehler
Abschnitt betitelt „Rate-Limits & Fehler“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. |
Endpunkte
Abschnitt betitelt „Endpunkte“GET /fortbildungen
Abschnitt betitelt „GET /fortbildungen“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}GET /fortbildungen/{publicId}
Abschnitt betitelt „GET /fortbildungen/{publicId}“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" }}POST /fortbildungen/{publicId}/buchungen
Abschnitt betitelt „POST /fortbildungen/{publicId}/buchungen“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/buchungenAuthorization: Bearer pob_live_xxxxxxxxxxxxxxxxxxxxContent-Type: application/jsonIdempotency-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).
POST /buchungen/{buchungId}/dokumente/upload-url
Abschnitt betitelt „POST /buchungen/{buchungId}/dokumente/upload-url“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-urlAuthorization: Bearer pob_live_xxxxxxxxxxxxxxxxxxxxContent-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-urlAuthorization: Bearer pob_live_xxxxxxxxxxxxxxxxxxxxContent-Type: application/json
{ "filename": "zeugnis.pdf", "mimeType": "application/pdf", "sizeBytes": 348122, "storageKey": "schule/…/fortbildungen/buchung/clx0a1b2c3d4/1a2b…"}// → 201 { "id": "doc_…" }POST /buchungen/{buchungId}/storno
Abschnitt betitelt „POST /buchungen/{buchungId}/storno“Scope: bookings:write
Storniert eine Buchung. Ein belegter Platz wird frei und die Warteliste rückt nach.
{ "status": "storniert"}GET /buchungen/{buchungId}
Abschnitt betitelt „GET /buchungen/{buchungId}“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}Datenschema
Abschnitt betitelt „Datenschema“Was wir senden (Antworten) und was wir von Ihnen empfangen wollen (Anfragen), Feld für Feld.
Fortbildung (Antwort, wir senden)
Abschnitt betitelt „Fortbildung (Antwort, wir senden)“| 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. |
Buchung anlegen, Anfrage-Body (wir empfangen)
Abschnitt betitelt „Buchung anlegen, Anfrage-Body (wir empfangen)“| 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.
Buchung anlegen, Antwort (wir senden)
Abschnitt betitelt „Buchung anlegen, Antwort (wir senden)“| 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. |
Status (Antwort, wir senden)
Abschnitt betitelt „Status (Antwort, wir senden)“| Feld | Typ | Beschreibung |
|---|---|---|
status |
string | pending | bestaetigt | warteliste | storniert | abgelaufen. |
testmode |
boolean | Ob es eine Test-Buchung ist. |
Buchungsablauf (Double-Opt-in)
Abschnitt betitelt „Buchungsablauf (Double-Opt-in)“- Ihr System sendet den Buchungs-
POST. Die API antwortet mitstatus: "pending". - PulsOra schickt dem Bewerber automatisch eine Bestätigungs-E-Mail.
- Klickt der Bewerber den Link, wird der Platz belegt (oder die Warteliste greift, wenn voll).
- Klickt er nicht, läuft die Buchung nach 24 Stunden ab.
- Ihr System erfährt den Ausgang über
GET /buchungen/{buchungId}.
Beispiel (curl)
Abschnitt betitelt „Beispiel (curl)“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 }'Datenschutz
Abschnitt betitelt „Datenschutz“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