# BatteryPass Entwicklerdokumentation > Integrations-API von BatteryPass für digitale EU-Batteriepässe: mit einem API-Schlüssel der Organisation Modelle, Einheiten, Werte, Pässe und Exporte lesen, Änderungen verfolgen, Tabellenimporte bereitstellen, Zustandsdaten und Lebenszyklus-Ereignisse melden und öffentliche Pässe prüfen. # Einführung > Was die Integrations-API von BatteryPass leistet, was im Portal bleibt und wo Sie anfangen. BatteryPass ist eine Plattform für digitale Batteriepässe nach der EU-Batterieverordnung (Verordnung (EU) 2023/1542). Hersteller pflegen Batteriemodelle, Einheiten und deren Daten im BatteryPass-Portal, lassen die Daten prüfen und veröffentlichen signierte Pässe. Diese Dokumentation beschreibt die **Integrations-API**: den Teil von BatteryPass, den Ihre eigenen Systeme (ERP, MES, Batteriemanagementsystem, Data Warehouse) mit einem API-Schlüssel nutzen. ## Was die API abdeckt | Bereich | Mit Schlüssel | Scope | | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | Batteriemodelle, Modellversionen, Einheiten | Lesen, Einheiten auch als Tabellenexport | `read` | | Werte | Lesen mit Herkunft (Quelle, Prüfstatus, Revisionen, belegende Dokumente) und Vollständigkeit | `read` | | Nachweise und Standorte | Metadaten von Dokumenten lesen, Dateien herunterladen, Produktionsstandorte auflisten | `read` | | Pässe | Aktuellen Stand, veröffentlichte Versionen, Unterschiede zwischen Versionen, Exporte (BatteryPass-JSON, PDF, QR-Code) und Registrierungsstatus lesen | `read` | | Feldregister | Version, Schema und Status lesen | `read` | | Zustandsdaten | Lesen und Messwerte wie Ladezustand oder Kapazitätsverlust melden | `read`, `condition` | | Lebenszyklusereignisse | Lesen und Ereignisse wie eine Inspektion oder Reparatur melden | `read`, `condition` | | Änderungen | Verfolgen, was sich in der Organisation geändert hat (Änderungsfeed zum Abfragen) | `read` | | Importe | Tabellenimporte von Einheiten bereitstellen: hochladen, zuordnen, prüfen; Mitglieder übernehmen und geben sie frei | `import` | | Öffentliche Pässe | Öffentliche Stufe lesen, Signaturen prüfen, öffentliche Dokumente laden | ohne Schlüssel | Jede Operation mit Parametern, Antworten und benötigtem Scope finden Sie in der [API-Referenz](https://passport.kabin.sh/docs/de/api-reference). ## Was im Portal bleibt - **Deklarierte Daten** (Modelle, Einheiten, Werte, Dokumente) erfassen Mitglieder im Portal oder importieren sie dort aus einer Tabelle. Die API legt keine deklarierten Daten an und ändert sie nicht. - **Prüfung, Freigabe und Veröffentlichung** bleiben bei den Mitgliedern. Standardmäßig gilt das Vier-Augen-Prinzip: Wer einen Wert erfasst hat, kann ihn nicht selbst freigeben. - **Mitglieder, API-Schlüssel und Einstellungen der Organisation** verwalten Eigentümer und Administration im Portal. Eine Ausnahme vom Prüfschritt sind Zustandsdaten, die ein Schlüssel meldet: Sie werden als freigegebene Messung mit Quelle und Messzeitpunkt gespeichert, und ein veröffentlichter Pass wird so aktualisiert, wie es die Einstellungen Ihrer Organisation vorsehen. Mehr dazu unter [Zustandsdaten und Lebenszyklus](https://passport.kabin.sh/docs/de/concepts/condition-and-lifecycle). > [!NOTE] > **EU-Register** > > Das EU-Register für digitale Produktpässe bietet Wirtschaftsakteuren noch keine Schnittstelle. BatteryPass > übermittelt deshalb bisher nichts dorthin, und `getRegistrySubmissionStatus` meldet `not_available`. ## Wie die Teile zusammenspielen ```mermaid flowchart LR Portal["Portal: Mitglieder erfassen, prüfen, veröffentlichen"] --> BP[("BatteryPass")] Import["Tabellenimport im Portal"] --> BP BP -- "API-Schlüssel, Scope read" --> ERP["Ihre Systeme: ERP, Data Warehouse"] BMS["BMS oder System eines Servicepartners"] -- "API-Schlüssel, Scope condition" --> BP BP --> Public["Öffentlicher Pass: QR-Code, Seite, JSON, ohne Schlüssel"] ``` ## Hier anfangen [**Schnellstart**](https://passport.kabin.sh/docs/de/quickstart): Schlüssel erstellen, ersten Aufruf senden und einen Pass lesen. [**Authentifizierung**](https://passport.kabin.sh/docs/de/authentication): Organisationsschlüssel, Scopes und was ein Schlüssel darf. [**Anleitungen**](https://passport.kabin.sh/docs/de/guides): Schritt für Schritt durch typische Integrationsaufgaben. [**Rezepte**](https://passport.kabin.sh/docs/de/recipes): Kurze, vollständige Programme zum Kopieren und Anpassen. [**API-Referenz**](https://passport.kabin.sh/docs/de/api-reference): Jede Operation des veröffentlichten Vertrags, mit Playground. --- # Schnellstart > In wenigen Minuten einen API-Schlüssel erstellen, den ersten Aufruf senden und einen Pass lesen. Sie brauchen: - die BatteryPass-Adresse Ihrer Organisation, zum Beispiel `https://batterypass.example.com`; - die Rolle Eigentümer in dieser Organisation oder einen Eigentümer, der den Schlüssel für Sie erstellt; - `curl` und `jq`, oder Node.js 20+ (die TypeScript-Beispiele sind ES-Module, z. B. `npx tsx quickstart.mts`), Python 3 mit `requests` oder Go. 1. **API-Schlüssel erstellen** Ein Eigentümer meldet sich im BatteryPass-Portal an, öffnet **Einstellungen → API-Schlüssel** und wählt **API-Schlüssel erstellen**. Geben Sie dem Schlüssel einen Namen, der das nutzende System nennt, und wählen Sie den Scope **Lesen** (`read`). Der Scope **Zustandsdaten melden** (`condition`) ist nur für Systeme gedacht, die Messwerte und Lebenszyklusereignisse melden. Das Geheimnis beginnt mit `bp_live_` und wird **nur ein einziges Mal** angezeigt. Legen Sie es sofort in Ihrem Secret-Speicher ab; später lässt es sich nicht mehr anzeigen. Schlüssel werden nur im Portal verwaltet, nicht über die API. 2. **Zwei Umgebungsvariablen setzen** ```bash title="Shell" export BATTERYPASS_URL="https://batterypass.example.com" export BATTERYPASS_API_KEY="bp_live_..." ``` `BATTERYPASS_URL` ist die BatteryPass-Adresse Ihrer Organisation ohne abschließenden Schrägstrich. Alle Beispiele dieser Dokumentation lesen diese beiden Variablen. Mehr dazu unter [Umgebungen und Basis-URL](https://passport.kabin.sh/docs/de/environments). 3. **Den ersten Aufruf senden** Listen Sie mit [`listBatteryModels`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-battery-models) die ersten fünf Batteriemodelle Ihrer Organisation auf: ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/battery-models?limit=5" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` ```json title="200" { "items": [ { "id": "0192f0a1-6c2e-7d4b-9a10-3e5f7b2c8d41", "name": "SolarVolt Home 10", "sku": "SV-H10", "gtin": "04012345000023", "category": "stationary", "archived_at": null, "row_version": 3, "created_at": "2026-01-08T09:12:44Z", "updated_at": "2026-03-02T15:40:10Z" } ], "next_cursor": null } ``` Ein `401` bedeutet, dass der Header fehlt oder der Schlüssel unbekannt oder widerrufen ist; ein `403`, dass dem Schlüssel der Scope `read` fehlt. Siehe [Authentifizierung](https://passport.kabin.sh/docs/de/authentication) und [Fehler](https://passport.kabin.sh/docs/de/errors). 4. **Batteriemodelle im Code auflisten** ```ts title="TypeScript" const res = await fetch(`${process.env.BATTERYPASS_URL}/api/v1/battery-models?limit=50`, { headers: { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }, }); if (!res.ok) throw new Error(`listBatteryModels: ${res.status} ${await res.text()}`); const page: { items: { id: string; name: string; gtin: string | null }[]; next_cursor: string | null } = await res.json(); for (const model of page.items) console.log(model.name, model.gtin ?? '(keine GTIN)', model.id); if (page.next_cursor) console.log('Weitere Modelle auf der nächsten Seite.'); ``` ```python title="Python" import os import requests res = requests.get( f"{os.environ['BATTERYPASS_URL']}/api/v1/battery-models", headers={"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"}, params={"limit": 50}, timeout=30, ) res.raise_for_status() page = res.json() for model in page["items"]: print(model["name"], model["gtin"] or "(keine GTIN)", model["id"]) if page["next_cursor"]: print("Weitere Modelle auf der nächsten Seite.") ``` ```go title="Go" package main import ( "encoding/json" "fmt" "log" "net/http" "os" "time" ) func main() { req, err := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1/battery-models?limit=50", nil) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY")) res, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { log.Fatalf("listBatteryModels: %s", res.Status) } var page struct { Items []struct { ID string `json:"id"` Name string `json:"name"` GTIN *string `json:"gtin"` } `json:"items"` NextCursor *string `json:"next_cursor"` } if err := json.NewDecoder(res.Body).Decode(&page); err != nil { log.Fatal(err) } for _, m := range page.Items { gtin := "(keine GTIN)" if m.GTIN != nil { gtin = *m.GTIN } fmt.Println(m.Name, gtin, m.ID) } if page.NextCursor != nil { fmt.Println("Weitere Modelle auf der nächsten Seite.") } } ``` Listen liefern höchstens `limit` Einträge und einen `next_cursor`; wie Sie alle Seiten lesen, zeigt [Paginierung](https://passport.kabin.sh/docs/de/pagination). 5. **Den Pass einer Einheit lesen** Suchen Sie mit [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries) eine Einheit mit veröffentlichtem Pass und lesen Sie dann ihren Pass mit [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport): ```bash title="cURL" BATTERY_ID=$(curl -sS "$BATTERYPASS_URL/api/v1/batteries?passport_status=published&limit=1" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" | jq -r '.items[0].id') curl -sS "$BATTERYPASS_URL/api/v1/batteries/$BATTERY_ID/passport" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` ```ts title="TypeScript" const base = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }; const units = await fetch(`${base}/batteries?passport_status=published&limit=1`, { headers }); if (!units.ok) throw new Error(`listBatteries: ${units.status}`); const { items }: { items: { id: string }[] } = await units.json(); if (items.length === 0) throw new Error('Noch keine Einheit mit veröffentlichtem Pass.'); const res = await fetch(`${base}/batteries/${items[0].id}/passport`, { headers }); if (!res.ok) throw new Error(`getPassport: ${res.status}`); const passport = await res.json(); console.log(passport.passport_id, passport.status, passport.public_url); ``` ```python title="Python" import os import requests base = f"{os.environ['BATTERYPASS_URL']}/api/v1" headers = {"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"} units = requests.get(f"{base}/batteries", headers=headers, params={"passport_status": "published", "limit": 1}, timeout=30) units.raise_for_status() items = units.json()["items"] if not items: raise SystemExit("Noch keine Einheit mit veröffentlichtem Pass.") res = requests.get(f"{base}/batteries/{items[0]['id']}/passport", headers=headers, timeout=30) res.raise_for_status() passport = res.json() print(passport["passport_id"], passport["status"], passport.get("public_url")) ``` ```go title="Go" package main import ( "encoding/json" "fmt" "log" "net/http" "os" "time" ) var client = &http.Client{Timeout: 30 * time.Second} // get sendet ein autorisiertes GET an die API und dekodiert die JSON-Antwort in out. func get(path string, out any) error { req, err := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1"+path, nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY")) res, err := client.Do(req) if err != nil { return err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return fmt.Errorf("GET %s: %s", path, res.Status) } return json.NewDecoder(res.Body).Decode(out) } func main() { var units struct { Items []struct { ID string `json:"id"` } `json:"items"` } if err := get("/batteries?passport_status=published&limit=1", &units); err != nil { log.Fatal(err) } if len(units.Items) == 0 { log.Fatal("Noch keine Einheit mit veröffentlichtem Pass.") } var passport struct { PassportID string `json:"passport_id"` Status string `json:"status"` PublicURL string `json:"public_url"` } if err := get("/batteries/"+units.Items[0].ID+"/passport", &passport); err != nil { log.Fatal(err) } fmt.Println(passport.PassportID, passport.Status, passport.PublicURL) } ``` ```json title="200" { "battery_id": "0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b12", "status": "published", "serial_number": "SV2027-000001", "gtin": "04012345000023", "passport_id": "urn:dpp:0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e", "gs1_digital_link": "https://batterypass.example.com/01/04012345000023/21/SV2027-000001", "public_url": "https://batterypass.example.com/pass/0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e", "first_published_at": "2026-03-10T08:30:00Z", "identifiers_locked_at": "2026-03-10T08:30:00Z", "row_version": 4 } ``` Die Antwort enthält außerdem `current_version` mit Versionsnummer, Hashes und Signaturschlüssel der aktuellen Version. Eine Einheit, die nie veröffentlicht wurde, hat einen Pass im Status `draft` ohne `passport_id`. 6. **Den öffentlichen Pass ohne Schlüssel lesen** Die öffentliche Stufe eines veröffentlichten Passes braucht keinen Schlüssel. [`getPublicPassport`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport) erwartet den UUID-Teil von `passport_id`, ohne das Präfix `urn:dpp:`: ```bash title="cURL" PASSPORT_UUID=$(curl -sS "$BATTERYPASS_URL/api/v1/batteries/$BATTERY_ID/passport" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" | jq -r '.passport_id | sub("^urn:dpp:"; "")') curl -sS "$BATTERYPASS_URL/api/v1/public/passports/$PASSPORT_UUID" | jq '{passport_id, status, links}' ``` ```ts title="TypeScript" // passport_id aus getPassport const passportId = 'urn:dpp:0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e'; const uuid = passportId.replace(/^urn:dpp:/, ''); const res = await fetch(`${process.env.BATTERYPASS_URL}/api/v1/public/passports/${uuid}`); if (!res.ok) throw new Error(`getPublicPassport: ${res.status}`); const pub = await res.json(); console.log(pub.status, pub.document?.model_name, pub.links.page); ``` ```python title="Python" import os import requests passport_id = "urn:dpp:0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e" # aus getPassport uuid = passport_id.removeprefix("urn:dpp:") res = requests.get(f"{os.environ['BATTERYPASS_URL']}/api/v1/public/passports/{uuid}", timeout=30) res.raise_for_status() pub = res.json() print(pub["status"], pub.get("document", {}).get("model_name"), pub["links"]["page"]) ``` ```go title="Go" package main import ( "encoding/json" "fmt" "log" "net/http" "os" "strings" "time" ) func main() { passportID := "urn:dpp:0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e" // aus getPassport uuid := strings.TrimPrefix(passportID, "urn:dpp:") client := &http.Client{Timeout: 30 * time.Second} res, err := client.Get(os.Getenv("BATTERYPASS_URL") + "/api/v1/public/passports/" + uuid) if err != nil { log.Fatal(err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { log.Fatalf("getPublicPassport: %s", res.Status) } var pub struct { Status string `json:"status"` Document *struct { ModelName string `json:"model_name"` } `json:"document"` Links struct { Page string `json:"page"` } `json:"links"` } if err := json.NewDecoder(res.Body).Decode(&pub); err != nil { log.Fatal(err) } model := "(kein Inhalt)" if pub.Document != nil { model = pub.Document.ModelName } fmt.Println(pub.Status, model, pub.Links.Page) } ``` `document` ist die signierte öffentliche Projektion der aktuellen Version; `verification` enthält ihre Ed25519-Signatur. Pässe, die nicht öffentlich sind (zum Beispiel `withdrawn`), antworten mit ihrem Status und ohne `document`. Öffentliche Abrufe werden fünf Minuten zwischengespeichert und sind auf 300 Anfragen pro Minute und Client-Adresse begrenzt. ## Nächste Schritte [**Authentifizierung**](https://passport.kabin.sh/docs/de/authentication): Scopes, Rechte eines Schlüssels, Rotation und 401 oder 403. [**Fehler**](https://passport.kabin.sh/docs/de/errors): Problem Details, Codes und die Request-ID. [**Zustandsdaten senden**](https://passport.kabin.sh/docs/de/guides/push-condition-data): Messwerte aus einem Batteriemanagementsystem melden. [**Signatur prüfen**](https://passport.kabin.sh/docs/de/guides/verify-signature): Einen Pass offline mit den öffentlichen Schlüsseln prüfen. --- # Authentifizierung > API-Schlüssel der Organisation, ihre Scopes, was ein Schlüssel darf und wie Sie ihn rotieren. Jede Operation der API braucht einen API-Schlüssel Ihrer Organisation. Ausgenommen sind nur die öffentlichen Abrufe von Pässen (Tag `public`), die ohne Schlüssel auskommen. ## Schlüssel senden Senden Sie den Schlüssel im Header `Authorization` mit dem Schema `Bearer`: ```http title="Header" Authorization: Bearer bp_live_... ``` ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/registry" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` ```ts title="TypeScript" const res = await fetch(`${process.env.BATTERYPASS_URL}/api/v1/registry`, { headers: { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }, }); console.log(res.status, await res.json()); ``` ```python title="Python" import os import requests res = requests.get( f"{os.environ['BATTERYPASS_URL']}/api/v1/registry", headers={"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"}, timeout=30, ) print(res.status_code, res.json()) ``` ```go title="Go" package main import ( "fmt" "io" "log" "net/http" "os" "time" ) func main() { req, err := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1/registry", nil) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY")) res, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(res.Status, string(body)) } ``` Die API liest Schlüssel nur aus diesem Header. Schreiben Sie einen Schlüssel nie in eine URL: Query-Strings und Pfade landen in Logs, Browserverläufen und Proxys. ## Schlüssel der Organisation - Ein Schlüssel gehört der **Organisation**, nicht einem Mitglied. Er funktioniert weiter, wenn das Mitglied, das ihn erstellt hat, die Organisation verlässt. - **Eigentümer** erstellen und widerrufen Schlüssel im Portal unter **Einstellungen → API-Schlüssel**. Die Verwaltung von Schlüsseln ist nicht Teil der API. - Das Geheimnis besteht aus `bp_live_` und 43 weiteren Zeichen. Es wird **einmal** angezeigt, beim Erstellen. BatteryPass speichert nur einen SHA-256-Hash und zeigt die ersten 12 Zeichen als Präfix, damit Sie Schlüssel unterscheiden können. - Eine Organisation hat höchstens **25 aktive Schlüssel**. Ein widerrufener Schlüssel bleibt mit dem Zeitpunkt des Widerrufs gelistet und funktioniert nie wieder. - Schlüssel haben **kein Ablaufdatum**. Sie beenden einen Schlüssel, indem Sie ihn widerrufen (siehe [Schlüssel rotieren](#schlüssel-rotieren)). ## Scopes Ein Schlüssel hat einen oder mehrere Scopes. Jede Operation nennt den benötigten Scope in `x-api-key-scope` im Vertrag und auf ihrer Seite in der [API-Referenz](https://passport.kabin.sh/docs/de/api-reference). | Scope | Erlaubt | Operationen | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `read` | Lesen, was ein Mitglied mit Lesezugriff in der Organisation sieht: Modelle, Versionen, Einheiten, Werte mit Herkunft und Vollständigkeit, Nachweise, Standorte, Pässe mit Versionen, Unterschieden und Exporten, Lebenszyklusereignisse, Zustandsdaten, das Register | Jede Operation mit `x-api-key-scope: read` | | `condition` | Zustandsdaten und Lebenszyklusereignisse melden | [`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition), [`createLifecycleEvent`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/create-lifecycle-event) | | `import` | Tabellenimporte von Einheiten bereitstellen: hochladen, zuordnen, prüfen und verfolgen; Mitglieder übernehmen und geben sie im Portal frei (ADR-026) | [`createImport`](https://passport.kabin.sh/docs/de/api-reference/imports/create-import), [`updateImportMapping`](https://passport.kabin.sh/docs/de/api-reference/imports/update-import-mapping), [`validateImport`](https://passport.kabin.sh/docs/de/api-reference/imports/validate-import), [`getImport`](https://passport.kabin.sh/docs/de/api-reference/imports/get-import), [`listImports`](https://passport.kabin.sh/docs/de/api-reference/imports/list-imports) | Die Scopes schließen einander nicht ein. Ein Batteriemanagementsystem, das Messwerte meldet und außerdem den aktuellen Zustand mit [`getCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/get-condition) liest, braucht beide. Alle Operationen mit ihrem Scope listet der Vertrag Ihrer Instanz: ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/openapi.json" \ | jq -r '.paths[][] | objects | select(has("operationId")) | "\(.operationId)\t\(.["x-api-key-scope"] // "public")"' ``` ## Was ein Schlüssel darf Ein Schlüssel handelt in seiner Organisation mit den Rechten eines **Mitglieds mit Lesezugriff** (Rolle `viewer`). Wo die Beschreibung einer Operation eine Rolle nennt („any member“, „editors and above“), zählt ein Schlüssel als `viewer`. Die Ausnahmen: Die beiden Operationen des Scopes `condition` akzeptieren einen Schlüssel mit diesem Scope, und die Importoperationen des Scopes `import` einen Schlüssel mit jenem. Ein Schlüssel kann nie: - deklarierte Daten anlegen oder ändern (Modelle, Einheiten, Werte, Dokumente); - etwas prüfen, freigeben, veröffentlichen oder zurückziehen, auch keinen Import übernehmen oder freigeben, selbst wenn er ihn bereitgestellt hat; - Mitglieder, API-Schlüssel oder Einstellungen der Organisation verwalten. Das bleibt bei den Mitgliedern im Portal, damit das Vier-Augen-Prinzip und die Prüfungen vor der Veröffentlichung ihre Bedeutung behalten. Was ein Schlüssel meldet, wird mit der Quelle `api` gespeichert, und das Lebenszyklusereignis der Meldung nennt den Schlüssel im Feld `api_key`. ## Sitzung und Schlüssel zusammen Eine Anfrage, die zugleich ein Sitzungs-Cookie des Portals und einen API-Schlüssel trägt, wird mit `401` abgelehnt. Das passiert etwa, wenn Sie Anfragen in einem Browser ausprobieren, in dem Sie im Portal angemeldet sind. Verwenden Sie einen Client ohne Cookies, zum Beispiel `curl` oder Ihren Servercode. ## 401 oder 403 | Status | `code` | Ursache | Was tun | | ------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `401` | `unauthenticated` | Der Header fehlt oder ist fehlerhaft, der Schlüssel ist unbekannt oder widerrufen, oder die Anfrage trägt zusätzlich ein Sitzungs-Cookie | Header und Umgebungsvariable prüfen; bei einem widerrufenen Schlüssel einen Eigentümer um einen neuen bitten | | `403` | `forbidden` | Dem Schlüssel fehlt der Scope der Operation, oder die Aktion braucht mehr als Lesezugriff | Einen Schlüssel mit passendem Scope verwenden; Aktionen über den Lesezugriff hinaus erledigen Mitglieder im Portal | | `429` | `rate_limited` | Zu viele Versuche mit unbekannten oder widerrufenen Schlüsseln von Ihrer Client-Adresse | Anhalten und den Schlüssel korrigieren; siehe [Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits) | > [!WARNING] > Wiederholen Sie einen `401` nicht in einer Schleife. Anfragen mit unbekannten oder widerrufenen Schlüsseln > sind auf 30 Versuche pro 15 Minuten und Client-Adresse begrenzt; danach erhalten weitere Versuche mit einem > solchen Schlüssel `429` statt `401`, bis das Zeitfenster endet. ## Schlüssel rotieren Schlüssel laufen nicht ab, also rotieren Sie sie selbst: in einem festen Rhythmus, sofort bei Verdacht auf ein Leck und wenn jemand ausscheidet, der den Schlüssel kannte. 1. **Neuen Schlüssel erstellen** Ein Eigentümer erstellt unter **Einstellungen → API-Schlüssel** einen neuen Schlüssel mit denselben Scopes. 2. **Neuen Schlüssel einsetzen** Legen Sie das neue Geheimnis in Ihrem Secret-Speicher ab und starten oder konfigurieren Sie das nutzende System neu. 3. **Prüfen, dass der alte Schlüssel ruht** Im Portal ändert sich die Angabe „Zuletzt genutzt“ des alten Schlüssels nicht mehr. Sie wird höchstens einmal pro Minute aktualisiert. 4. **Alten Schlüssel widerrufen** Der Widerruf wirkt sofort und lässt sich nicht rückgängig machen. ## Schlüssel geheim halten - Verwenden Sie Schlüssel nur in serverseitigen Systemen, nie in Browsercode, mobilen Apps oder öffentlichen Repositorys. - Erstellen Sie für jedes System einen eigenen Schlüssel, damit Sie ihn einzeln widerrufen können. - Geben Sie jedem Schlüssel nur die Scopes, die er braucht. - Das Präfix `bp_live_` hilft Secret-Scannern, geleakte Schlüssel zu finden. Mehr dazu unter [Sichere Schlüsselverwaltung](https://passport.kabin.sh/docs/de/guides/secure-key-handling) und [Sicherheit](https://passport.kabin.sh/docs/de/general/security). --- # Fehler > Wie die API Fehler als RFC 9457 Problem Details mit stabilen Codes und einer Request-ID meldet. Jede Fehlerantwort hat einen Status `4xx` oder `5xx` und einen Body vom Typ `application/problem+json` (RFC 9457 Problem Details). Ihr Code entscheidet anhand des HTTP-Status und des stabilen `code`; die Texte sind für Menschen gedacht. ## Felder eines Problems - **type** `string` (erforderlich): Problemtyp. `about:blank`, sofern kein eigener Typ dokumentiert ist. - **title** `string` (erforderlich): Kurze Zusammenfassung; bei `about:blank` die HTTP-Statusbezeichnung, z. B. `Bad Request`. - **status** `integer` (erforderlich): Der HTTP-Statuscode, im Body wiederholt. - **code** `string` (erforderlich): Stabiler, maschinenlesbarer Code in snake_case, z. B. `validation_error`. Siehe [Codes](#codes). - **detail** `string`: Erklärung dieses Einzelfalls, auf Englisch. Gut fürs Log, aber nicht zum Auswerten. - **instance** `string`: URI-Referenz der Anfrage, die das Problem ausgelöst hat, falls vorhanden. - **request_id** `string`: Die ID der Anfrage, gleich dem Response-Header `X-Request-Id`. Nennen Sie sie, wenn Sie den Support kontaktieren. - **errors** `object[]`: Angaben je Feld bei `validation_error` und bei `conflict`, wenn ein Wert eindeutig sein muss. **Feldfehler** - **field** `string` (erforderlich): Das Body-Feld (mit Punkten und Listenpositionen, z. B. `values[0].measured_at`), der Name des Query-Parameters oder Headers (z. B. `limit`) oder `body`, wenn der Body als Ganzes ungültig ist (fehlerhaftes JSON, eine unbekannte Eigenschaft, ein falscher Content-Type). - **code** `string` (erforderlich): Stabiler Grund, siehe [Codes für Feldfehler](#codes-für-feldfehler). - **message** `string` (erforderlich): Erklärung auf Englisch; übersetzen Sie anhand von `code`. ## Codes für Feldfehler | Code | Bedeutung | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `required` | Ein Pflichtfeld, ein Pflichtparameter oder ein Pflichtheader fehlt. | | `too_long` | Ein Text ist länger als erlaubt. | | `too_short` | Ein Text ist kürzer als erlaubt. | | `out_of_range` | Eine Zahl, ein Datum oder ein Zeitpunkt liegt außerhalb des erlaubten Bereichs, z. B. ein `measured_at` in der Zukunft. | | `invalid_format` | Der Wert hat das falsche Format (Datum, UUID, Muster), oder der Body ist kein gültiges JSON bzw. nicht `application/json`. | | `invalid_check_digit` | Die Prüfziffer einer GTIN oder GLN stimmt nicht. | | `invalid_value` | Der Wert ist hier nicht erlaubt: ein unbekannter Enum-Wert, ein ungültiger Parameter oder Cursor, eine fehlschlagende feldübergreifende Prüfung. | | `unknown_field` | Das Feld ist hier unbekannt, z. B. ein Feld, das keine Zustandsangabe der Einheit ist, oder eine unbekannte Eigenschaft im Body. | | `already_exists` | Ein Wert, der eindeutig sein muss, ist schon vergeben (zusammen mit `conflict`). | ## Beispiele Eine Zustandsmeldung mit einem Messzeitpunkt in der Zukunft ([`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition)) wird als Ganzes abgelehnt: ```json title="400" { "type": "about:blank", "title": "Bad Request", "status": 400, "code": "validation_error", "detail": "The request failed validation.", "request_id": "4f9c2a7d1e0b5c8a3f6e2d19", "errors": [ { "field": "values[0].measured_at", "code": "out_of_range", "message": "measured_at lies in the future" } ] } ``` Zu viele Anfragen: Der Header `Retry-After` nennt die Wartezeit in Sekunden. ```http title="429" HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 17 X-Request-Id: 8b1e6f03c2d94a7e5f0a1b2c { "type": "about:blank", "title": "Too Many Requests", "status": 429, "code": "rate_limited", "detail": "Too many attempts. Try again later.", "request_id": "8b1e6f03c2d94a7e5f0a1b2c" } ``` ## Codes Die Werte von `ProblemCode` im veröffentlichten Vertrag. Codes mit dem Hinweis „Nur im Portal“ kommen mit einem API-Schlüssel nicht vor; sie stehen hier, weil der Vertrag sie enthält. | Code | HTTP-Status | Bedeutung | | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `validation_error` | 400 | Die Anfrage ist ungültig. \`errors\[]\` nennt jedes betroffene Feld, jeden Query-Parameter oder Header mit einem stabilen Fehlercode. | | `invalid_token` | 400 | Nur im Portal: ein E-Mail- oder Einladungslink ist unbekannt, verbraucht oder abgelaufen. | | `unauthenticated` | 401 | Kein gültiger API-Schlüssel: der Header fehlt, der Schlüssel ist unbekannt oder widerrufen, oder die Anfrage trägt zugleich ein Sitzungs-Cookie. | | `invalid_credentials` | 401 | Nur im Portal: E-Mail-Adresse oder Passwort bei der Anmeldung sind falsch. | | `forbidden` | 403 | Der Schlüssel hat nicht den Scope, den die Operation verlangt, oder die Rechte eines Betrachters reichen nicht aus. | | `email_not_verified` | 403 | Nur im Portal: die E-Mail-Adresse ist noch nicht bestätigt. | | `not_found` | 404 | Die Ressource gibt es nicht, oder sie gehört zu einer anderen Organisation. | | `method_not_allowed` | 405 | Der Pfad existiert, aber nicht mit dieser HTTP-Methode. | | `conflict` | 409 | Die Anfrage widerspricht dem aktuellen Zustand, zum Beispiel ist das Batteriemodell der Einheit archiviert: ihre Daten lassen sich lesen, aber nicht mehr ändern. | | `identifiers_locked` | 409 | Nur im Portal: Kennungen einer veröffentlichten Einheit lassen sich nicht mehr ändern. | | `payload_too_large` | 413 | Der Request-Body ist größer als erlaubt (standardmäßig 1 MiB). | | `publish_blocked` | 422 | Nur im Portal: die Veröffentlichung scheitert an offenen Pflichtangaben; \`blockers\[]\` nennt sie. | | `rate_limited` | 429 | Zu viele Anfragen. Warten Sie die Sekunden aus dem Header \`Retry-After\` ab und versuchen Sie es dann erneut. | | `request_closed` | 410 | Nur für Lieferanten- und Servicepartner-Links: der Link ist abgelaufen, zurückgezogen oder verbraucht. | | `internal_error` | 500 | Ein Fehler auf unserer Seite. Wiederholen Sie die Anfrage später; nennen Sie dem Support die \`request_id\`. | | `address_lookup_unavailable` | 503 | Nur im Portal: die Adresssuche ist gerade nicht erreichbar; die Adresse lässt sich von Hand eingeben. | ## Request-ID Jede Antwort, ob erfolgreich oder nicht, trägt einen Header `X-Request-Id`. In einem Problem hat `request_id` denselben Wert. Der Server vergibt die ID selbst und ignoriert eine mitgesendete. Protokollieren Sie sie bei jeder fehlgeschlagenen Anfrage und nennen Sie sie, wenn Sie den [Support](https://passport.kabin.sh/docs/de/general/support) kontaktieren. ## Fehler behandeln - Entscheiden Sie anhand von `status` und `code`, nie anhand von `title`, `detail` oder `message`: Diese Texte können sich ändern. - Rechnen Sie mit neuen Codes. Behandeln Sie einen unbekannten `code` nach seiner HTTP-Statusklasse. - Beheben Sie `400`, `401`, `403`, `404` und `409`, statt die Anfrage unverändert zu wiederholen. - Wiederholen Sie `429` nach `Retry-After`, `5xx` und Netzwerkfehler mit exponentiellem Backoff. Bevor Sie einen Schreibzugriff wiederholen, lesen Sie [Wiederholungen und Idempotenz](https://passport.kabin.sh/docs/de/idempotency). - Antworten enthalten nie SQL, Stacktraces oder Fehler von Drittsystemen; ein unerwarteter Fehler ist `500 internal_error` mit Request-ID. Die Anleitung [Fehler behandeln](https://passport.kabin.sh/docs/de/guides/handle-errors) zeigt eine vollständige Fehlerbehandlung in TypeScript, Python und Go. --- # Paginierung > Lange Listen seitenweise mit `limit` und einem undurchsichtigen `cursor` lesen. Listen, die groß werden können, verwenden Keyset-Paginierung: Sie fordern mit `limit` eine Seite an, und die Antwort sagt Ihnen, wo die nächste Seite beginnt. ## Welche Operationen blättern | Operation | Reihenfolge | | ------------------------------------------------------------------------------------------------------------ | -------------------------- | | [`listBatteryModels`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-battery-models) | Nach Name, dann ID | | [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries) | Nach Seriennummer, dann ID | | [`listEvidence`](https://passport.kabin.sh/docs/de/api-reference/evidence/list-evidence) | Neueste zuerst | | [`listFacilities`](https://passport.kabin.sh/docs/de/api-reference/facilities/list-facilities) | Nach Name, dann ID | | [`listLifecycleEvents`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events) | Neueste zuerst | | [`listPassportVersions`](https://passport.kabin.sh/docs/de/api-reference/passports/list-passport-versions) | Neueste zuerst | Andere Listen, etwa Werte, Wertehistorien und Modellversionen, liefern alle Einträge in einer Antwort. ## Anfrage - **limit** `integer` (query, Standard: 50): Anzahl der Einträge pro Seite, von 1 bis 200. - **cursor** `string` (query): Der `next_cursor` der vorigen Seite. Für die erste Seite weglassen. Senden Sie die Filter der Operation (zum Beispiel `battery_model_id` oder `passport_status` bei `listBatteries`) mit jeder Seite unverändert mit. ## Antwort - **items** `object[]` (erforderlich): Die Einträge dieser Seite, höchstens `limit`. - **next_cursor** `string | null` (erforderlich): Der Cursor für die nächste Seite oder `null`, wenn dies die letzte Seite ist. ```json title="200" { "items": [ { "id": "0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b12", "serial_number": "SV2027-000001" }, { "id": "0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b13", "serial_number": "SV2027-000002" } ], "next_cursor": "eyJzIjoiU1YyMDI3LTAwMDAwMiJ9" } ``` Das Beispiel zeigt nur zwei Felder je Einheit; echte Einträge haben das vollständige Schema der Operation. ## Regeln für Cursor - Ein Cursor ist **undurchsichtig**. Lesen, bauen oder ändern Sie ihn nicht; sein Inhalt kann sich jederzeit ändern. - Ein Cursor gehört zu **der Abfrage, die ihn erzeugt hat**: dieselbe Operation, dieselben Pfadparameter, dieselben Filter. Verwenden Sie ihn gleich für die nächste Seite und speichern Sie Cursor nicht als dauerhafte Lesezeichen. - `next_cursor: null` heißt, dass keine weiteren Einträge folgen. - Ein unbrauchbarer Cursor ergibt `400 validation_error` auf dem Feld `cursor`; ein `limit` außerhalb von 1 bis 200 ein `400` auf dem Feld `limit`. - Seiten sind keine Momentaufnahme. Einträge, die während des Blätterns entstehen oder sich ändern, können erscheinen oder fehlen. Für eine konsistente Kopie blättern Sie durch und gleichen danach über die ID ab (siehe [Einheiten synchronisieren](https://passport.kabin.sh/docs/de/guides/sync-units)). ## Alle Seiten lesen Die folgenden Schleifen lesen alle Einheiten mit veröffentlichtem Pass, 200 pro Seite. ```ts title="TypeScript" type Battery = { id: string; serial_number: string }; type Page = { items: T[]; next_cursor: string | null }; const base = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }; async function* listBatteries(filters: Record): AsyncGenerator { let cursor: string | null = null; do { const query = new URLSearchParams({ ...filters, limit: '200' }); if (cursor) query.set('cursor', cursor); const res = await fetch(`${base}/batteries?${query}`, { headers }); if (!res.ok) throw new Error(`listBatteries: ${res.status} ${await res.text()}`); const page: Page = await res.json(); yield* page.items; cursor = page.next_cursor; } while (cursor); } let count = 0; for await (const unit of listBatteries({ passport_status: 'published' })) { console.log(unit.serial_number, unit.id); count++; } console.log(`${count} Einheiten`); ``` ```python title="Python" import os import requests base = f"{os.environ['BATTERYPASS_URL']}/api/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['BATTERYPASS_API_KEY']}" def list_batteries(**filters): params = {**filters, "limit": 200} while True: res = session.get(f"{base}/batteries", params=params, timeout=30) res.raise_for_status() page = res.json() yield from page["items"] if page["next_cursor"] is None: return params["cursor"] = page["next_cursor"] count = 0 for unit in list_batteries(passport_status="published"): print(unit["serial_number"], unit["id"]) count += 1 print(count, "Einheiten") ``` ```go title="Go" package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" "time" ) type battery struct { ID string `json:"id"` SerialNumber string `json:"serial_number"` } type batteryPage struct { Items []battery `json:"items"` NextCursor *string `json:"next_cursor"` } func fetchPage(client *http.Client, query url.Values) (batteryPage, error) { var page batteryPage req, err := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1/batteries?"+query.Encode(), nil) if err != nil { return page, err } req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY")) res, err := client.Do(req) if err != nil { return page, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return page, fmt.Errorf("listBatteries: %s", res.Status) } return page, json.NewDecoder(res.Body).Decode(&page) } func main() { client := &http.Client{Timeout: 30 * time.Second} query := url.Values{"limit": {"200"}, "passport_status": {"published"}} count := 0 for { page, err := fetchPage(client, query) if err != nil { log.Fatal(err) } for _, unit := range page.Items { fmt.Println(unit.SerialNumber, unit.ID) count++ } if page.NextCursor == nil { break } query.Set("cursor", *page.NextCursor) } fmt.Println(count, "Einheiten") } ``` > [!TIP] > Jede Seite ist eine Anfrage gegen das Limit Ihrer Organisation von 600 Anfragen pro Minute. Verwenden Sie > `limit=200` für Massenabfragen und lesen Sie für lange Durchläufe [Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits). --- # Ratenlimits > Wie viele Anfragen BatteryPass annimmt und wie Sie nach einem 429 zurückhaltend wiederholen. BatteryPass begrenzt Anfragen, um die Plattform und die anderen Organisationen darauf zu schützen. Eine Anfrage über einem Limit wird mit `429 rate_limited` und einem Header `Retry-After` beantwortet. ## Limits | Limit | Gezählt für | Zeitfenster | Gilt für | | ------------ | ---------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- | | 600 Anfragen | Alle API-Schlüssel einer Organisation zusammen | 1 Minute | Jede Anfrage mit gültigem Schlüssel, unabhängig vom Ergebnis | | 30 Versuche | Eine Client-Adresse | 15 Minuten | Anfragen mit unbekanntem oder widerrufenem Schlüssel | | 300 Anfragen | Eine Client-Adresse | 1 Minute | Öffentliche Abrufe von Pässen, öffentliche Dokumente und der GS1-Resolver (`/01/{gtin}/21/{serial}`) | Die Zeitfenster sind fest: Ein Fenster beginnt, zählt hoch und wird an seinem Ende zurückgesetzt. Alle Schlüssel einer Organisation teilen sich die 600 Anfragen; weitere Schlüssel erhöhen das Limit nicht. ## Die Antwort 429 ```http title="429" HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 17 X-Request-Id: 8b1e6f03c2d94a7e5f0a1b2c { "type": "about:blank", "title": "Too Many Requests", "status": 429, "code": "rate_limited", "detail": "Too many attempts. Try again later.", "request_id": "8b1e6f03c2d94a7e5f0a1b2c" } ``` `Retry-After` ist die Zahl der Sekunden bis zum Ende des aktuellen Zeitfensters. Eine mit `429` beantwortete Anfrage wurde nicht ausgeführt; Sie können sie nach der Wartezeit wiederholen, auch Schreibzugriffe. ## Zurückhaltend wiederholen Warten Sie mindestens `Retry-After` Sekunden, fügen Sie etwas zufällige Streuung hinzu, damit parallele Worker nicht alle im selben Moment starten, und geben Sie nach einigen Versuchen auf: ```ts title="TypeScript" const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // Wiederholt eine Anfrage nach 429-Antworten und wartet so lange, wie Retry-After sagt. async function fetchWithBackoff(url: string, init: RequestInit = {}, attempts = 5): Promise { for (let attempt = 1; ; attempt++) { const res = await fetch(url, init); if (res.status !== 429 || attempt === attempts) return res; const seconds = Number(res.headers.get('Retry-After')) || 2 ** attempt; await sleep(seconds * 1000 + Math.random() * 1000); } } const res = await fetchWithBackoff(`${process.env.BATTERYPASS_URL}/api/v1/battery-models`, { headers: { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }, }); console.log(res.status); ``` ```python title="Python" import os import random import time import requests def get_with_backoff(url, attempts=5, **kwargs): """Wiederholt ein GET nach 429-Antworten und wartet so lange, wie Retry-After sagt.""" for attempt in range(1, attempts + 1): res = requests.get(url, timeout=30, **kwargs) if res.status_code != 429 or attempt == attempts: return res delay = int(res.headers.get("Retry-After", 2 ** attempt)) time.sleep(delay + random.random()) res = get_with_backoff( f"{os.environ['BATTERYPASS_URL']}/api/v1/battery-models", headers={"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"}, ) print(res.status_code) ``` ```go title="Go" package main import ( "fmt" "log" "math/rand/v2" "net/http" "os" "strconv" "time" ) // doWithBackoff wiederholt eine Anfrage ohne Body nach 429-Antworten und wartet so lange, wie Retry-After sagt. func doWithBackoff(client *http.Client, req *http.Request, attempts int) (*http.Response, error) { for attempt := 1; ; attempt++ { res, err := client.Do(req) if err != nil || res.StatusCode != http.StatusTooManyRequests || attempt == attempts { return res, err } res.Body.Close() wait := time.Duration(1< # Wiederholungen und Idempotenz > Welche Anfragen Sie nach einem Timeout gefahrlos wiederholen können und wie Sie doppelte Ereignisse vermeiden. Wenn eine Anfrage in einen Timeout läuft oder die Verbindung abbricht, wissen Sie nicht, ob BatteryPass sie ausgeführt hat. Ob Sie sie einfach erneut senden können, hängt von der Operation ab. ## Überblick | Operationen | Methode | Gefahrlos wiederholbar | Warum | | ------------------------------------------------------------------------------------------------------------ | ------- | ------------------------------- | --------------------------------------------------------------------------------------------------- | | Alle Lesezugriffe, auch öffentliche Abrufe von Pässen | `GET` | Ja | Sie ändern nichts. | | [`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition) | `PATCH` | Ja, mit demselben `measured_at` | Eine Messung, die gleich alt oder älter als die gespeicherte ist, wird übersprungen und aufgeführt. | | [`createLifecycleEvent`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/create-lifecycle-event) | `POST` | Nein | Jeder erfolgreiche Aufruf erfasst ein neues Ereignis. | Eine mit `429` beantwortete Anfrage wurde nicht ausgeführt und lässt sich nach `Retry-After` immer wiederholen (siehe [Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits)). Eine Antwort `4xx` heißt, dass die Anfrage als Ganzes abgelehnt wurde: Korrigieren Sie sie, statt sie zu wiederholen. ## Lesezugriffe Jedes `GET` der API können Sie beliebig oft wiederholen. Wiederholen Sie Netzwerkfehler und Antworten `5xx` mit exponentiellem Backoff. ## Zustandsdaten `updateCondition` führt Messwerte mit den Zustandsdaten der Einheit zusammen. Für jeden Wert vergleicht BatteryPass sein `measured_at` mit dem des gespeicherten Werts: - **neuer** als die gespeicherte Messung oder gleicher Zeitpunkt mit anderem Wert: gespeichert, aufgeführt in `accepted`; - **gleicher Zeitpunkt und gleicher Wert**: übersprungen mit dem Grund `unchanged`; - **älter** als die gespeicherte Messung: übersprungen mit dem Grund `older_measurement`. Eine wiederholte Anfrage mit demselben Body ändert also nichts. Senden Sie zum Beispiel: ```json title="Request" { "measured_at": "2026-10-24T02:00:00Z", "values": [ { "field_key": "condition.state_of_charge", "value": 87.5 }, { "field_key": "condition.number_of_full_cycles", "value": 412 } ] } ``` Wurde der erste Versuch gespeichert, antwortet die Wiederholung: ```json title="200" { "accepted": [], "skipped": [ { "field_key": "condition.state_of_charge", "reason": "unchanged" }, { "field_key": "condition.number_of_full_cycles", "reason": "unchanged" } ], "publication": { "outcome": "not_published" } } ``` Diese Anfrage erfasst kein neues Lebenszyklusereignis und veröffentlicht nichts: `not_published` heißt hier, dass diese Anfrage nichts veröffentlicht hat; der Pass behält seine aktuelle Version. > [!WARNING] > Setzen Sie `measured_at` auf den Zeitpunkt der Messung, zum Beispiel den Zeitpunkt, zu dem Ihr > Batteriemanagementsystem den Wert erfasst hat, und behalten Sie ihn bei einer Wiederholung bei. Ein > `measured_at` aus der Uhrzeit beim Senden macht jede Wiederholung zu einer neuen Messung. ## Lebenszyklusereignisse `createLifecycleEvent` erkennt keine Duplikate: Jeder erfolgreiche Aufruf erfasst ein neues Ereignis, und die veröffentlichte API kennt keinen Idempotenzschlüssel. Prüfen Sie nach einem Timeout, einem Verbindungsabbruch oder einer Antwort `5xx`, ob das Ereignis angekommen ist, bevor Sie es erneut senden: 1. Lesen Sie [`listLifecycleEvents`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events) für die Einheit (neueste zuerst). 2. Suchen Sie ein Ereignis mit `source: "api"` und Ihrem `type`, `occurred_on` und `description`. 3. Senden Sie das Ereignis nur erneut, wenn es fehlt. Schreiben Sie eine eigene Referenz in `description`, etwa die Nummer des Serviceauftrags, damit sich das Ereignis leicht wiederfinden lässt. BatteryPass speichert die Beschreibung ohne führende und nachfolgende Leerzeichen. ```ts title="TypeScript" type NewEvent = { type: string; occurred_on: string; description: string }; type LifecycleEvent = NewEvent & { id: string; source: string }; const base = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}`, 'Content-Type': 'application/json', }; // Sucht ein Ereignis, das diese Integration schon erfasst hat (gleicher Typ, Tag und Beschreibung). async function findEvent(batteryId: string, e: NewEvent): Promise { let cursor: string | null = null; do { const query = new URLSearchParams({ limit: '200' }); if (cursor) query.set('cursor', cursor); const res = await fetch(`${base}/batteries/${batteryId}/lifecycle-events?${query}`, { headers }); if (!res.ok) throw new Error(`listLifecycleEvents: ${res.status}`); const page: { items: LifecycleEvent[]; next_cursor: string | null } = await res.json(); const hit = page.items.find( (x) => x.source === 'api' && x.type === e.type && x.occurred_on === e.occurred_on && x.description === e.description, ); if (hit) return hit; // Neueste zuerst: Sobald eine Seite ältere Tage erreicht, ist das Ereignis nicht da. if (page.items.some((x) => x.occurred_on < e.occurred_on)) return undefined; cursor = page.next_cursor; } while (cursor); return undefined; } async function recordEvent(batteryId: string, e: NewEvent): Promise { for (let attempt = 1; attempt <= 3; attempt++) { let res: Response | undefined; try { res = await fetch(`${base}/batteries/${batteryId}/lifecycle-events`, { method: 'POST', headers, body: JSON.stringify(e), signal: AbortSignal.timeout(30_000), }); } catch { res = undefined; // Timeout oder Verbindungsfehler } if (res?.status === 201) return res.json(); if (res && res.status < 500) throw new Error(`createLifecycleEvent: ${res.status} ${await res.text()}`); // Keine Antwort oder 5xx: Das Ereignis ist vielleicht trotzdem erfasst. const existing = await findEvent(batteryId, e); if (existing) return existing; } throw new Error('createLifecycleEvent: nach 3 Versuchen kein Erfolg'); } const event = await recordEvent('0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b12', { type: 'inspection', occurred_on: '2026-10-24', description: 'Serviceauftrag SO-2026-0815: jährliche Inspektion', }); console.log(event.id); ``` ```python title="Python" import os import requests base = f"{os.environ['BATTERYPASS_URL']}/api/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['BATTERYPASS_API_KEY']}" def find_event(battery_id, event): """Sucht ein Ereignis, das diese Integration schon erfasst hat (gleicher Typ, Tag und Beschreibung).""" params = {"limit": 200} while True: res = session.get(f"{base}/batteries/{battery_id}/lifecycle-events", params=params, timeout=30) res.raise_for_status() page = res.json() for item in page["items"]: if (item["source"] == "api" and item["type"] == event["type"] and item["occurred_on"] == event["occurred_on"] and item.get("description") == event["description"]): return item # Neueste zuerst: Sobald eine Seite ältere Tage erreicht, ist das Ereignis nicht da. if any(item["occurred_on"] < event["occurred_on"] for item in page["items"]) or not page["next_cursor"]: return None params["cursor"] = page["next_cursor"] def record_event(battery_id, event, attempts=3): for _ in range(attempts): try: res = session.post(f"{base}/batteries/{battery_id}/lifecycle-events", json=event, timeout=30) except (requests.Timeout, requests.ConnectionError): res = None # Timeout oder Verbindungsfehler if res is not None and res.status_code == 201: return res.json() if res is not None and res.status_code < 500: raise RuntimeError(f"createLifecycleEvent: {res.status_code} {res.text}") # Keine Antwort oder 5xx: Das Ereignis ist vielleicht trotzdem erfasst. existing = find_event(battery_id, event) if existing: return existing raise RuntimeError(f"createLifecycleEvent: nach {attempts} Versuchen kein Erfolg") event = record_event("0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b12", { "type": "inspection", "occurred_on": "2026-10-24", "description": "Serviceauftrag SO-2026-0815: jährliche Inspektion", }) print(event["id"]) ``` ```go title="Go" package main import ( "bytes" "encoding/json" "errors" "fmt" "log" "net/http" "net/url" "os" "time" ) type newEvent struct { Type string `json:"type"` OccurredOn string `json:"occurred_on"` Description string `json:"description"` } type lifecycleEvent struct { ID string `json:"id"` Type string `json:"type"` OccurredOn string `json:"occurred_on"` Description string `json:"description"` Source string `json:"source"` } var ( base = os.Getenv("BATTERYPASS_URL") + "/api/v1" client = &http.Client{Timeout: 30 * time.Second} ) // send ruft die API auf und dekodiert eine erfolgreiche JSON-Antwort in out; es liefert den HTTP-Status. func send(method, path string, body, out any) (int, error) { var buf bytes.Buffer if body != nil { if err := json.NewEncoder(&buf).Encode(body); err != nil { return 0, err } } req, err := http.NewRequest(method, base+path, &buf) if err != nil { return 0, err } req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := client.Do(req) if err != nil { return 0, err } defer res.Body.Close() if res.StatusCode >= 300 { return res.StatusCode, nil } return res.StatusCode, json.NewDecoder(res.Body).Decode(out) } // findEvent sucht ein Ereignis, das diese Integration schon erfasst hat (gleicher Typ, Tag und Beschreibung). func findEvent(batteryID string, e newEvent) (*lifecycleEvent, error) { query := url.Values{"limit": {"200"}} for { var page struct { Items []lifecycleEvent `json:"items"` NextCursor *string `json:"next_cursor"` } status, err := send(http.MethodGet, "/batteries/"+batteryID+"/lifecycle-events?"+query.Encode(), nil, &page) if err != nil { return nil, err } if status != http.StatusOK { return nil, fmt.Errorf("listLifecycleEvents: %d", status) } older := false for _, x := range page.Items { if x.Source == "api" && x.Type == e.Type && x.OccurredOn == e.OccurredOn && x.Description == e.Description { return &x, nil } older = older || x.OccurredOn < e.OccurredOn } // Neueste zuerst: Sobald eine Seite ältere Tage erreicht, ist das Ereignis nicht da. if older || page.NextCursor == nil { return nil, nil } query.Set("cursor", *page.NextCursor) } } func recordEvent(batteryID string, e newEvent) (*lifecycleEvent, error) { for attempt := 1; attempt <= 3; attempt++ { var created lifecycleEvent status, err := send(http.MethodPost, "/batteries/"+batteryID+"/lifecycle-events", e, &created) if err == nil && status == http.StatusCreated { return &created, nil } if err == nil && status < 500 { return nil, fmt.Errorf("createLifecycleEvent: %d", status) } // Keine Antwort oder 5xx: Das Ereignis ist vielleicht trotzdem erfasst. existing, err := findEvent(batteryID, e) if err != nil { return nil, err } if existing != nil { return existing, nil } } return nil, errors.New("createLifecycleEvent: nach 3 Versuchen kein Erfolg") } func main() { e, err := recordEvent("0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b12", newEvent{ Type: "inspection", OccurredOn: "2026-10-24", Description: "Serviceauftrag SO-2026-0815: jährliche Inspektion", }) if err != nil { log.Fatal(err) } fmt.Println(e.ID) } ``` Die Beispiele behandeln ein `429` wie jede andere Ablehnung; kombinieren Sie sie im Betrieb mit dem Backoff aus [Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits). ## Der Header Idempotency-Key BatteryPass kennt einen Header `Idempotency-Key`, aber nur bei Operationen des Portals wie dem Veröffentlichen und Massenjobs. Keine der veröffentlichten Operationen akzeptiert ihn; senden Sie ihn also nicht an die API, dort bewirkt er nichts. --- # Stabilität und Versionierung > Worauf sich Integrationen verlassen können, wie sich die API ändert und wo Sie den Vertrag laden. ## Zwei Versionsnummern | Version | Wo | Ändert sich, wenn | | --------------- | ------------------------------------------- | ----------------------------------------------------- | | Pfadversion | `/api/v1` in jeder URL | sich veröffentlichte Operationen inkompatibel ändern. | | Vertragsversion | `info.version` im Vertrag, derzeit `0.10.0` | sich der Vertrag in irgendeiner Weise ändert. | Das [Changelog](https://passport.kabin.sh/docs/de/changelog) führt jede Vertragsversion mit ihren Änderungen auf. ## Worauf Sie sich verlassen können Der veröffentlichte Vertrag (ADR-020) enthält genau die Operationen, die Integrationen nutzen dürfen: die Operationen, die einen API-Schlüssel akzeptieren, und die öffentlichen Abrufe von Pässen (Tag `public`). Alles darin ist die API, die diese Dokumentation beschreibt und die [API-Referenz](https://passport.kabin.sh/docs/de/api-reference) auflistet. Das BatteryPass-Portal nutzt weitere, interne Operationen: Anmeldung, Mitglieder, Verwaltung von API-Schlüsseln, Prüfung, Veröffentlichung, Importe. Sie stehen nicht im veröffentlichten Vertrag, brauchen eine Portalsitzung und können sich mit jedem Release ohne Ankündigung ändern. Bauen Sie nicht darauf auf. ## Kompatible Änderungen Diese Änderungen können mit jedem Release ohne neue Pfadversion kommen: - neue Operationen; - neue optionale Query-Parameter und neue optionale Eigenschaften in Request-Bodys; - neue Eigenschaften in Antworten; - neue Enum-Werte, zum Beispiel ein neuer Passstatus oder Typ von Lebenszyklusereignis; - neue Problem-Codes und Codes für Feldfehler; - eine neue Version des Feldregisters mit neuen Feldern, Bezeichnungen oder Anforderungen (lesbar mit [`getRegistryInfo`](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-info)). Schreiben Sie Ihren Client so, dass ihn das nicht bricht: - ignorieren Sie unbekannte Eigenschaften in Antworten; - behandeln Sie unbekannte Enum-Werte mit einem sinnvollen Standard, statt abzubrechen; - behandeln Sie einen unbekannten Problem-`code` nach seinem HTTP-Status; - validieren Sie Antworten nicht streng gegen eine ältere Kopie des Vertrags. ## Inkompatible Änderungen Inkompatibel ist es, eine Operation, einen Parameter oder eine Eigenschaft zu entfernen oder umzubenennen, einen Typ oder eine Bedeutung zu ändern oder optionale Eingaben verpflichtend zu machen. Solche Änderungen an veröffentlichten Operationen kommen nur mit einer neuen Pfadversion und werden im [Changelog](https://passport.kabin.sh/docs/de/changelog) angekündigt. ## Vertrag herunterladen Der Vertrag ist ein OpenAPI-3.0-Dokument, als JSON und YAML, ohne Authentifizierung abrufbar: | Adresse | Inhalt | | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `$BATTERYPASS_URL/api/v1/openapi.json`, `$BATTERYPASS_URL/api/v1/openapi.yaml` | Der Vertrag der BatteryPass-Instanz, die Ihre Organisation nutzt | | [`/docs/openapi.json`](https://passport.kabin.sh/docs/openapi.json), [`/docs/openapi.yaml`](https://passport.kabin.sh/docs/openapi.yaml) | Der Vertrag, den diese Dokumentation beschreibt | Für Ihre Integration maßgeblich ist der Vertrag Ihrer Instanz: Er passt zu der Version, die Ihre Anfragen beantwortet, und kann älter oder neuer sein als der auf dieser Website. Prüfen Sie seine Version, bevor Sie einen Client erzeugen: ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/openapi.json" | jq -r '.info.version' ``` > [!TIP] > Erzeugen Sie typisierte Clients aus dem Vertrag und merken Sie sich die `info.version`, aus der Sie erzeugt > haben. Ein Vergleich mit der Version der Instanz in Ihrem Monitoring zeigt, wann ein Blick ins Changelog > fällig ist. --- # Umgebungen und Basis-URL > Wo die API Ihrer Organisation liegt, welche Adressen sie bedient und wie Sie gefahrlos testen. ## Basis-URL BatteryPass läuft je Instanz; einen gemeinsamen, globalen API-Host gibt es nicht. Ihre Basis-URL ist die BatteryPass-Adresse Ihrer Organisation, also dieselbe Adresse, unter der Sie das Portal nutzen, zum Beispiel `https://batterypass.example.com`. Die API liegt unter `/api/v1` auf dieser Adresse. ## Adressen | Pfad auf Ihrer BatteryPass-Adresse | Was | Authentifizierung | | ---------------------------------------------- | --------------------------------------------------------------------- | ----------------- | | `/api/v1/...` | Die Integrations-API | API-Schlüssel | | `/api/v1/public/...` | Öffentliche Abrufe von Pässen | Keine | | `/api/v1/openapi.json`, `/api/v1/openapi.yaml` | Der veröffentlichte Vertrag | Keine | | `/01/{gtin}/21/{serial}` | GS1-Digital-Link-Resolver, das Ziel der QR-Codes | Keine | | `/pass/{passportId}` | Öffentliche Passseite für Browser | Keine | | `/.well-known/batterypass/keys.json` | Öffentliche Signaturschlüssel (JWK-Set) zum Prüfen von Passsignaturen | Keine | | `/docs/` | Diese Dokumentation | Keine | > [!NOTE] > Eine Instanz kann einen eigenen Resolver-Host in ihre QR-Codes drucken. Übernehmen Sie den Digital Link einer > Einheit aus dem Feld `gs1_digital_link` von > [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport), statt ihn selbst zusammenzusetzen. Siehe > [GS1 Digital Link](https://passport.kabin.sh/docs/de/concepts/digital-link). ## Konventionen - **HTTPS.** Senden Sie Anfragen und vor allem Schlüssel nur über HTTPS. - **JSON.** Request- und Response-Bodys sind UTF-8-JSON; senden Sie zu einem Body `Content-Type: application/json`. Bodys sind standardmäßig auf 1 MiB begrenzt (`413 payload_too_large`), unbekannte Eigenschaften werden abgelehnt. - **Zeitpunkte** (`date-time`) sind UTC nach RFC 3339, z. B. `2026-10-24T02:00:00Z`. Senden Sie Zeitpunkte mit Zeitzonenversatz; UTC ist die sicherste Wahl. - **Datumsangaben** (`date`, z. B. `occurred_on`, `manufacturing_date`) haben die Form `YYYY-MM-DD` ohne Zeitzone. - **IDs** sind UUID-Strings. Die dauerhafte Passkennung ist `urn:dpp:{uuid}`; öffentliche Routen erwarten nur die UUID. ## Client einrichten Alle Beispiele lesen zwei Umgebungsvariablen: | Variable | Wert | | --------------------- | -------------------------------------------------------------------------------------------------- | | `BATTERYPASS_URL` | Ihre BatteryPass-Adresse ohne abschließenden Schrägstrich, z. B. `https://batterypass.example.com` | | `BATTERYPASS_API_KEY` | Ein API-Schlüssel Ihrer Organisation, `bp_live_...` | ```bash title="Shell" export BATTERYPASS_URL="https://batterypass.example.com" export BATTERYPASS_API_KEY="bp_live_..." ``` ```ts title="TypeScript" const url = process.env.BATTERYPASS_URL?.replace(/\/+$/, ''); const key = process.env.BATTERYPASS_API_KEY; if (!url || !key) throw new Error('BATTERYPASS_URL und BATTERYPASS_API_KEY setzen.'); if (!url.startsWith('https://')) console.warn('BATTERYPASS_URL sollte HTTPS verwenden.'); export const api = `${url}/api/v1`; export const headers = { Authorization: `Bearer ${key}` }; ``` ```python title="Python" import os url = os.environ.get("BATTERYPASS_URL", "").rstrip("/") key = os.environ.get("BATTERYPASS_API_KEY", "") if not url or not key: raise SystemExit("BATTERYPASS_URL und BATTERYPASS_API_KEY setzen.") if not url.startswith("https://"): print("BATTERYPASS_URL sollte HTTPS verwenden.") api = f"{url}/api/v1" headers = {"Authorization": f"Bearer {key}"} ``` ```go title="Go" package main import ( "fmt" "log" "os" "strings" ) func main() { url := strings.TrimRight(os.Getenv("BATTERYPASS_URL"), "/") key := os.Getenv("BATTERYPASS_API_KEY") if url == "" || key == "" { log.Fatal("BATTERYPASS_URL und BATTERYPASS_API_KEY setzen.") } if !strings.HasPrefix(url, "https://") { log.Print("BATTERYPASS_URL sollte HTTPS verwenden.") } api := url + "/api/v1" fmt.Println("API-Basis:", api) } ``` Bewahren Sie den Schlüssel in einem Secret-Speicher oder der Secret-Verwaltung Ihrer Plattform auf, nicht im Quellcode oder in Dateien, die Sie committen. Siehe [Sichere Schlüsselverwaltung](https://passport.kabin.sh/docs/de/guides/secure-key-handling). ## Testen BatteryPass hat keine öffentliche Sandbox. Testen Sie in einer Umgebung, in der Fehler keinen Schaden anrichten: - **Eine eigene Testorganisation** oder eine eigene BatteryPass-Instanz mit eigenen Schlüsseln. Daten sind immer nach Organisation getrennt; ein Testschlüssel sieht oder ändert also nie Produktionsdaten. Bitten Sie die Betreiber Ihrer BatteryPass-Instanz, sie einzurichten. - **Schreibzugriffe nicht gegen die Produktion testen.** Eine Zustandsmeldung per Schlüssel kann einen öffentlichen Pass je nach Einstellung der Organisation sofort neu veröffentlichen, und Lebenszyklusereignisse lassen sich über die API nicht löschen. - **Auf Öffentliches achten.** Pässe, die in einer Testorganisation veröffentlicht werden, sind ebenfalls öffentlich, unter denselben Adressen. Verwenden Sie für Testdaten keine GTINs echter Produkte und drucken oder verteilen Sie keine QR-Codes aus Tests. - **Konfiguration trennen.** Halten Sie Test- und Produktionswerte von `BATTERYPASS_URL` und `BATTERYPASS_API_KEY` auseinander und geben Sie Testschlüsseln nur die Scopes, die der Test braucht. --- # Batteriemodelle, Versionen und Einheiten > Wie BatteryPass Produkt, technische Stände und einzelne Batterien trennt und wo welcher Wert gespeichert ist. BatteryPass hält Produktdaten auf drei Ebenen. Was alle Batterien eines Produkts gemeinsam haben, pflegen Sie einmal an der Modellversion; was sich von Batterie zu Batterie unterscheidet, gehört zur Einheit. Jede Einheit bekommt einen eigenen Pass, der bei der Veröffentlichung aus beiden Ebenen zusammengesetzt wird. ```mermaid flowchart LR M["Batteriemodell: Name, SKU, GTIN, Kategorie"] --> VA["Modellversion A: abgelöst"] M --> VB["Modellversion B: freigegeben"] VA --> U1["Einheit SN-000184"] VB --> U2["Einheit SN-000417"] U2 --> P["Pass von SN-000417"] ``` ## Die drei Ebenen | Ebene | Was sie ist | Wichtige Felder | Lesen mit | | ------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Batteriemodell (`BatteryModel`) | Das Handelsprodukt | `name`, `sku`, `gtin`, `category`, `archived_at` | [`listBatteryModels`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-battery-models), [`getBatteryModel`](https://passport.kabin.sh/docs/de/api-reference/catalog/get-battery-model) | | Modellversion (`ModelVersion`) | Ein technischer Stand des Modells, etwa nach einem Zellwechsel | `version_label`, `effective_from`, `status`, `unit_count`, `approved_at`, `registry_version` | [`listModelVersions`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-model-versions), [`getModelVersion`](https://passport.kabin.sh/docs/de/api-reference/catalog/get-model-version) | | Einheit (`Battery`) | Eine physische Batterie mit Seriennummer | `serial_number`, `model_version_id`, `manufacturing_date`, `facility_id`, `battery_status`, `passport_status` | [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries), [`getBattery`](https://passport.kabin.sh/docs/de/api-reference/catalog/get-battery), [`exportUnits`](https://passport.kabin.sh/docs/de/api-reference/catalog/export-units) | Die `category` eines Modells (`ev`, `lmt`, `industrial`, `stationary`, `industrial_without_bms`) bestimmt, welche Felder das Register für seine Batterien verlangt. ## Status einer Modellversion Eine Modellversion hat ein Startdatum, `effective_from`, und einen Status: | `status` | Bedeutung | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `draft` | In Vorbereitung. Einheiten einer Entwurfsversion lassen sich nicht veröffentlichen. | | `approved` | Der aktuelle technische Stand. Ein Modell hat höchstens eine freigegebene Version; die Freigabe erteilt ein Prüfer im Portal. | | `superseded` | Von einer späteren freigegebenen Version abgelöst. Sie bleibt lesbar und behält ihre Einheiten; deren veröffentlichte Pässe ändern sich nicht. | Ein Enddatum gibt es nicht: Eine Version gilt ab ihrem `effective_from`, bis eine spätere Version übernimmt. Wird eine Einheit im Portal oder per Tabellenimport ohne Angabe einer Version angelegt, ordnet BatteryPass ihr die Version zu, die an ihrem Herstellungsdatum galt: die mit dem spätesten `effective_from` an oder vor diesem Datum, wobei freigegebene und abgelöste Versionen Vorrang vor Entwürfen haben. Die Freigabe einer neuen Version verschiebt keine bestehende Einheit. Mitglieder verschieben Einheiten im Portal nach einer Vorschau der Auswirkungen auf eine andere Version; der veröffentlichte Pass einer verschobenen Einheit wird dann `update_needed` ([Pässe](https://passport.kabin.sh/docs/de/concepts/passports)). ## Welche Werte wo liegen Das [Feldregister](https://passport.kabin.sh/docs/de/general/glossary) ordnet jedes Feld einer Ebene zu. Sie lesen es mit [`getRegistrySchema`](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-schema): Jedes Feld hat `level` mit dem Wert `model` oder `unit`. | Ebene | Typische Felder | Werte | Vollständigkeit | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Modellversion | Hersteller, Masse, Garantie, Chemie und Zusammensetzung, gefährliche Stoffe, CO₂-Fußabdruck, Sorgfaltspflichten, Kreislauffähigkeit, Leistungskennwerte, Kennzeichnung und Konformitätserklärung, z. B. `performance.nominal_voltage`, `materials.battery_chemistry` | [`listModelVersionValues`](https://passport.kabin.sh/docs/de/api-reference/values/list-model-version-values) | [`getModelVersionCompleteness`](https://passport.kabin.sh/docs/de/api-reference/values/get-model-version-completeness) | | Einheit | Seriennummer, Herstellungsdatum und -ort, Batteriestatus, Datum der Inbetriebnahme und die Zustandsdaten, z. B. `identification.serial_number`, `condition.capacity_fade` | [`listBatteryValues`](https://passport.kabin.sh/docs/de/api-reference/values/list-battery-values) | [`getBatteryCompleteness`](https://passport.kabin.sh/docs/de/api-reference/values/get-battery-completeness) | Einige Registerfelder sind keine eigenen Werte, sondern Eigenschaften des Modells oder der Einheit selbst: Produktname, SKU, GTIN und Kategorie des Modells; Seriennummer, Herstellungsdatum, Produktionsstätte, Batteriestatus und Datum der Inbetriebnahme der Einheit. Sie lesen sie an `BatteryModel` und `Battery`. > [!NOTE] > Die Vollständigkeit einer Einheit enthält auch die Zusammenfassung ihrer Modellversion (`model_version`). Ein > Aufruf zeigt also, ob eine der beiden Ebenen die Veröffentlichung noch blockiert. ## Kennungen | Kennung | Wo | Regeln | | ------------------------------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | GTIN | Am Batteriemodell | Mit 8, 12, 13 oder 14 Stellen angenommen, als GTIN-14 mit gültiger Prüfziffer gespeichert | | Seriennummer | An der Einheit | Buchstaben, Ziffern, `-`, `/` und `.`, höchstens 50 Zeichen, eindeutig je Modell | | GTIN und Seriennummer zusammen | Der Schlüssel der Einheit | Eindeutig über alle Organisationen; der [GS1 Digital Link](https://passport.kabin.sh/docs/de/concepts/digital-link) enthält genau dieses Paar | Sobald die Kennungen gesperrt sind (für den Etikettendruck vor der Veröffentlichung oder bei der ersten Veröffentlichung), ist `identifiers_locked_at` gesetzt; Seriennummer und GTIN des Modells lassen sich dann nicht mehr ändern. Second Life: Eine umgewidmete, wiederverwendete oder wiederaufbereitete Batterie ist eine neue Einheit. Sie nennt das Original in `predecessor_battery_id` (das Original führt sie in `successor_battery_ids`) und bekommt einen eigenen Pass. ## Den Katalog lesen Lesen Sie die Versionen eines Modells und dann die Einheiten einer Version: ```bash title="cURL" curl -s "$BATTERYPASS_URL/api/v1/battery-models/$MODEL_ID/versions" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" curl -s "$BATTERYPASS_URL/api/v1/batteries?model_version_id=$VERSION_ID&limit=200" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` `listModelVersions` liefert höchstens 200 Versionen, das späteste `effective_from` zuerst, ohne Cursor. `listBatteries` ist nach Seriennummer sortiert und wird mit `limit` und `cursor` geblättert ([Paginierung](https://passport.kabin.sh/docs/de/pagination)); außerdem filtert es nach `battery_model_id`, `passport_status`, `serial_prefix` und `import_id`. > [!NOTE] > Die API liest den Katalog, sie legt ihn nicht an und ändert ihn nicht. Modelle, Versionen und Einheiten legen > Mitglieder im Portal oder per Tabellenimport an; Modellversionen geben Prüfer frei. --- # Pässe und Versionen > Der Pass einer Einheit, seine Status, seine signierten und unveränderlichen Versionen, die Unterschiede zwischen ihnen und die Exporte. Jede Einheit hat genau einen Pass. Jede Veröffentlichung fügt eine nummerierte Version hinzu, die sich danach nie mehr ändert. Ändern sich die Daten einer Einheit, wird der Pass erneut veröffentlicht und bekommt eine neue Version; die alten Versionen bleiben als Historie des Passes lesbar. ## Der Pass Den Pass einer Einheit lesen Sie mit [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport). | Feld | Bedeutung | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `passport_id` | Die dauerhafte Kennung `urn:dpp:{uuid}`, vergeben beim Sperren der Kennungen oder bei der ersten Veröffentlichung. Öffentliche Operationen erwarten den UUID-Teil als `passportId`. | | `status` | Siehe unten. | | `gs1_digital_link` | Der Link im QR-Code ([GS1 Digital Link](https://passport.kabin.sh/docs/de/concepts/digital-link)); fest, sobald die Kennungen gesperrt sind. | | `gtin`, `serial_number` | Die Kennungen der Einheit, wie sie gesperrt wurden. | | `current_version` | Zusammenfassung der Version, die gerade online ist. | | `first_published_at`, `identifiers_locked_at` | Wann der Pass zum ersten Mal online ging und wann seine Kennungen festgelegt wurden. | | `public_url` | Die öffentliche Passseite, sobald veröffentlicht. | | `withdrawn_at`, `withdrawn_reason` | Gesetzt, wenn der Pass offline genommen wurde. | Eine Einheit, deren Kennungen nicht gesperrt sind und die nie veröffentlicht wurde, hat einen Pass im Status `draft` ohne `passport_id`. ## Status ```mermaid stateDiagram-v2 [*] --> draft draft --> qr_locked: Kennungen gesperrt draft --> published: erste Veröffentlichung qr_locked --> published: erste Veröffentlichung published --> update_needed: Daten oder Register geändert update_needed --> published: erneut veröffentlicht published --> withdrawn: mit Begründung zurückgezogen ``` | `status` | Online | Bedeutung | | ------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `draft` | Nein | Noch nichts veröffentlicht, keine Kennung. | | `qr_locked` | Nein | Kennungen gesperrt, damit Etiketten vor der ersten Veröffentlichung gedruckt werden können. Öffentliche Abfragen liefern den Status ohne Inhalt. | | `published` | Ja | Die aktuelle Version ist öffentlich. | | `update_needed` | Ja | Weiter mit der aktuellen Version online, muss aber erneut veröffentlicht werden: Das Register hat sich geändert, die Einheit wurde einer anderen Modellversion zugeordnet, oder eine automatische Veröffentlichung von Zustandsdaten wurde blockiert. | | `withdrawn` | Nein | Von einem Admin oder Inhaber mit Begründung offline genommen. Öffentliche Abfragen liefern den Status ohne Inhalt. | | `suspended`, `deleted_recycled` | Nein | Im Vertrag für später vorgesehen; die aktuelle Version setzt sie nicht. | ## Versionen Eine Version entsteht durch eine Veröffentlichung. Prüfer veröffentlichen im Portal mit einer Begründung; Zustandsdaten können eine Version auch automatisch veröffentlichen ([Zustandsdaten und Lebenszyklus](https://passport.kabin.sh/docs/de/concepts/condition-and-lifecycle)). Unveränderter Inhalt erzeugt keine neue Version. Jede Version speichert unveränderlich: - den **Snapshot**: jedes Feld beider Ebenen in jeder Zugriffsstufe, mit Wert, Einheit, Status und Herkunft (Quelle, Revision, Prüfer, Prüf- oder Messzeitpunkt, Dokumente); - das **öffentliche Dokument**: die Projektion des Snapshots auf die Stufe `public`; - eine [Signatur](https://passport.kabin.sh/docs/de/concepts/signatures) für beide, mit den Hashes `canonical_hash` und `public_hash` und der `key_id`; - den BatteryPass-v2.0-Export und seinen Schema-Bericht (nur zur Information: Er blockiert keine Veröffentlichung). | Operation | Liefert | | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | [`listPassportVersions`](https://passport.kabin.sh/docs/de/api-reference/passports/list-passport-versions) | Versionsübersichten, die neueste zuerst. Mit `at` ist der erste Eintrag die Version, die zu diesem Zeitpunkt galt. | | [`getPassportVersion`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-version) | Eine Version mit `snapshot`, `public_document`, beiden Signaturen und dem Bericht `batterypass_v2`. | | [`getPublicPassportVersion`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-version) | Den öffentlichen Pass, wie er zum Zeitpunkt `at` war, ohne Schlüssel. | Eine Übersicht enthält `version_no`, `published_at`, `reason`, `published_by_name` (fehlt, wenn das System veröffentlicht hat), `registry_version`, die Hashes, `key_id` und `valid_until`. `valid_until` wird gesetzt, sobald eine spätere Version diese ablöst; gültig zum Zeitpunkt `t` ist also die Version mit `published_at ≤ t < valid_until`. ## Unterschiede zwischen Versionen [`getPassportVersionDiff`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-version-diff) vergleicht zwei signierte Snapshots Feld für Feld. Standardmäßig vergleicht es eine Version mit ihrer Vorgängerin; `against` nennt eine andere Versionsnummer. | `fields[].change` | Bedeutung | | ----------------- | ------------------------------------------------------------------------ | | `added` | Das Feld hat in dieser Version einen Wert und hatte vorher keinen. | | `removed` | Das Feld hatte vorher einen Wert und hat jetzt keinen. | | `changed` | Der Wert hat sich geändert. | | `provenance` | Gleicher Wert, neuere Revision: erneut freigegeben oder erneut gemessen. | Jede Änderung hat `before` und `after` mit Wert, Einheit, Zugriffsstufe, Quelle, Revision, `measured_at` und den Titeln der Dokumente. `metadata[]` listet Änderungen an `model_version`, `registry_version`, `category`, `facility`, `operator` und `predecessor`. ```bash title="cURL" curl -s "$BATTERYPASS_URL/api/v1/batteries/$BATTERY_ID/passport/versions/3/diff" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` ## Exporte | Export | Operation | Inhalt | | ----------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | BatteryPass JSON | [`exportPassportJson`](https://passport.kabin.sh/docs/de/api-reference/passports/export-passport-json) | Das BatteryPass-v2.0-Dokument einer Version mit allen Zugriffsstufen, wie bei der Veröffentlichung gespeichert. Als Anhang gesendet. | | PDF | [`exportPassportPdf`](https://passport.kabin.sh/docs/de/api-reference/passports/export-passport-pdf) | Alle Zugriffsstufen mit markierten eingeschränkten Zeilen, der Signatur und dem QR-Code; `lang=de` oder `lang=en`. | | QR-Code | [`getPassportQr`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-qr) | Der GS1 Digital Link als PNG (`size` 128 bis 2048 Pixel, Standard 512) oder SVG (`format=svg`). Erst nach dem Sperren der Kennungen; vorher `409 conflict`. | | Öffentliches JSON | [`getPublicPassport`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport) | Das signierte öffentliche Dokument der aktuellen Version, ohne Schlüssel. | > [!WARNING] > BatteryPass JSON und PDF enthalten die eingeschränkten Zugriffsstufen. Behandeln Sie diese Dateien vertraulich > und veröffentlichen Sie sie nicht; für alle gedacht ist das öffentliche JSON > ([Zugriffsstufen](https://passport.kabin.sh/docs/de/concepts/access-tiers)). ## EU-Register [`getRegistrySubmissionStatus`](https://passport.kabin.sh/docs/de/api-reference/passports/get-registry-submission-status) meldet die Registrierung des Passes im EU-Register. Das EU-Register bietet Wirtschaftsakteuren noch keine Schnittstelle; BatteryPass übermittelt deshalb nichts, und der Zustand ist `not_available`. Ohne Bestätigung des Registers wird ein Pass nie als registriert angezeigt. > [!NOTE] > Veröffentlichen und das Sperren von Kennungen sind Portal-Aktionen für Prüfer, das Zurückziehen eines Passes > eine für Admins und Inhaber. Ein API-Schlüssel liest Pässe und ihre Versionen, veröffentlicht oder zieht aber > nie zurück. --- # Zugriffsstufen > Welche Passdaten öffentlich sind, welche eingeschränkt, und wie BatteryPass eingeschränkte Daten aus öffentlichen Antworten heraushält. Die Batterieverordnung teilt die Daten eines Passes nach Empfängern auf (Art. 77 Abs. 2, Anhang XIII). BatteryPass ordnet jedem Feld des Registers genau eine Zugriffsstufe zu und filtert serverseitig: Eine Antwort enthält nie Daten oberhalb der Stufe, für die sie bestimmt ist. ## Die Stufen `Tier` bezeichnet die Stufe eines Felds oder Dokuments, `PassportTier` die Stufe, auf die ein Passdokument projiziert wird. Eine Projektion enthält ihre Stufe und die vorherigen: `legitimate_interest` schließt `public` ein, `authority` beide. | Stufe | Empfänger | Beispiele | | --------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | `public` | Alle, die den QR-Code scannen | Hersteller, Kategorie, Chemie, Kapazität, Spannungen, gefährliche Stoffe, Konformitätserklärung | | `legitimate_interest` | Personen mit berechtigtem Interesse sowie notifizierte Stellen, Marktüberwachungsbehörden und die Kommission | Detaillierte Zusammensetzung, Demontageinformationen, Batteriestatus, Zustandsdaten wie Kapazitätsverlust und Zyklenzahl | | `authority` | Notifizierte Stellen, Marktüberwachungsbehörden und die Kommission | Ergebnisse von Prüfberichten | | `internal` | Nur Ihre Organisation | Nie Teil einer Passprojektion | Die Stufe jedes Felds legt das Register fest: [`getRegistrySchema`](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-schema) liefert `tier` für jedes Feld. Im aktuellen Register sind alle Felder der Zustandsdaten `legitimate_interest`. ## Die öffentliche Projektion Bei der Veröffentlichung projiziert BatteryPass den Snapshot der Version auf die Stufe `public`, speichert das Ergebnis als öffentliches Dokument der Version und [signiert es](https://passport.kabin.sh/docs/de/concepts/signatures). Öffentliche Operationen liefern dieses gespeicherte Dokument; sie projizieren alte Versionen nie neu mit neuerem Code oder einem neueren Register. Eine spätere Registeränderung kann in einer bereits veröffentlichten Version also weder Daten offenlegen noch verbergen. Das öffentliche Dokument enthält nur Felder der Stufe `public`. Für Felder darüber, die einen Wert haben, nennt es in `restricted_fields_present` die Schlüssel, nie die Werte. Dokumente einer eingeschränkten Stufe fehlen ganz, nicht einmal ihr Titel erscheint. ```bash title="cURL" curl -s "$BATTERYPASS_URL/api/v1/public/passports/$PASSPORT_UUID" \ | jq '{tier: .document.tier, restricted: .document.restricted_fields_present}' ``` ```json title="Antwort" { "tier": "public", "restricted": ["identification.battery_status", "condition.capacity_fade"] } ``` ## Öffentliche Operationen liefern nur die öffentliche Stufe Diese Operationen brauchen keinen Schlüssel und liefern ausschließlich Daten der Stufe `public`: | Operation | Liefert | | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | | [`getPublicPassport`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport) | Das öffentliche Dokument der aktuellen Version | | [`getPublicPassportByProductId`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-by-product-id) | Dasselbe, gesucht über GTIN und Seriennummer | | [`getPublicPassportVersion`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-version) | Das öffentliche Dokument, das zu einem Zeitpunkt galt | | [`downloadPublicPassportDocument`](https://passport.kabin.sh/docs/de/api-reference/public/download-public-passport-document) | Ein Dokument der Stufe `public`, das ein öffentliches Feld nennt; alles andere ist `404` | | [`verifyPassport`](https://passport.kabin.sh/docs/de/api-reference/public/verify-passport) | Das Ergebnis der Signaturprüfung, ohne Daten | Der [Resolver](https://passport.kabin.sh/docs/de/concepts/digital-link) hinter dem QR-Code verweist auf dieselbe öffentliche Seite und dasselbe JSON. ## Was ein API-Schlüssel liest Ein API-Schlüssel liest die Pässe Ihrer eigenen Organisation mit den Rechten eines Betrachters, und dazu gehören die eingeschränkten Stufen Ihrer eigenen Daten: Der `snapshot` von [`getPassportVersion`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-version), [`exportPassportJson`](https://passport.kabin.sh/docs/de/api-reference/passports/export-passport-json) und [`exportPassportPdf`](https://passport.kabin.sh/docs/de/api-reference/passports/export-passport-pdf) enthalten alle Stufen. Eine Ausnahme: Dateien der Stufen `authority` und `internal` dürfen nur Bearbeiter und höhere Rollen herunterladen, deshalb antwortet [`downloadEvidence`](https://passport.kabin.sh/docs/de/api-reference/evidence/download-evidence) einem Schlüssel bei ihnen mit `403 forbidden`. > [!WARNING] > Was Ihre Integration mit einem Schlüssel liest, ist nicht automatisch öffentlich. Halten Sie eingeschränkte Werte > aus allem heraus, was Endkunden sehen, und veröffentlichen Sie nur, was das öffentliche JSON enthält. ## Eingeschränkte Stufen für Dritte Dritte, etwa eine Werkstatt oder eine Behörde, lesen eingeschränkte Stufen nicht über die API. Dafür hat BatteryPass einen eigenen Zugang im Portal: - Er ist ausgeschaltet, solange der Betreiber der Installation ihn nicht einschaltet (`TIER_PORTAL_ENABLED`, standardmäßig aus). Solange er aus ist, enthält das öffentliche JSON kein `links.restricted`, und nichts verweist auf ihn. - Ist er an, gibt ein Inhaber oder Admin des Wirtschaftsakteurs der E-Mail-Adresse eines Lesers die Stufe `legitimate_interest` oder `authority` frei, mit einem Zweck und einer Laufzeit von höchstens einem Jahr, und kann die Freigabe jederzeit widerrufen. - Der Leser meldet sich mit dieser bestätigten Adresse im Portal an und sieht die aktuelle Version in der freigegebenen Stufe. Eingeschränkte Dokumente erscheinen nur mit ihrem Titel. - Jeder eingeschränkte Lesezugriff wird im Audit-Log der Organisation des Passes festgehalten. Dieser Zugang ist keine Funktion von API-Schlüsseln und nicht Teil des veröffentlichten Vertrags. > [!NOTE] > Der Durchführungsrechtsakt nach Art. 77 Abs. 9, der festlegt, wer ein berechtigtes Interesse hat und wie er es > nachweist, steht noch aus. Bis dahin entscheidet der Wirtschaftsakteur, wem er Zugang gewährt; BatteryPass stellt > diese Freigaben nicht als den Zugangsweg der Verordnung dar. --- # Herkunft und Freigabe > Jeder Wert behält Quelle, Urheber, Zeitpunkt und Nachweise und wird abgelöst statt überschrieben; deklarierte Daten werden geprüft, bevor sie in einen Pass gelangen, von einer zweiten Person, wenn die Organisation das Vier-Augen-Prinzip nutzt. Ein Pass ist nur so vertrauenswürdig wie seine Werte. BatteryPass hält deshalb fest, woher jeder Wert stammt, wer ihn erfasst und freigegeben hat und welche Dokumente ihn belegen, und es bewahrt jeden früheren Stand auf. ## Ein Wert ist eine Revision Jeder Wert wird als Revision gespeichert (`FieldValue`). Die API liefert diese Felder: | Feld | Bedeutung | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- | | `value`, `unit` | Der Wert als JSON und seine Maßeinheit; `value` ist `null`, wenn das Feld geleert wurde | | `revision` | 1, 2, 3 … je Feld und Ziel (Modellversion oder Einheit) | | `source` | Woher der Wert stammt, siehe unten | | `recorded_by`, `recorded_at` | Wer diese Revision erfasst hat und wann | | `responsible` | Das für den Wert verantwortliche Mitglied | | `review_status`, `reviewed_by`, `reviewed_at`, `review_note` | Die Prüfentscheidung | | `evidence[]` | Die Dokumente, auf denen diese Revision beruht (Art, Titel, Zugriffsstufe, Revision) | | `checks[]` | Ergebnisse der automatischen Prüfungen: `pass`, `warn` oder `fail` mit einer Meldung | | `measured_at` | Bei Zustandsdaten: wann der Wert gemessen wurde | | `superseded_at` | Wann eine neuere Revision diese abgelöst hat | | `registry_version` | Die Registerversion, unter der der Wert erfasst wurde | | `source` | Bedeutung | | ---------- | ----------------------------------------------------------------------------------------------- | | `manual` | Ein Mitglied hat ihn im Portal eingegeben | | `import` | Er stammt aus einem Tabellenimport | | `supplier` | Ein Lieferant hat ihn über einen Anfragelink gesendet, oder ein Servicepartner hat ihn gemeldet | | `api` | Ein API-Schlüssel hat ihn gemeldet (nur Zustandsdaten) | | `system` | BatteryPass hat ihn geschrieben | ## Werte werden abgelöst, nie überschrieben Eine Änderung bearbeitet nie einen gespeicherten Wert. Sie fügt eine neue Revision ein und markiert die vorherige als abgelöst; so bleibt die ganze Historie erhalten, einschließlich wer was wann geändert hat. Auch eine Prüfentscheidung ist eine Revision: Die Freigabe fügt eine Revision mit demselben Wert und dem Prüfer ein. | Operation | Liefert | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | [`listModelVersionValues`](https://passport.kabin.sh/docs/de/api-reference/values/list-model-version-values), [`listBatteryValues`](https://passport.kabin.sh/docs/de/api-reference/values/list-battery-values) | Die aktuelle Revision jedes Felds einer Modellversion oder Einheit | | [`listModelVersionValueHistory`](https://passport.kabin.sh/docs/de/api-reference/values/list-model-version-value-history), [`listBatteryValueHistory`](https://passport.kabin.sh/docs/de/api-reference/values/list-battery-value-history) | Alle Revisionen eines Felds, die neueste zuerst | | [`listEvidence`](https://passport.kabin.sh/docs/de/api-reference/evidence/list-evidence), [`getEvidence`](https://passport.kabin.sh/docs/de/api-reference/evidence/get-evidence), [`downloadEvidence`](https://passport.kabin.sh/docs/de/api-reference/evidence/download-evidence) | Die Dokumente, ihre Revisionen, wo sie verwendet werden, und die Dateien | ```bash title="cURL" curl -s "$BATTERYPASS_URL/api/v1/model-versions/$VERSION_ID/values/performance.nominal_voltage/history" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ | jq '.items[] | {revision, value, source, review_status, reviewed_at}' ``` Dokumente sind auf dieselbe Weise versioniert: Eine neue Revision eines Dokuments löst die alte ab, und ein Wert, der auf einem abgelösten Dokument beruht, wird durch eine fehlgeschlagene Prüfung markiert. ## Prüfstatus | `review_status` | Bedeutung | | --------------- | --------------------------------------------------------------------------------------- | | `captured` | Erfasst; eine automatische Prüfung ist fehlgeschlagen oder noch nicht bestanden | | `auto_checked` | Die automatischen Prüfungen sind bestanden; wartet auf einen Prüfer | | `approved` | Von einem Prüfer freigegeben; nur freigegebene Werte gelangen in eine Passversion | | `rejected` | Von einem Prüfer mit einer Notiz abgelehnt; das verantwortliche Mitglied korrigiert ihn | Die Freigabe erteilt ein Prüfer (Rolle `reviewer`, `admin` oder `owner`) im Portal. Sie wird verweigert, solange - eine Prüfung des Werts fehlschlägt, - das Register für das Feld einen Nachweis verlangt und kein aktuelles Dokument den Wert belegt, oder - der Prüfer den Wert selbst erfasst hat und in der Organisation das Vier-Augen-Prinzip gilt (Standard). Die Werte eines Tabellenimports lassen sich in einem Schritt je Import freigeben, für jeden Wert nach denselben Regeln. Werte, die eine dieser Regeln zurückhält, bleiben für die Einzelprüfung offen. > [!NOTE] > Zustandsdaten sind die Ausnahme: Als Messwerte gemeldete Werte werden ohne Prüfer angenommen und als `approved` > mit ihrem `measured_at` gespeichert. Siehe [Zustandsdaten und Lebenszyklus](https://passport.kabin.sh/docs/de/concepts/condition-and-lifecycle). ## Warum API-Schlüssel nicht freigeben oder veröffentlichen Ein API-Schlüssel gehört der Organisation, nicht einer Person. Das Vier-Augen-Prinzip vergleicht den Urheber eines Werts mit seinem Prüfer, und eine Veröffentlichung braucht einen Prüfer, der dafür einsteht. Ein Schlüssel, hinter dem niemand steht, könnte an beidem nicht teilnehmen, ohne es zu schwächen. Deshalb schreiben Schlüssel nie deklarierte Daten, geben keine Werte frei und veröffentlichen oder ziehen keine Pässe zurück. Sie lesen, und mit dem Scope `condition` melden sie Messwerte und Lebenszyklus-Ereignisse, die `source` `api` tragen; das Lebenszyklus-Ereignis jeder Meldung nennt den Schlüssel in `api_key`. Deklarierte Daten kommen über das Portal, Lieferantenanfragen oder den Tabellenimport zu BatteryPass und werden von Mitgliedern freigegeben. Mit dem Scope `import` stellt ein Schlüssel einen Tabellenimport bereit: hochladen, zuordnen, prüfen. Übernehmen und die importierten Werte freigeben können nur Mitglieder im Portal, sodass auch Daten aus einem ERP erst durch die Entscheidung eines Mitglieds zu deklarierten Daten werden (ADR-026). Siehe [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production). ## Vollständigkeit [`getModelVersionCompleteness`](https://passport.kabin.sh/docs/de/api-reference/values/get-model-version-completeness) und [`getBatteryCompleteness`](https://passport.kabin.sh/docs/de/api-reference/values/get-battery-completeness) werten das Register für eine Modellversion oder Einheit aus und geben jedem Feld einen Status `display`: | `display` | Bedeutung | | ------------------------- | -------------------------------------------------------------------------------- | | `required_missing` | Jetzt Pflicht, kein aktueller freigegebener Wert; blockiert die Veröffentlichung | | `required_pending_review` | Ein Wert existiert, ist aber nicht freigegeben; blockiert die Veröffentlichung | | `ok` | Ein freigegebener Wert liegt vor | | `not_applicable` | Als nicht zutreffend markiert, mit gespeicherter Begründung | | `later_required` | Die Pflicht beginnt später oder ist noch nicht ausgelöst | | `optional_missing` | Optional und leer | Einen Prozentwert gibt es bewusst nicht. `summary` zählt die Status, und `summary.blocking` ist die Zahl der Felder, die die Veröffentlichung noch blockieren. --- # Zustandsdaten und Lebenszyklus-Ereignisse > Wie gemessene Zustandsdaten ohne Prüfschritt in einen Pass gelangen, wie die Neuveröffentlichung geregelt ist und was Lebenszyklus-Ereignisse festhalten. Der Pass einer Batterie endet nicht am Werkstor. Zwei Arten von Informationen begleiten sie durch ihr Leben: **Zustandsdaten**, also der gemessene Gesundheitszustand und die Nutzung einer Einheit, und **Lebenszyklus-Ereignisse**, die festhalten, was mit ihr geschehen ist. Beides lässt sich über die API mit einem Schlüssel melden, der den Scope `condition` hat. ## Zustandsdaten Zustandsdaten sind die dynamischen Felder des Registers (der Abschnitt `condition`, Anhang XIII Nummer 4): Messwerte einer Einheit wie `condition.capacity_fade`, `condition.remaining_capacity`, `condition.number_of_full_cycles` oder, bei Elektrofahrzeugbatterien, `condition.state_of_certified_energy`. Welche Felder gelten, hängt von der Batteriekategorie ab; [`getCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/get-condition) nennt sie in `field_keys`, mit ihren aktuellen Werten und `measured_at`. Im aktuellen Register gehören alle Zustandsfelder zur Stufe `legitimate_interest`; im öffentlichen JSON erscheinen sie daher nie ([Zugriffsstufen](https://passport.kabin.sh/docs/de/concepts/access-tiers)). ### Messwerte statt deklarierter Angaben Was ein Batteriemanagementsystem gemessen hat, kann ein Prüfer nicht besser beurteilen als das Gerät selbst. Eine Zustandsmeldung wird deshalb als Messung angenommen, ohne den Prüfschritt, den deklarierte Daten brauchen: - Jeder Wert wird direkt als `approved` gespeichert, mit seinem `measured_at`, seiner `source` (`api` für einen Schlüssel, `supplier` für den Bericht eines Servicepartners, `manual` für ein Mitglied). Ein Mitglied steht in `recorded_by`, ein Schlüssel im `api_key` des Lebenszyklus-Ereignisses der Meldung (Werte nennen keinen Schlüssel). - `reviewed_at` ist der Zeitpunkt der Annahme; einen Prüfer gibt es nicht, das Vier-Augen-Prinzip gilt hier also nicht. - Jede angenommene Meldung erzeugt ein Lebenszyklus-Ereignis `condition_update` und einen Audit-Eintrag. Zustandswerte, die ein Mitglied im Editor der Einheit eingibt oder per Tabelle importiert, durchlaufen weiterhin die normale Prüfung ([Herkunft und Freigabe](https://passport.kabin.sh/docs/de/concepts/provenance-and-review)). ### Eine Meldung senden [`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition) führt die gesendeten Werte mit den Zustandsdaten der Einheit zusammen: ```bash title="cURL" curl -s -X PATCH "$BATTERYPASS_URL/api/v1/batteries/$BATTERY_ID/condition" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "measured_at": "2026-10-24T06:00:00Z", "note": "Wöchentliche BMS-Auslesung", "values": [ { "field_key": "condition.capacity_fade", "value": 3.2 }, { "field_key": "condition.number_of_full_cycles", "value": 412 } ] }' ``` | Regel | Wirkung | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Zusammenführen | Nur die genannten Felder ändern sich; `null` leert ein Feld. Höchstens 50 Werte je Anfrage. | | `measured_at` | Jeder Wert hat sein eigenes oder das der Anfrage. Es darf nicht in der Zukunft und nicht vor dem Herstellungsdatum der Einheit liegen. | | Ältere oder gleiche Messung | Ein Wert, der vor dem gespeicherten gemessen wurde, oder zur selben Zeit mit demselben Wert, wird übersprungen und in `skipped[]` genannt (`older_measurement`, `unchanged`). | | Alles oder nichts | Ein Feld, das kein dynamisches Feld der Einheit ist (`unknown_field`), ein ungültiger Wert oder zusammengeführte Daten, die eine feldübergreifende Prüfung nicht bestehen, lehnen die ganze Meldung ab. | Die Antwort nennt die angenommenen Werte (`accepted`), die übersprungenen (`skipped`), das erzeugte Ereignis (`event`) und das Ergebnis der Veröffentlichung (`publication`). ### Regel für die Neuveröffentlichung Ist der Pass der Einheit veröffentlicht, müssen angenommene Zustandsdaten in eine neue Passversion gelangen. Wann das geschieht, legt die Organisation in ihren Einstellungen im Portal fest (`condition_republish`, von `getCondition` als `republish` geliefert): | Regel | Verhalten | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `immediate` (Standard) | Dieselbe Anfrage veröffentlicht die nächste Version, mit der Begründung `condition update` und ohne Mitglied als Veröffentlichenden. | | `daily` | Ein täglicher Job um 02:00 UTC veröffentlicht jeden veröffentlichten Pass, dessen Einheit nach der aktuellen Version Zustandsdaten erhalten hat; die Meldungen eines Tages ergeben so eine Version. | `publication.outcome` in der Antwort sagt, was geschehen ist: | `outcome` | Bedeutung | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `published` | In dieser Anfrage wurde eine neue Version veröffentlicht. | | `unchanged` | Am Pass hat sich nichts geändert, es kam keine Version hinzu. | | `scheduled` | Der tägliche Job veröffentlicht sie. | | `blocked` | Die Veröffentlichungsprüfung hat abgelehnt (siehe `blockers`); die Meldung bleibt angenommen, und der Pass wird `update_needed`, bleibt aber online. | | `not_published` | Die Einheit hat noch keinen veröffentlichten Pass. | ## Lebenszyklus-Ereignisse Ein Lebenszyklus-Ereignis hält fest, was an einem bestimmten Tag mit einer Einheit geschehen ist. Sie melden es mit [`createLifecycleEvent`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/create-lifecycle-event) und lesen die Historie, das neueste zuerst, mit [`listLifecycleEvents`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events). | Meldbarer Typ (`ReportedEventType`) | Typische Verwendung | | ----------------------------------- | ------------------------------------------------------------------------- | | `placed_on_market` | Die Batterie wurde in Verkehr gebracht | | `put_into_service` | Die Batterie wurde in Betrieb genommen | | `inspection` | Eine Inspektion oder Diagnose | | `repair` | Eine Reparatur | | `component_replaced` | Ein Modul, eine Zelle oder ein anderes Bauteil wurde getauscht | | `condition_update` | Eine Zustandsauslesung, die als eigenes Ereignis festgehalten werden soll | | `waste` | Die Batterie wurde zu Abfall | | `recycled` | Die Batterie wurde recycelt | | `custom` | Alles andere, beschrieben in `description` | Zwei weitere Arten zeichnet BatteryPass selbst auf: `condition_update` bei jeder angenommenen Zustandsmeldung und `repurposed`, `reused` oder `remanufactured`, wenn ein Mitglied im Portal eine Second-Life-Batterie anlegt. Ein Ereignis hat `occurred_on` (ein Datum zwischen Herstellungsdatum und heute), optional eine `description` (bis zu 2000 Zeichen) und bis zu 20 `evidence_ids`, also Dokumente, die bereits in der Nachweisbibliothek liegen, etwa ein Reparaturprotokoll. Ereignisse, die ein Schlüssel meldet, tragen `source` `api` und nennen den Schlüssel in `api_key`. > [!WARNING] > Ereignisse sind Aufzeichnungen: Sie ändern keine Daten der Einheit. Ein Ereignis `put_into_service` setzt nicht > das Datum `putting_into_service` der Einheit, und `waste` oder `recycled` ändert weder ihren Batteriestatus noch > beendet es ihren Pass. Messwerte melden Sie mit `updateCondition`; Daten der Einheit ändern Mitglieder im Portal. > Ereignisse lassen sich nachträglich weder bearbeiten noch löschen. ## Nächste Schritte [**Zustandsdaten senden**](https://passport.kabin.sh/docs/de/guides/push-condition-data): Messwerte aus einem Batteriemanagementsystem melden. [**Lebenszyklus-Ereignisse erfassen**](https://passport.kabin.sh/docs/de/guides/record-lifecycle-events): Reparaturen, Inspektionen und andere Ereignisse melden. --- # GS1 Digital Link und Resolver > Der Link in jedem QR-Code und wie der Resolver Browser zur öffentlichen Seite und Maschinen zum öffentlichen JSON leitet. Jeder Pass hat eine Webadresse, die die Batterie kennzeichnet: einen GS1 Digital Link. Genau diesen Link trägt der QR-Code auf der Batterie. Dieselbe Adresse bedient Menschen und Maschinen: Der Resolver dahinter entscheidet anhand der Anfrage, was er zurückgibt. ## Der Link ```text https://batterypass.example.com/01/04012345000023/21/SV2027-000184 ``` | Teil | Bedeutung | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `https://batterypass.example.com` | Der Ursprung des Resolvers: Ihre BatteryPass-Adresse oder ein eigener Resolver-Hostname, den Ihr Betreiber eingerichtet hat | | `/01/04012345000023` | GS1-Datenbezeichner `01`: die GTIN des Modells, immer 14-stellig | | `/21/SV2027-000184` | GS1-Datenbezeichner `21`: die Seriennummer der Einheit; ein `/` in der Seriennummer wird prozentkodiert (`%2F`) übertragen | Der Link steht in `gs1_digital_link` von [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport) und ist fest, sobald die Kennungen gesperrt sind. [`getPassportQr`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-qr) erzeugt aus genau diesem Link ein PNG oder SVG (Fehlerkorrekturstufe M, Ruhezone von vier Modulen). Übernehmen Sie den Link immer aus der API, statt ihn selbst zusammenzusetzen. ## Was der Resolver liefert Der Resolver liegt an der Wurzel des Ursprungs (`/01/…/21/…`), nicht unter `/api/v1`, und braucht keinen Schlüssel. | Anfrage | Antwort | | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | Ein Browser (`Accept: text/html` oder ohne Vorliebe für JSON) | `307` zur öffentlichen Passseite `/pass/{passportId}` | | `?linkType=describedby` (oder `gs1:describedby`) | `307` zum öffentlichen JSON `/api/v1/public/passports/{passportId}` | | `?linkType=all` oder `Accept: application/json`, `application/linkset+json` bzw. `application/ld+json` ohne `linkType` | `200` mit einem Linkset (`application/linkset+json`) | | Unbekannte GTIN und Seriennummer | `404`: eine kurze Seite für Browser, ein Problem `not_found` für JSON-Clients | | Zu viele Anfragen von einer Adresse | `429 rate_limited` mit `Retry-After` | Jede Antwort zu einem bekannten Pass trägt die Links zusätzlich im Header `Link`, mit den Relationen `https://gs1.org/voc/pip` (die Seite), `describedby` (das öffentliche JSON) und `linkset`. Antworten dürfen fünf Minuten zwischengespeichert werden und unterscheiden sich nach `Accept`. Ein Pass, dessen Kennungen gesperrt, der aber noch nicht veröffentlicht ist, oder einer, der zurückgezogen wurde, wird trotzdem aufgelöst: Seine Seite erklärt den Zustand, und das öffentliche JSON liefert den Status ohne Inhalt. ## Den Link aus Code verfolgen ```bash title="cURL" # Das öffentliche JSON des Passes hinter einem QR-Code curl -sL "https://batterypass.example.com/01/04012345000023/21/SV2027-000184?linkType=describedby" # Das Linkset curl -s -H "Accept: application/linkset+json" \ "https://batterypass.example.com/01/04012345000023/21/SV2027-000184" ``` Das Linkset nennt die Seite und das öffentliche JSON: ```json title="Linkset" { "linkset": [ { "anchor": "https://batterypass.example.com/01/04012345000023/21/SV2027-000184", "https://gs1.org/voc/pip": [ { "href": "https://batterypass.example.com/pass/0192f0c4-7b1e-7c3a-9d2e-5a8b6c4d3e21", "type": "text/html", "title": "Battery passport" } ], "describedby": [ { "href": "https://batterypass.example.com/api/v1/public/passports/0192f0c4-7b1e-7c3a-9d2e-5a8b6c4d3e21", "type": "application/json", "title": "Public passport (JSON)" } ] } ] } ``` Kennen Sie GTIN und Seriennummer bereits, können Sie den Resolver überspringen und [`getPublicPassportByProductId`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-by-product-id) mit `{gtin}:{serial}` aufrufen, zum Beispiel `04012345000023:SV2027-000184`. Die GTIN darf dort 8, 12, 13 oder 14 Stellen haben. > [!NOTE] > Resolver, öffentliche Passabfragen und öffentliche Dokumente teilen sich ein Limit von 300 Anfragen pro Minute und > Client-Adresse. Eine Scanner-App, die viele Codes auflöst, sollte die Cache-Header beachten; öffentliche > Passabfragen dürfen fünf Minuten zwischengespeichert werden. ## Verwandte Seiten [**Digital Link auflösen**](https://passport.kabin.sh/docs/de/guides/resolve-digital-link): Aus einem gescannten Code die öffentlichen Passdaten gewinnen. [**QR-Etiketten für eine Charge**](https://passport.kabin.sh/docs/de/recipes/qr-labels-for-a-batch): Die QR-Codes für die Einheiten einer Charge abrufen. --- # Signaturen > Was BatteryPass in jeder Passversion signiert, wie Hash und Ed25519-Signatur entstehen und wie Sie sie mit den veröffentlichten Schlüsseln prüfen. Jede veröffentlichte Passversion ist signiert. So kann jeder prüfen, dass die Daten von Ihrer BatteryPass-Installation stammen und danach nicht verändert wurden. Für die Prüfung braucht es weder ein Konto noch einen Aufruf bei BatteryPass: Das öffentliche JSON und die veröffentlichten Schlüssel genügen. ## Was signiert wird Bei der Veröffentlichung signiert BatteryPass zwei Dokumente jeder Version mit dem aktiven Schlüssel, in derselben Transaktion, die sie speichert: | Dokument | Enthält | Hash und Signatur | Wo Sie es lesen | | --------------------- | --------------------------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Öffentliches Dokument | Die Projektion auf die Stufe `public`, mit Bezeichnungen | `public_hash`, `public_signature` | `document` und `verification` des öffentlichen JSON ([`getPublicPassport`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport)); `public_document` von [`getPassportVersion`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-version) | | Snapshot | Jedes Feld in jeder Zugriffsstufe, mit Herkunft und Dokumentverweisen | `canonical_hash`, `signature` | `snapshot` von [`getPassportVersion`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-version), für Ihre Organisation | Das öffentliche Dokument sieht jeder Scanner, also kann es jeder prüfen. Der Snapshot belegt den vollständigen Datensatz gegenüber Ihnen und gegenüber Behörden, denen Sie ihn vorlegen. Im öffentlichen JSON ist nur `document` signiert; die Geschwister `passport_id`, `status`, `valid_until`, `verification` und `links` ändern sich im Leben eines Passes und sind nicht abgedeckt. ```json title="verification" { "algorithm": "Ed25519", "canonicalization": "RFC8785", "hash": "sha256:22471733ece6d811d244144731fc505180c5e7760a7a016c1322338bce0f56e5", "signature": "w0nnz3kjhrEGb1DtMoGp9qlH9g8q0h9YDJRb2jfabq7AOzPMUpnWXoxmIlptgJPTDVy3LWxLQkOpQYuGYXgJ4w", "key_id": "2027-10-k2", "signed_at": "2026-10-24T09:15:02Z", "keys_url": "https://batterypass.example.com/.well-known/batterypass/keys.json" } ``` ## Wie eine Signatur entsteht und geprüft wird 1. **Kanonisieren** Serialisieren Sie das signierte Objekt (`document` oder `snapshot`) mit dem JSON Canonicalization Scheme, JCS (RFC 8785). JCS sortiert die Objektfelder, entfernt Leerraum und schreibt Zahlen und Zeichenketten in einer festgelegten Form; dieselben Daten ergeben so immer dieselben Bytes. Kanonisieren Sie das geparste Objekt, nicht die Bytes der Antwort. 2. **Hashen** Berechnen Sie SHA-256 über die kanonischen Bytes und schreiben Sie ihn als `sha256:` gefolgt vom Hex-Digest in Kleinbuchstaben. Er muss `verification.hash` entsprechen (bzw. `public_hash`, `canonical_hash`). 3. **Schlüssel finden** Laden Sie das JWK-Set von `/.well-known/batterypass/keys.json` der BatteryPass-Adresse, der Sie vertrauen, und nehmen Sie den Schlüssel, dessen `kid` gleich `verification.key_id` ist. `keys_url` ist nicht signiert: zeigt es auf eine andere Adresse, gilt die Prüfung als fehlgeschlagen. 4. **Prüfen** Dekodieren Sie `x` des Schlüssels und die Signatur aus base64url ohne Padding (32 und 64 Bytes) und prüfen Sie die Ed25519-Signatur über die kanonischen Bytes, nicht über den Hash. Die [Anleitung zur Signaturprüfung](https://passport.kabin.sh/docs/de/guides/verify-signature) zeigt das Schritt für Schritt mit vollständigem Code. ## Die veröffentlichten Schlüssel Die öffentlichen Schlüssel liegen als JWK-Set unter `/.well-known/batterypass/keys.json` Ihrer BatteryPass-Adresse, außerhalb von `/api/v1`. Das Set braucht keinen Schlüssel, ist von jedem Ursprung aus lesbar (CORS) und darf eine Stunde zwischengespeichert werden. ```json title="keys.json" { "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "WuoaQRoQuh2kPs5yYB6HxNz3Y58ndE8fRHnzG10NxoU", "kid": "2027-10-k2", "use": "sig", "alg": "EdDSA" }, { "kty": "OKP", "crv": "Ed25519", "x": "JEpD6r2NnmNrL4_7UWS9wIWLFyjCfaSClypHm110Vl4", "kid": "2026-10-k1", "use": "sig", "alg": "EdDSA" } ] } ``` Der aktive Schlüssel steht an erster Stelle. Schlüssel wechselt der Betreiber Ihrer Installation: Ein neuer Schlüssel signiert neue Versionen, die alten öffentlichen Schlüssel bleiben im Set, damit frühere Signaturen weiter prüfbar sind. Ein Schlüssel, der eine veröffentlichte Version signiert hat, wird nie entfernt. Zwischenspeichern Sie Schlüssel nach `kid` und laden Sie das Set erneut, wenn Ihnen eine unbekannte `kid` begegnet. ## Prüfung auf dem Server oder offline | | [`verifyPassport`](https://passport.kabin.sh/docs/de/api-reference/public/verify-passport) | Offline-Prüfung | | ---------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Was geprüft wird | Beide Signaturen und beide Hashes einer Version gegen die gespeicherten Dokumente | Das öffentliche Dokument, das Ihnen vorliegt, gegen den veröffentlichten Schlüssel | | Ergebnis | `valid`, `public_document_valid`, `snapshot_valid`, `key_id`, `version_no`, `signed_at`, `checked_at` | Ihr eigenes Ja oder Nein | | Vertrauen | Sie vertrauen der Antwort des Servers | Sie vertrauen nur dem Schlüssel, den Sie fest hinterlegen können | | Braucht | Die UUID des Passes; `version_no` optional (Standard: aktuelle Version) | Das öffentliche JSON und das JWK-Set | `verifyPassport` ist eine Abkürzung für eine schnelle Prüfung. Für einen unabhängigen Nachweis, etwa in einem Archiv oder im System eines Kunden, prüfen Sie offline. > [!NOTE] > Eine gültige Signatur zeigt, dass diese BatteryPass-Installation genau diese Daten in dieser Version > veröffentlicht hat. Sie sagt nichts darüber, ob die Daten richtig oder vollständig sind, und sie ist kein > qualifiziertes elektronisches Siegel nach eIDAS. --- # Sicherheit und Datenschutz > Wie BatteryPass den API-Zugang und die Passdaten schützt und was Ihre Integration tun und vermeiden sollte. Diese Seite beschreibt die technischen Maßnahmen, die für Integrationen gelten, und wofür Sie auf Ihrer Seite verantwortlich sind. Sie ist eine Beschreibung, keine Zertifizierung und keine Prüfbestätigung. ## Transport Produktive BatteryPass-Installationen laufen über HTTPS; die Referenz-Bereitstellung beendet TLS an ihrem Reverse Proxy und sendet `Strict-Transport-Security`. Senden Sie API-Schlüssel nur per HTTPS an Ihre BatteryPass-Adresse, und schalten Sie die Zertifikatsprüfung in Ihrem HTTP-Client nicht ab. ## API-Schlüssel | Maßnahme | Detail | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Geheimnis | `bp_live_` gefolgt von 43 base64url-Zeichen (32 Zufallsbytes) | | Speicherung | BatteryPass speichert nur den SHA-256 des Schlüssels. Der Schlüssel wird einmal angezeigt, wenn ein Inhaber ihn im Portal anlegt; danach lässt er sich nicht mehr anzeigen. | | Wiedererkennung | Die ersten 12 Zeichen (`bp_live_` und vier weitere) bleiben als Präfix erhalten, damit Inhaber Schlüssel im Portal unterscheiden können. | | Nur im Header | Schlüssel werden nur in `Authorization: Bearer` angenommen, nie in einer URL. Eine Anfrage mit Sitzungs-Cookie und Schlüssel zugleich wird mit `401` abgelehnt. | | Laufzeit | Schlüssel laufen nicht ab. Nur ein Widerruf stoppt einen Schlüssel; ein widerrufener Schlüssel funktioniert nie wieder. Zum Wechseln legen Sie einen neuen Schlüssel an und widerrufen den alten. | | Anzahl | Höchstens 25 aktive Schlüssel je Organisation. | Inhaber legen Schlüssel im Portal an, widerrufen sie dort und sehen, wann jeder Schlüssel zuletzt benutzt wurde. Siehe [Authentifizierung](https://passport.kabin.sh/docs/de/authentication) und [Schlüssel sicher verwalten](https://passport.kabin.sh/docs/de/guides/secure-key-handling). ## Was ein Schlüssel darf - **Scopes.** Ein Schlüssel hat einen oder mehrere der Scopes `read`, `condition` und `import`. Jede Operation nennt ihren Scope in `x-api-key-scope`; ein Schlüssel ohne diesen Scope erhält `403 forbidden`. Geben Sie jedem System nur den Scope, den es braucht. - **Rechte eines Betrachters.** Ein Schlüssel handelt in seiner Organisation mit den Rechten eines Betrachters. Er ändert nie deklarierte Daten, gibt keine Werte frei, veröffentlicht oder zieht keine Pässe zurück und verwaltet weder Mitglieder noch Schlüssel noch Einstellungen. - **Trennung der Organisationen.** Jede Anfrage ist auf die Organisation des Schlüssels beschränkt. Ids von Ressourcen einer anderen Organisation verhalten sich, als gäbe es sie nicht (`404 not_found`). ## Ratenlimits | Limit | Gilt für | | -------------------------- | ----------------------------------------------------------------------------------- | | 600 Anfragen pro Minute | Alle Schlüssel einer Organisation zusammen | | 30 Versuche pro 15 Minuten | Unbekannte oder widerrufene Schlüssel, je Client-Adresse | | 300 Anfragen pro Minute | Öffentliche Passabfragen, öffentliche Dokumente und den Resolver, je Client-Adresse | Eine begrenzte Anfrage erhält `429 rate_limited` mit dem Header `Retry-After` in Sekunden ([Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits)). ## Audit Jeder Schreibvorgang mit einem Schlüssel (eine Zustandsmeldung oder ein Lebenszyklus-Ereignis) wird im Audit-Log der Organisation mit dem Akteur `api_key` und der Id des Schlüssels festgehalten, in derselben Transaktion wie die Änderung. Die entstehenden Werte und Ereignisse tragen `source` `api`; das Lebenszyklus-Ereignis jeder Meldung nennt den Schlüssel (`api_key`). Anlegen und Widerrufen von Schlüsseln wird mit dem handelnden Mitglied protokolliert. Lesezugriffe mit einem Schlüssel werden nicht protokolliert. ## Passdaten - Öffentliche Operationen und der Resolver liefern nur die gespeicherte Projektion `public` einer Version ([Zugriffsstufen](https://passport.kabin.sh/docs/de/concepts/access-tiers)). Eingeschränkte Stufen erscheinen nie in einer öffentlichen Antwort, und eingeschränkte Dokumente werden dort nie verlinkt. - Das Lesen eingeschränkter Stufen durch Dritte ist eine eigene Portal-Funktion hinter einem standardmäßig ausgeschalteten Schalter; jeder solche Zugriff wird protokolliert. - Jede veröffentlichte Version ist signiert, nachträgliche Änderungen sind also erkennbar ([Signaturen](https://passport.kabin.sh/docs/de/concepts/signatures)). - Fehler sind Problem Details mit stabilem `code` und einer `request_id`; sie enthalten kein SQL, keine Stacktraces und keine Fehlertexte von Drittsystemen ([Fehler](https://passport.kabin.sh/docs/de/errors)). ## Datenschutz für Integrationen In BatteryPass geht es um Batterien, nicht um Menschen, und Ihre Integration sollte das beibehalten. > [!WARNING] > Senden Sie keine personenbezogenen Daten in Freitextfeldern, also in `note` einer Zustandsmeldung und in > `description` eines Lebenszyklus-Ereignisses. Schreiben Sie „Modul 3 getauscht“ statt Name, Adresse, Telefonnummer oder Kennzeichen > des Technikers oder Endkunden. Ereignisse und Messwerte lassen sich über die API weder bearbeiten noch löschen, > jedes Mitglied Ihrer Organisation kann sie lesen, und sie sind Teil des vollständigen Exports der Organisation. Auch was Sie lesen, kann personenbezogene Daten enthalten: Die Herkunft nennt Mitglieder (`recorded_by`, `reviewed_by`, `responsible`, `published_by_name`), und ein Ereignis, das ein Servicepartner gemeldet hat, kann dessen E-Mail-Adresse tragen. Übernimmt Ihre Integration solche Daten in ein anderes System, braucht dieses System dafür eine Rechtsgrundlage und eine Löschregel. Übernehmen Sie nur, was Sie brauchen. ## Diese Dokumentation Die Seiten dieser Dokumentation laden keine Analyse-, Werbe- oder Tracking-Skripte Dritter. Die Menüeinträge, die eine Seite in einem KI-Assistenten öffnen, senden die Adresse der Seite erst an diesen Dienst, wenn Sie sie anklicken. ## Ein Sicherheitsproblem melden Vermuten Sie, dass ein Schlüssel offengelegt wurde, bitten Sie einen Inhaber Ihrer Organisation, ihn sofort zu widerrufen und einen neuen anzulegen. Eine vermutete Schwachstelle melden Sie direkt dem Betreiber Ihrer BatteryPass-Installation, nicht in einem gemeinsamen Kanal ([Support](https://passport.kabin.sh/docs/de/general/support)). --- # Support > Wie Sie Hilfe zu einer Integration bekommen, was in eine Anfrage gehört und wo der Stand der Funktionen dokumentiert ist. ## Wen Sie ansprechen | Frage | Ansprechpartner | | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | API-Schlüssel, Scopes, Einstellungen der Organisation wie die Regel für die Neuveröffentlichung | Ein Inhaber Ihrer Organisation, im Portal | | Daten in einem Pass, Prüfung, Veröffentlichung | Die für die Daten verantwortlichen Mitglieder Ihrer Organisation | | Unerklärliche Fehler, Ausfälle, vermutete Sicherheitsprobleme | Ihr Ansprechpartner für BatteryPass oder der Betreiber Ihrer BatteryPass-Installation | Supportwege und Reaktionszeiten vereinbaren Sie mit dem Betreiber Ihrer Installation; diese Dokumentation legt sie nicht fest. ## Vorher prüfen - `code`, `detail` und `errors[]` des Problems nennen meist die Ursache; [Fehler](https://passport.kabin.sh/docs/de/errors) erklärt jeden Code, und [Fehler behandeln](https://passport.kabin.sh/docs/de/guides/handle-errors) zeigt, wie Sie darauf reagieren. - `429 rate_limited`: Warten Sie die Sekunden aus `Retry-After` ab ([Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits)). - `403 forbidden` mit einem Schlüssel: Dem Schlüssel fehlt der Scope der Operation, oder die Operation verlangt mehr als die Rechte eines Betrachters ([Authentifizierung](https://passport.kabin.sh/docs/de/authentication)). ## Die Request-Id angeben Jede Antwort, ob erfolgreich oder nicht, trägt den Header `X-Request-Id`. Fehlerantworten wiederholen ihn als `request_id` im Problem. Mit dieser Id findet der Betreiber Ihre Anfrage in den Logs. Eine `X-Request-Id`, die Sie selbst senden, ignoriert BatteryPass und vergibt immer eine eigene; protokollieren Sie also die aus der Antwort. ```bash title="cURL" curl -s -D - -o /dev/null "$BATTERYPASS_URL/api/v1/battery-models?limit=1" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" | grep -i '^x-request-id' ``` ```ts title="TypeScript" const base = process.env.BATTERYPASS_URL; const key = process.env.BATTERYPASS_API_KEY; const res = await fetch(`${base}/api/v1/battery-models?limit=1`, { headers: { Authorization: `Bearer ${key}` }, }); const requestId = res.headers.get('X-Request-Id'); if (res.ok) { console.log(`ok request_id=${requestId}`); } else { const problem = await res.json(); console.error(`${res.status} ${problem.code} request_id=${problem.request_id ?? requestId}`); } ``` ```python title="Python" import os import requests base = os.environ["BATTERYPASS_URL"] key = os.environ["BATTERYPASS_API_KEY"] res = requests.get( f"{base}/api/v1/battery-models", params={"limit": 1}, headers={"Authorization": f"Bearer {key}"}, timeout=30, ) request_id = res.headers.get("X-Request-Id") if res.ok: print(f"ok request_id={request_id}") else: problem = res.json() print(f"{res.status_code} {problem.get('code')} request_id={problem.get('request_id', request_id)}") ``` ```go title="Go" package main import ( "encoding/json" "fmt" "log" "net/http" "os" ) func main() { req, err := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1/battery-models?limit=1", nil) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() requestID := res.Header.Get("X-Request-Id") if res.StatusCode >= 400 { var problem struct { Code string `json:"code"` } _ = json.NewDecoder(res.Body).Decode(&problem) log.Printf("%d %s request_id=%s", res.StatusCode, problem.Code, requestID) return } fmt.Println("ok request_id=" + requestID) } ``` ## Was in die Anfrage gehört 1. **Den Aufruf benennen** Die `request_id`, die Uhrzeit in UTC, die Operation (etwa `updateCondition`, oder Methode und Pfad) und den HTTP-Status mit `code`, `detail` und `errors[]` des Problems. 2. **Schlüssel und Daten benennen** Das Präfix des Schlüssels (seine ersten 12 Zeichen, wie das Portal sie zeigt) und die beteiligten Ids: Batterie-Id, UUID des Passes, Versionsnummer. 3. **Den Fall beschreiben** Was Sie erwartet haben, was passiert ist und ob es jedes Mal oder nur manchmal auftritt. > [!WARNING] > Senden Sie nie den vollständigen API-Schlüssel, und lassen Sie personenbezogene Daten sowie Exporte mit > eingeschränkten Stufen weg (BatteryPass JSON und PDF einer Passversion). Request-Id und Ids genügen, um die Anfrage > zu finden. ## Stand der Funktionen - Das [Changelog](https://passport.kabin.sh/docs/de/changelog) nennt, was sich an der API und an dieser Dokumentation geändert hat, auch als [RSS-Feed](https://passport.kabin.sh/docs/de/changelog/rss.xml). - Der veröffentlichte Vertrag nennt seine Version in `info.version`; Sie laden ihn als [JSON](https://passport.kabin.sh/docs/openapi.json) oder [YAML](https://passport.kabin.sh/docs/openapi.yaml) herunter. - [`getRegistryInfo`](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-info) und [`getRegistryStatus`](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-status) melden das verwendete Feldregister und wie viele Pässe nach einer Registeränderung aktualisiert werden müssen. - [`getRegistrySubmissionStatus`](https://passport.kabin.sh/docs/de/api-reference/passports/get-registry-submission-status) meldet den Stand eines Passes im EU-Register; er ist `not_available`, solange das EU-Register Wirtschaftsakteuren keine Schnittstelle bietet. --- # Glossar > Die Fach- und API-Begriffe dieser Dokumentation, auf Deutsch und Englisch. Die Spalte „Englisch“ nennt den Begriff, den die englischen Seiten dieser Dokumentation verwenden. Code, Feldnamen und Operation-Ids sind in beiden Sprachen gleich. ## Produkte und Pässe | Begriff | Englisch | Bedeutung | | ----------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Batteriepass | Battery passport | Der digitale Datensatz einer einzelnen Batterie nach Verordnung (EU) 2023/1542 (Art. 77). In BatteryPass hat jede Einheit einen Pass mit nummerierten, signierten Versionen. Siehe [Pässe](https://passport.kabin.sh/docs/de/concepts/passports). | | Passversion | Passport version | Eine Veröffentlichung eines Passes. Sie ändert sich nie und enthält Snapshot, öffentliches Dokument und deren Signaturen. | | Wirtschaftsakteur | Economic operator | Hersteller, Einführer oder Bevollmächtigter, der eine Batterie in Verkehr bringt und für ihren Pass verantwortlich ist. | | Batteriemodell | Battery model | Das Handelsprodukt mit Name, SKU, GTIN und Kategorie. Siehe [Batteriemodelle](https://passport.kabin.sh/docs/de/concepts/battery-models). | | Modellversion | Model version | Ein technischer Stand eines Batteriemodells, gültig ab seinem Datum `effective_from`. Hier liegen die Werte der Modellebene. Status `draft`, `approved` oder `superseded`. | | Einheit | Unit | Eine physische Batterie mit Seriennummer, einer Modellversion zugeordnet. In der API heißt sie `Battery`. | | GTIN | GTIN | Global Trade Item Number, die GS1-Nummer des Batteriemodells. BatteryPass speichert sie 14-stellig. | | GS1 Digital Link | GS1 Digital Link | Eine Webadresse aus GS1-Kennungen, hier `/01/{gtin}/21/{serial}`. Genau diese trägt der QR-Code. Siehe [GS1 Digital Link](https://passport.kabin.sh/docs/de/concepts/digital-link). | | Resolver | Resolver | Der Dienst hinter dem Digital Link: Er leitet Browser zur öffentlichen Seite und Maschinen zum öffentlichen JSON oder zu einem Linkset. | | Second Life | Second life | Eine umgewidmete, wiederverwendete oder wiederaufbereitete Batterie. Sie wird eine neue Einheit mit eigenem Pass, der auf das Original verweist. | ## Daten und Prüfung | Begriff | Englisch | Bedeutung | | ------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Feldregister | Field registry | Die versionierte Liste aller Passfelder: Ebene, Zugriffsstufe, Pflicht je Batteriekategorie, Validierung und Exportpfade. Sie lesen es mit [`getRegistrySchema`](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-schema). | | Zugriffsstufe | Tier (access tier) | Wer ein Feld oder Dokument sehen darf: `public`, `legitimate_interest`, `authority` oder `internal`. Siehe [Zugriffsstufen](https://passport.kabin.sh/docs/de/concepts/access-tiers). | | Herkunft | Provenance | Woher ein Wert stammt: Quelle, Urheber, Zeitpunkt, Prüfer und Nachweise, für jede Revision aufbewahrt. Siehe [Herkunft und Freigabe](https://passport.kabin.sh/docs/de/concepts/provenance-and-review). | | Nachweis | Evidence | Ein Dokument, das einen Wert belegt, etwa eine Konformitätserklärung oder ein Prüfbericht. Nachweise sind versioniert und haben eine eigene Zugriffsstufe. | | Vier-Augen-Prinzip | Four-eyes principle | Die Regel, dass ein anderes Mitglied einen Wert freigibt als das, das ihn erfasst hat. In jeder Organisation standardmäßig eingeschaltet. | ## Zustand und Lebenszyklus | Begriff | Englisch | Bedeutung | | ----------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Zustandsdaten | Condition data | Gemessene Werte einer Einheit, der Abschnitt `condition` des Registers. Als Messwerte ohne Prüfschritt angenommen. Siehe [Zustandsdaten und Lebenszyklus](https://passport.kabin.sh/docs/de/concepts/condition-and-lifecycle). | | Gesundheitszustand (State of Health, SoH) | State of health | Wie viel ihrer ursprünglichen Leistungsfähigkeit eine Batterie noch hat. Das Register drückt ihn über Felder wie Kapazitätsverlust, verbleibende Kapazität und, bei Elektrofahrzeugbatterien, den State of Certified Energy aus. | | Lebenszyklus-Ereignis | Lifecycle event | Eine datierte Aufzeichnung dessen, was mit einer Einheit geschehen ist, etwa eine Reparatur oder Inspektion. Ereignisse ändern keine Daten der Einheit. | ## API | Begriff | Englisch | Bedeutung | | ------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | API-Schlüssel | API key | Der Zugangsschlüssel einer Organisation, `bp_live_…`, gesendet als `Authorization: Bearer`. Er handelt mit den Rechten eines Betrachters. Siehe [Authentifizierung](https://passport.kabin.sh/docs/de/authentication). | | Scope | Scope | Was ein Schlüssel darf: `read` (Daten lesen), `condition` (Zustandsdaten und Lebenszyklus-Ereignisse melden) oder `import` (Tabellenimporte bereitstellen, die Mitglieder übernehmen). | | Problem Details | Problem Details | Das Fehlerformat nach RFC 9457 (`application/problem+json`), hier mit stabilem `code` und einer `request_id`. Siehe [Fehler](https://passport.kabin.sh/docs/de/errors). | | Keyset-Paginierung | Keyset pagination | Blättern mit `limit` und einem undurchsichtigen `cursor`; eine Seite liefert `items` und `next_cursor`, das auf der letzten Seite `null` ist. Siehe [Paginierung](https://passport.kabin.sh/docs/de/pagination). | ## Signaturen | Begriff | Englisch | Bedeutung | | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | JCS | JCS | JSON Canonicalization Scheme (RFC 8785): eine feste Art, JSON zu serialisieren, sodass dieselben Daten immer dieselben Bytes zum Hashen und Signieren ergeben. | | Ed25519 | Ed25519 | Das Signaturverfahren von BatteryPass (EdDSA auf Curve25519, RFC 8032). Öffentliche Schlüssel haben 32 Bytes, Signaturen 64 Bytes. | | JWK | JWK | JSON Web Key (RFC 7517), ein öffentlicher Schlüssel als JSON. BatteryPass veröffentlicht seine Ed25519-Schlüssel als `OKP`-Schlüssel (RFC 8037) in einem JWK-Set unter `/.well-known/batterypass/keys.json`. Siehe [Signaturen](https://passport.kabin.sh/docs/de/concepts/signatures). | --- # Anleitungen > Schritt-für-Schritt-Anleitungen für die häufigsten Aufgaben einer Integration, mit Anfragen, Antworten und den wichtigen Fehlerfällen. Jede Anleitung führt eine Aufgabe von Anfang bis Ende durch: was Sie brauchen, die Anfragen in der richtigen Reihenfolge, was zurückkommt und mit welchen Fehlern Sie rechnen sollten. Die Beispiele verwenden die Umgebungsvariablen `BATTERYPASS_URL` und `BATTERYPASS_API_KEY` aus dem [Schnellstart](https://passport.kabin.sh/docs/de/quickstart). ## Öffentliche Pässe [**Signatur offline prüfen**](https://passport.kabin.sh/docs/de/guides/verify-signature): Das öffentliche Dokument kanonisieren, den Hash vergleichen und die Ed25519-Signatur mit dem veröffentlichten Schlüssel prüfen. [**QR-Code auflösen**](https://passport.kabin.sh/docs/de/guides/resolve-digital-link): Vom gescannten GS1 Digital Link zum öffentlichen Pass-JSON, oder per GTIN und Seriennummer nachschlagen. ## Daten lesen [**Einheiten abgleichen**](https://passport.kabin.sh/docs/de/guides/sync-units): Einheiten seitenweise mit dem Cursor übernehmen, nach einer Unterbrechung fortsetzen und Änderungen erkennen. [**Pässe exportieren**](https://passport.kabin.sh/docs/de/guides/export-passports): BatteryPass-JSON, PDF und QR-Code einer Passversion sowie die Einheiten als Tabelle. ## Daten melden [**Zustandsdaten senden**](https://passport.kabin.sh/docs/de/guides/push-condition-data): Messwerte aus einem Batteriemanagementsystem mit `updateCondition` melden. [**Lebenszyklus-Ereignisse erfassen**](https://passport.kabin.sh/docs/de/guides/record-lifecycle-events): Inspektionen, Reparaturen und andere Ereignisse einer Einheit melden und ihren Lebenszyklus lesen. ## Im Betrieb [**Fehler und Wiederholungen**](https://passport.kabin.sh/docs/de/guides/handle-errors): Welche Antworten Sie wiederholen, Backoff mit Jitter und was ins Log gehört. [**Schlüssel sicher verwalten**](https://passport.kabin.sh/docs/de/guides/secure-key-handling): Wo Schlüssel liegen, welcher Scope für welches System, Wechsel ohne Ausfall und was nach einem Leck zu tun ist. Sie suchen ein Skript zum Übernehmen? Die [Rezepte](https://passport.kabin.sh/docs/de/recipes) sind kurze, lauffähige Programme für häufige Aufgaben. --- # Passsignatur offline prüfen > Prüfen Sie allein mit dem öffentlichen JSON und den veröffentlichten Schlüsseln, dass die öffentlichen Daten eines Passes genau so signiert wurden. Jede veröffentlichte Passversion trägt eine Ed25519-Signatur über ihr öffentliches Dokument. Sie können sie ohne API-Schlüssel prüfen und müssen BatteryPass nicht fragen, ob die Daten echt sind: Das öffentliche JSON und das Schlüsselset unter `/.well-known/batterypass/keys.json` genügen. Den Hintergrund erklärt [Signaturen](https://passport.kabin.sh/docs/de/concepts/signatures). ## Was Sie brauchen - Den Pass: seine ID (die UUID aus `urn:dpp:{uuid}`) oder den GS1 Digital Link aus seinem QR-Code. - Die BatteryPass-Adresse, die ihn veröffentlicht hat, als `BATTERYPASS_URL`. Öffentliche Abfragen brauchen keinen Schlüssel. - Eine Implementierung von RFC 8785 (JCS) und Ed25519 für Ihre Sprache: | Sprache | JSON-Kanonisierung (RFC 8785) | Ed25519 | | -------------------- | ------------------------------------------------------------ | -------------------------------------------------------- | | TypeScript, Node 20+ | [`canonicalize`](https://www.npmjs.com/package/canonicalize) | `node:crypto` | | Python 3 | [`rfc8785`](https://pypi.org/project/rfc8785/) | [`cryptography`](https://pypi.org/project/cryptography/) | | Go | [`github.com/gowebpki/jcs`](https://github.com/gowebpki/jcs) | `crypto/ed25519` | ```mermaid flowchart LR J["Öffentliches JSON"] --> D["document"] J --> V["verification"] D -->|"RFC 8785"| C["Kanonische Bytes"] C -->|"SHA-256"| H{"Gleich verification.hash?"} K["keys.json"] -->|"kid = verification.key_id"| P["Öffentlicher Ed25519-Schlüssel"] C --> S{"verification.signature gültig?"} P --> S ``` ## Schritt für Schritt 1. **Öffentliches JSON abrufen** Lesen Sie den Pass mit [`getPublicPassport`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport) oder folgen Sie dem Digital Link mit `?linkType=describedby`: Der Resolver antwortet mit `307` und dem öffentlichen JSON als Ziel. [`getPublicPassportByProductId`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-by-product-id) liefert dasselbe Dokument über GTIN und Seriennummer. ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/public/passports/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10" # oder ausgehend vom QR-Code (-L folgt der Weiterleitung) curl -sSL "https://batterypass.example.com/01/04012345000023/21/SV2027-000184?linkType=describedby" ``` ```json title="200" { "passport_id": "urn:dpp:0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10", "status": "published", "document": { "passport_id": "urn:dpp:0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10", "gtin": "04012345000023", "serial_number": "SV2027-000184", "gs1_digital_link": "https://batterypass.example.com/01/04012345000023/21/SV2027-000184", "model_name": "SolarVolt Home 10", "tier": "public", "version": { "no": 3, "published_at": "2026-10-20T09:12:44Z", "registry_version": "0.5.0" }, "category": { "key": "stationary", "label": { "de": "Stationärer Energiespeicher (> 2 kWh)", "en": "Stationary storage battery (> 2 kWh)" } }, "sections": [ { "key": "performance", "label": { "de": "Leistung & Haltbarkeit", "en": "Performance & durability" }, "fields": [ { "key": "performance.nominal_voltage", "label": { "de": "Nennspannung", "en": "Nominal voltage" }, "tier": "public", "level": "model", "status": "provided", "value": 51.2, "unit": "V" } ] } ], "restricted_fields_present": ["condition.state_of_charge"] }, "verification": { "algorithm": "Ed25519", "canonicalization": "RFC8785", "hash": "sha256:9c1f0e5b7d4a3c2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e", "signature": "q2v0Yb3m8yJ1kQe7Zt5nC4xL9pW2sR6uH0aF3dG8jK1oM5iT7vX4zB2cN6eQ9rS1wY3uA5gD8hJ0lP4fK7mV2Q", "key_id": "2026-10-k1", "signed_at": "2026-10-20T09:12:44Z", "keys_url": "https://batterypass.example.com/.well-known/batterypass/keys.json" }, "links": { "page": "https://batterypass.example.com/pass/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10", "json": "https://batterypass.example.com/api/v1/public/passports/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10", "keys": "https://batterypass.example.com/.well-known/batterypass/keys.json", "gs1_digital_link": "https://batterypass.example.com/01/04012345000023/21/SV2027-000184" } } ``` Signiert ist nur `document`. `passport_id`, `status`, `valid_until`, `verification` und `links` ändern sich im Leben eines Passes und fallen nicht unter die Signatur. Ein Pass, der gerade nicht öffentlich ist (`qr_locked`, `suspended`, `withdrawn`, `deleted_recycled`), antwortet nur mit seinem Status, ohne `document` und `verification`: Dann gibt es nichts zu prüfen. 2. **Dokument kanonisieren** Lesen Sie die Antwort mit einem gewöhnlichen JSON-Parser und serialisieren Sie das Objekt `document` mit JCS (RFC 8785). JCS sortiert die Felder, entfernt Leerraum und schreibt Zahlen und Zeichenketten in einer festen Form. Dieselben Daten ergeben so immer dieselben Bytes, egal in welcher Reihenfolge und mit welchen Leerzeichen die Antwort kam. Kanonisieren Sie das geparste Objekt; hashen Sie weder die Bytes der Antwort noch die Ausgabe Ihres eigenen JSON-Writers. 3. **Hash vergleichen** Berechnen Sie SHA-256 über die kanonischen Bytes und schreiben Sie das Ergebnis als `sha256:` gefolgt vom Hex-Digest in Kleinbuchstaben. Es muss `verification.hash` entsprechen. Weicht es ab, ist Ihr Dokument nicht das signierte. 4. **Schlüssel wählen** Laden Sie das JWK-Set von `/.well-known/batterypass/keys.json` der BatteryPass-Adresse, von der der Pass stammen soll, und nehmen Sie den Schlüssel, dessen `kid` gleich `verification.key_id` ist: ```json title="keys.json" { "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "WuoaQRoQuh2kPs5yYB6HxNz3Y58ndE8fRHnzG10NxoU", "kid": "2026-10-k1", "use": "sig", "alg": "EdDSA" } ] } ``` Das Set braucht keinen Schlüssel, darf eine Stunde zwischengespeichert werden und enthält auch abgelöste Schlüssel. Signaturen alter Versionen bleiben so nach einem Schlüsselwechsel prüfbar. 5. **Signatur prüfen** Dekodieren Sie `x` (32 Bytes) und `verification.signature` (64 Bytes) aus base64url ohne Padding und prüfen Sie die Ed25519-Signatur über die kanonischen Bytes, nicht über den Hash. > [!WARNING] > **Woher die Schlüssel kommen** > > `verification.keys_url` und `links.keys` sind nicht signiert. Laden Sie die Schlüssel von der Adresse, der Sie > vertrauen (der BatteryPass-Adresse des Herstellers oder einem früher gespeicherten Schlüsselset), und werten Sie > eine `keys_url`, die woanders hinzeigt, als fehlgeschlagene Prüfung. ## Vollständiger Code Jedes Programm erwartet die UUID des Passes als Argument und gibt `valid` aus oder bricht mit dem Grund ab. ```ts title="TypeScript" // verify.mts: npm install canonicalize, then: npx tsx verify.mts import { createHash, createPublicKey, verify } from 'node:crypto'; import canonicalize from 'canonicalize'; const base = process.env.BATTERYPASS_URL; // the BatteryPass address that published the passport const passportId = process.argv[2]; // the UUID of urn:dpp:{uuid} async function getJSON(url: string) { const res = await fetch(url); if (!res.ok) throw new Error(`GET ${url}: ${res.status}`); return res.json(); } const passport = await getJSON(`${base}/api/v1/public/passports/${passportId}`); const { document, verification } = passport; if (!document || !verification) throw new Error(`status ${passport.status}: nothing to verify`); // 1. Canonicalise the signed document (RFC 8785) and compare its SHA-256 with verification.hash. const canonical = Buffer.from(canonicalize(document) ?? '', 'utf8'); const hash = `sha256:${createHash('sha256').update(canonical).digest('hex')}`; if (hash !== verification.hash) throw new Error(`hash mismatch: ${hash}`); // 2. Take the key named by verification.key_id from the published JWK set. const { keys } = await getJSON(`${base}/.well-known/batterypass/keys.json`); const jwk = keys.find((k: { kid: string }) => k.kid === verification.key_id); if (!jwk) throw new Error(`unknown key ${verification.key_id}`); // 3. Check the Ed25519 signature (base64url, no padding) over the canonical bytes. const publicKey = createPublicKey({ key: jwk, format: 'jwk' }); const valid = verify(null, canonical, publicKey, Buffer.from(verification.signature, 'base64url')); console.log(valid ? 'valid' : 'INVALID'); process.exitCode = valid ? 0 : 1; ``` ```python title="Python" # verify.py: pip install requests cryptography rfc8785, then: python verify.py import base64 import hashlib import os import sys import requests import rfc8785 from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey base = os.environ["BATTERYPASS_URL"] # the BatteryPass address that published the passport passport_id = sys.argv[1] # the UUID of urn:dpp:{uuid} def get_json(url): res = requests.get(url, timeout=10) res.raise_for_status() return res.json() def b64url(text): return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4)) passport = get_json(f"{base}/api/v1/public/passports/{passport_id}") document, verification = passport.get("document"), passport.get("verification") if not document or not verification: sys.exit(f"status {passport['status']}: nothing to verify") # 1. Canonicalise the signed document (RFC 8785) and compare its SHA-256 with verification.hash. canonical = rfc8785.dumps(document) digest = "sha256:" + hashlib.sha256(canonical).hexdigest() if digest != verification["hash"]: sys.exit(f"hash mismatch: {digest}") # 2. Take the key named by verification.key_id from the published JWK set. keys = get_json(f"{base}/.well-known/batterypass/keys.json")["keys"] jwk = next((k for k in keys if k["kid"] == verification["key_id"]), None) if jwk is None: sys.exit(f"unknown key {verification['key_id']}") # 3. Check the Ed25519 signature (base64url, no padding) over the canonical bytes. try: Ed25519PublicKey.from_public_bytes(b64url(jwk["x"])).verify(b64url(verification["signature"]), canonical) except InvalidSignature: sys.exit("INVALID") print("valid") ``` ```go title="Go" // verify: go get github.com/gowebpki/jcs, then: go run . package main import ( "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "log" "net/http" "os" "github.com/gowebpki/jcs" ) type publicPassport struct { Status string `json:"status"` Document json.RawMessage `json:"document"` // kept as received, canonicalised below Verification *struct { Hash string `json:"hash"` Signature string `json:"signature"` KeyID string `json:"key_id"` } `json:"verification"` } type jwkSet struct { Keys []struct { Kid string `json:"kid"` X string `json:"x"` } `json:"keys"` } func getJSON(url string, v any) error { res, err := http.Get(url) if err != nil { return err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return fmt.Errorf("GET %s: %s", url, res.Status) } return json.NewDecoder(res.Body).Decode(v) } func main() { base := os.Getenv("BATTERYPASS_URL") // the BatteryPass address that published the passport passportID := os.Args[1] // the UUID of urn:dpp:{uuid} var p publicPassport if err := getJSON(base+"/api/v1/public/passports/"+passportID, &p); err != nil { log.Fatal(err) } if p.Document == nil || p.Verification == nil { log.Fatalf("status %s: nothing to verify", p.Status) } // 1. Canonicalise the signed document (RFC 8785) and compare its SHA-256 with verification.hash. canonical, err := jcs.Transform(p.Document) if err != nil { log.Fatal(err) } sum := sha256.Sum256(canonical) if hash := "sha256:" + hex.EncodeToString(sum[:]); hash != p.Verification.Hash { log.Fatalf("hash mismatch: %s", hash) } // 2. Take the key named by verification.key_id from the published JWK set. var set jwkSet if err := getJSON(base+"/.well-known/batterypass/keys.json", &set); err != nil { log.Fatal(err) } var pub []byte for _, k := range set.Keys { if k.Kid == p.Verification.KeyID { pub, _ = base64.RawURLEncoding.DecodeString(k.X) } } if len(pub) != ed25519.PublicKeySize { log.Fatalf("unknown key %s", p.Verification.KeyID) } // 3. Check the Ed25519 signature (base64url, no padding) over the canonical bytes. sig, err := base64.RawURLEncoding.DecodeString(p.Verification.Signature) if err != nil || !ed25519.Verify(pub, canonical, sig) { log.Fatal("INVALID") } fmt.Println("valid") } ``` Viele Pässe auf einmal prüft das Rezept [Signaturen mehrerer Pässe prüfen](https://passport.kabin.sh/docs/de/recipes/verify-signatures). ## Wenn die Prüfung fehlschlägt | Ergebnis | Bedeutung | Was zu tun ist | | ----------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Kein `document` oder `verification` | Der Pass ist gerade nicht öffentlich; `status` nennt den Grund | Status anzeigen; es gibt nichts zu prüfen | | Hash weicht ab | Das Dokument ist nicht das signierte oder wurde nicht richtig kanonisiert | Das geparste `document` mit einer RFC-8785-Bibliothek kanonisieren; weicht der Hash dann noch ab, gelten die Daten als nicht geprüft | | Unbekannte `key_id` | Das geladene Schlüsselset kennt diesen Schlüssel nicht | Schlüsselset neu laden (Ihres ist vielleicht älter als die Signatur); fehlt er weiterhin, ist die Prüfung fehlgeschlagen | | Signatur ungültig | Das Dokument wurde nicht mit diesem Schlüssel signiert | Die Daten gelten als nicht geprüft | ## Ältere Versionen [`getPublicPassportVersion`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-version) liefert das öffentliche JSON der Version, die zu einem Zeitpunkt galt (`?at=2026-09-01T00:00:00Z`); sobald eine spätere Version sie abgelöst hat, ist `valid_until` gesetzt. Sie prüfen sie genauso: Versionen ändern sich nach der Veröffentlichung nie, und abgelöste Schlüssel bleiben im Schlüsselset. ## Prüfung auf dem Server [`verifyPassport`](https://passport.kabin.sh/docs/de/api-reference/public/verify-passport) führt dieselbe Prüfung auf dem Server aus und prüft zusätzlich den vollständigen Snapshot der Version, den ein Scanner nie erhält. Das eignet sich für einen schnellen Blick oder ein Monitoring. Unabhängig vom Wort des Servers ist nur die Offline-Prüfung oben. ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/public/passports/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10/verify?version_no=3" ``` ```json title="200" { "valid": true, "version_no": 3, "key_id": "2026-10-k1", "public_document_valid": true, "snapshot_valid": true, "signed_at": "2026-10-20T09:12:44Z", "checked_at": "2026-10-24T08:00:00Z" } ``` Ohne `version_no` prüft der Server die aktuelle Version. `valid` ist nur dann `true`, wenn beide Signaturen gültig sind und beide Hashes stimmen. --- # Zustandsdaten aus einem Batteriemanagementsystem senden > Melden Sie Messwerte wie Ladezustand oder Vollzyklen mit updateCondition und lesen Sie, was übernommen, übersprungen und neu veröffentlicht wurde. Ein Batteriemanagementsystem (BMS), ein Servicewerkzeug oder eine Monitoring-Plattform meldet seine Messwerte mit [`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition). BatteryPass übernimmt die Werte als Messungen ohne Prüfschritt, speichert sie mit Messzeitpunkt und `source` `api`, ergänzt den Lebenszyklus der Einheit um ein Ereignis `condition_update`, das den Schlüssel nennt (`api_key`), und veröffentlicht den Pass nach der Regel Ihrer Organisation neu. Die Grundlagen stehen unter [Zustand und Lebenszyklus](https://passport.kabin.sh/docs/de/concepts/condition-and-lifecycle). ## Bevor Sie beginnen - **Ein Schlüssel mit dem Scope `condition`.** Eine Eigentümerin oder ein Eigentümer erstellt ihn im Portal unter **Einstellungen → API-Schlüssel**. Lesen (etwa [`getCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/get-condition) oder [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries)) braucht den Scope `read`; geben Sie dem Schlüssel beide Scopes, wenn das System beides tut. Mehr unter [Schlüssel sicher verwalten](https://passport.kabin.sh/docs/de/guides/secure-key-handling). - **Die ID der Einheit.** Speichern Sie sie in Ihrem System neben der Seriennummer, etwa mit [Einheiten abgleichen](https://passport.kabin.sh/docs/de/guides/sync-units), oder suchen Sie sie mit `listBatteries?serial_prefix=`. - **Die Felder.** Angenommen werden nur die dynamischen Felder des Feldregisters (Abschnitt `condition`), und nur die, die für die Batteriekategorie der Einheit gelten. `getCondition` nennt sie mit ihren aktuellen Werten und zeigt die Regel für die Neuveröffentlichung: ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/condition" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` ```json title="200 (gekürzt)" { "battery_id": "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f", "field_keys": [ "condition.state_of_charge", "condition.remaining_capacity", "condition.capacity_fade", "condition.number_of_full_cycles" ], "values": [ { "field_key": "condition.number_of_full_cycles", "value": 412, "revision": 6, "source": "api", "review_status": "approved", "measured_at": "2026-10-23T06:00:00Z", "recorded_at": "2026-10-23T06:00:04Z", "registry_version": "0.5.0", "checks": [] } ], "republish": "immediate" } ``` ## Ein Update senden `PATCH /api/v1/batteries/{batteryId}/condition` mit einem JSON-Body: - **values** `object[]` (body, erforderlich): 1 bis 50 Werte, jedes Feld höchstens einmal. **Wert** - **field_key** `string` (erforderlich): Ein dynamisches Feld der Einheit, z. B. `condition.state_of_charge`. - **value** `any` (erforderlich): Der Wert in der Einheit des Feldes (z. B. `%` oder `Ah`); `null` leert das Feld. - **measured_at** `date-time`: Wann dieser Wert gemessen wurde; ohne Angabe gilt `measured_at` des Updates. * **measured_at** `date-time` (body): Der Messzeitpunkt aller Werte ohne eigenen. * **note** `string` (body): Höchstens 500 Zeichen; wird die Beschreibung des Lebenszyklus-Ereignisses. ```bash title="cURL" curl -sS -X PATCH "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/condition" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "measured_at": "2026-10-24T06:00:00Z", "note": "BMS daily report", "values": [ { "field_key": "condition.state_of_charge", "value": 81.5 }, { "field_key": "condition.number_of_full_cycles", "value": 412 }, { "field_key": "condition.remaining_capacity", "value": 187.2, "measured_at": "2026-10-24T05:40:00Z" } ] }' ``` ```ts title="TypeScript" const batteryId = '0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f'; const res = await fetch(`${process.env.BATTERYPASS_URL}/api/v1/batteries/${batteryId}/condition`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ measured_at: '2026-10-24T06:00:00Z', note: 'BMS daily report', values: [ { field_key: 'condition.state_of_charge', value: 81.5 }, { field_key: 'condition.number_of_full_cycles', value: 412 }, { field_key: 'condition.remaining_capacity', value: 187.2, measured_at: '2026-10-24T05:40:00Z' }, ], }), }); const result = await res.json(); if (!res.ok) throw new Error(`${res.status} ${result.code} (request_id ${result.request_id})`); console.log(result.publication.outcome, result.skipped); ``` ```python title="Python" import os import requests battery_id = "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f" res = requests.patch( f"{os.environ['BATTERYPASS_URL']}/api/v1/batteries/{battery_id}/condition", headers={"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"}, json={ "measured_at": "2026-10-24T06:00:00Z", "note": "BMS daily report", "values": [ {"field_key": "condition.state_of_charge", "value": 81.5}, {"field_key": "condition.number_of_full_cycles", "value": 412}, {"field_key": "condition.remaining_capacity", "value": 187.2, "measured_at": "2026-10-24T05:40:00Z"}, ], }, timeout=30, ) result = res.json() if not res.ok: raise SystemExit(f"{res.status_code} {result['code']} (request_id {result.get('request_id')})") print(result["publication"]["outcome"], result["skipped"]) ``` ```go title="Go" package main import ( "bytes" "encoding/json" "fmt" "log" "net/http" "os" ) func main() { batteryID := "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f" body, _ := json.Marshal(map[string]any{ "measured_at": "2026-10-24T06:00:00Z", "note": "BMS daily report", "values": []map[string]any{ {"field_key": "condition.state_of_charge", "value": 81.5}, {"field_key": "condition.number_of_full_cycles", "value": 412}, {"field_key": "condition.remaining_capacity", "value": 187.2, "measured_at": "2026-10-24T05:40:00Z"}, }, }) url := os.Getenv("BATTERYPASS_URL") + "/api/v1/batteries/" + batteryID + "/condition" req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var result struct { Skipped []map[string]string `json:"skipped"` Publication struct { Outcome string `json:"outcome"` } `json:"publication"` Code string `json:"code"` RequestID string `json:"request_id"` } if err := json.NewDecoder(res.Body).Decode(&result); err != nil { log.Fatal(err) } if res.StatusCode != http.StatusOK { log.Fatalf("%d %s (request_id %s)", res.StatusCode, result.Code, result.RequestID) } fmt.Println(result.Publication.Outcome, result.Skipped) } ``` ```json title="200 (gekürzt)" { "accepted": [ { "field_key": "condition.state_of_charge", "value": 81.5, "unit": "%", "revision": 9, "source": "api", "review_status": "approved", "measured_at": "2026-10-24T06:00:00Z", "recorded_at": "2026-10-24T06:00:03Z", "reviewed_at": "2026-10-24T06:00:03Z", "registry_version": "0.5.0", "checks": [] }, { "field_key": "condition.remaining_capacity", "value": 187.2, "unit": "Ah", "revision": 5, "source": "api", "review_status": "approved", "measured_at": "2026-10-24T05:40:00Z", "recorded_at": "2026-10-24T06:00:03Z", "reviewed_at": "2026-10-24T06:00:03Z", "registry_version": "0.5.0", "checks": [] } ], "skipped": [ { "field_key": "condition.number_of_full_cycles", "reason": "unchanged" } ], "event": { "id": "0192f8b1-2a3c-7d4e-9f50-61a2b3c4d5e6", "battery_id": "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f", "type": "condition_update", "occurred_on": "2026-10-24", "description": "BMS daily report", "source": "api", "api_key": { "id": "0192e4c7-8b9a-7c6d-8e5f-4a3b2c1d0e9f", "name": "BMS Landshut" }, "field_keys": ["condition.state_of_charge", "condition.remaining_capacity"], "evidence": [], "created_at": "2026-10-24T06:00:03Z" }, "publication": { "outcome": "published", "version": { "version_no": 4, "published_at": "2026-10-24T06:00:03Z", "reason": "condition update", "registry_version": "0.5.0", "canonical_hash": "sha256:5d0c9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d", "public_hash": "sha256:7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d5d0c9e8f7a6b5c4d3e2f1a0b9c8d", "key_id": "2026-10-k1", "batterypass_v2_valid": true, "batterypass_v2_issue_count": 0 } } } ``` ## Das Ergebnis lesen `accepted` enthält die gespeicherten Werte mit `source: api`, `review_status: approved` und ihrem `measured_at`. `skipped` nennt Werte, die gültig waren, aber nicht gespeichert wurden: | `reason` | Bedeutung | | ------------------- | ---------------------------------------------------------------------------------------------------------------- | | `older_measurement` | Der gespeicherte Wert wurde später gemessen als der gesendete. | | `unchanged` | Der gespeicherte Wert hat denselben Wert und denselben Messzeitpunkt, oder Sie haben ein Feld ohne Wert geleert. | `event` ist das Ereignis `condition_update` im Lebenszyklus der Einheit; es fehlt, wenn nichts übernommen wurde. Sein `occurred_on` ist der UTC-Tag der jüngsten übernommenen Messung, und `note` wird seine `description`. `publication.outcome` sagt, was mit dem Pass geschah: | `outcome` | Bedeutung | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `published` | Eine neue Passversion mit dem Grund `condition update` ist jetzt online (`version`). | | `unchanged` | Sofort veröffentlicht, aber der öffentliche Inhalt hat sich nicht geändert; es entstand keine neue Version. | | `scheduled` | Ihre Organisation veröffentlicht täglich: Der Lauf um 02:00 UTC übernimmt die neuen Werte. | | `blocked` | Die Veröffentlichungsprüfung hat abgelehnt (`blockers`). Die Werte sind gespeichert; der Pass bleibt im Status `update_needed` online, bis ein Mitglied ihn erneut veröffentlicht. | | `not_published` | Die Einheit hat keinen öffentlichen Pass; die Werte erscheinen mit der ersten Veröffentlichung. | Die Regel (`immediate` oder `daily`) ist eine Einstellung der Organisation im Portal; `getCondition` zeigt sie als `republish`. Die meisten Zustandsfelder gehören zur Zugriffsstufe `legitimate_interest`: Sie stehen im signierten Snapshot und in den eingeschränkten Ansichten, während das öffentliche JSON nur ihre Schlüssel in `restricted_fields_present` nennt (siehe [Zugriffsstufen](https://passport.kabin.sh/docs/de/concepts/access-tiers)). ## Regeln für ein Update - **Merge.** Nur die genannten Felder ändern sich; alle anderen behalten ihren Wert. `null` leert ein Feld. - **Messzeitpunkt.** Jeder Wert braucht ein `measured_at`, ein eigenes oder das der Anfrage. Es darf höchstens fünf Minuten nach der Serveruhr liegen und nicht vor dem Herstellungsdatum der Einheit. Senden Sie es in UTC (`Z`). - **Die jüngste Messung gewinnt.** Ein Wert, der früher gemessen wurde als der gespeicherte, wird übersprungen; verspätete oder doppelte Lieferungen richten also keinen Schaden an. Dasselbe Update zweimal zu senden ist unbedenklich: Beim zweiten Mal kommt alles als `unchanged` zurück. - **Alles oder nichts.** Ist ein Wert ungültig oder verletzen die zusammengeführten Zustandsdaten eine feldübergreifende Prüfung des Registers, wird das ganze Update abgelehnt und nichts gespeichert. - **Werte, keine Dokumente.** `updateCondition` nimmt keine `evidence_ids`; einen Diagnosebericht hängen Sie an ein [Lebenszyklus-Ereignis](https://passport.kabin.sh/docs/de/guides/record-lifecycle-events). ## Fehler ```json title="400" { "type": "about:blank", "title": "Bad Request", "status": 400, "code": "validation_error", "detail": "The request failed validation.", "request_id": "4f9c2a7d1e0b5c8a3f6e2d19", "errors": [ { "field": "values[0].field_key", "code": "unknown_field", "message": "performance.nominal_voltage is not condition data of this unit" }, { "field": "values[1].measured_at", "code": "out_of_range", "message": "measured_at lies in the future" } ] } ``` | Status und `code` | Typische Ursache | Was zu tun ist | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | `400 validation_error` | `values[i].field_key` mit `unknown_field` (kein dynamisches Feld dieser Einheit); `values[i].measured_at` mit `required` oder `out_of_range`; `values[i].value` außerhalb des Wertebereichs; ein Feld doppelt; eine fehlgeschlagene feldübergreifende Prüfung; `note` zu lang | Daten korrigieren; nicht unverändert wiederholen | | `401 unauthenticated` | Schlüssel fehlt, ist unbekannt oder widerrufen | Schlüssel im Secret-Speicher prüfen | | `403 forbidden` | Dem Schlüssel fehlt der Scope `condition` | Einen Schlüssel mit diesem Scope erstellen lassen | | `404 not_found` | Die Einheit gibt es in der Organisation des Schlüssels nicht | ID prüfen | | `409 conflict` | Das Batteriemodell der Einheit ist archiviert: Seine Daten lassen sich lesen, aber nicht ändern | Für Einheiten dieses Modells nichts mehr melden | | `429 rate_limited` | Mehr als 600 Anfragen pro Minute über alle Schlüssel der Organisation | `Retry-After` abwarten, dann erneut senden | [Fehler und Wiederholungen](https://passport.kabin.sh/docs/de/guides/handle-errors) zeigt eine Hilfsfunktion für Wiederholungen; das Rezept [Nächtlicher Zustandsdaten-Push](https://passport.kabin.sh/docs/de/recipes/nightly-condition-push) sendet ein Update je Einheit mit Backoff. --- # Lebenszyklus-Ereignisse erfassen > Melden Sie Inspektionen, Reparaturen und andere Ereignisse im Leben einer Einheit mit createLifecycleEvent und lesen Sie den Lebenszyklus mit listLifecycleEvents. Ein Lebenszyklus-Ereignis hält fest, dass mit einer Batterie etwas geschehen ist: Sie wurde in Betrieb genommen, geprüft, repariert, ein Bauteil wurde getauscht. Ereignisse sind Aufzeichnungen und ändern keine Daten der Einheit. Messwerte melden Sie mit [`updateCondition`](https://passport.kabin.sh/docs/de/guides/push-condition-data). Hintergrund: [Zustand und Lebenszyklus](https://passport.kabin.sh/docs/de/concepts/condition-and-lifecycle). | Operation | Scope | Was sie tut | | ------------------------------------------------------------------------------------------------------------ | ----------- | -------------------------------------------- | | [`createLifecycleEvent`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/create-lifecycle-event) | `condition` | Erfasst ein Ereignis für eine Einheit | | [`listLifecycleEvents`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events) | `read` | Der Lebenszyklus der Einheit, neueste zuerst | ## Ereignisarten | `type` | Wofür | | -------------------- | --------------------------------------------------------------- | | `placed_on_market` | Die Batterie wurde in Verkehr gebracht. | | `put_into_service` | Die Batterie wurde eingebaut und in Betrieb genommen. | | `inspection` | Eine Prüfung oder ein Wartungstermin. | | `repair` | Eine Reparatur. | | `component_replaced` | Ein Modul, eine Zelle oder ein anderes Bauteil wurde getauscht. | | `waste` | Die Batterie ist zu Abfall geworden. | | `recycled` | Die Batterie wurde dem Recycling übergeben. | | `custom` | Alles andere; beschreiben Sie es in `description`. | `ReportedEventType` im Vertrag nennt zusätzlich `condition_update`, doch `createLifecycleEvent` lehnt diesen Typ mit `400 validation_error` auf `type` ab: Solche Ereignisse schreibt `updateCondition`. Die Arten des zweiten Lebens (`repurposed`, `reused`, `remanufactured`) erscheinen im Lebenszyklus, wenn ein Mitglied im Portal eine Einheit für das zweite Leben anlegt; über die API lassen sie sich nicht erfassen. ## Ein Ereignis erfassen 1. **Dokumente finden (optional)** `evidence_ids` nennt Dokumente aus der Nachweisbibliothek Ihrer Organisation, etwa einen Diagnosebericht oder ein Reparaturprotokoll, höchstens 20 und jedes nur einmal. Mitglieder laden Dokumente im Portal hoch; die IDs finden Sie mit [`listEvidence`](https://passport.kabin.sh/docs/de/api-reference/evidence/list-evidence) (Schlüssel mit Scope `read`), zum Beispiel mit `?kind=diagnostic_report`. 2. **Ereignis senden** `POST /api/v1/batteries/{batteryId}/lifecycle-events` - **type** `string` (body, erforderlich): Eine der Arten oben. - **occurred_on** `date` (body, erforderlich): Der Tag des Ereignisses (`YYYY-MM-DD`), zwischen dem Herstellungsdatum der Einheit und heute. - **description** `string` (body): Höchstens 2000 Zeichen. - **evidence_ids** `uuid[]` (body): Höchstens 20 verschiedene Dokumente der Nachweisbibliothek. ```bash title="cURL" curl -sS -X POST "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/lifecycle-events" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "inspection", "occurred_on": "2026-10-22", "description": "Jahresinspektion: Gehäuse, Steckverbinder und BMS-Protokoll geprüft, ohne Befund.", "evidence_ids": ["0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b"] }' ``` ```ts title="TypeScript" const batteryId = '0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f'; const res = await fetch(`${process.env.BATTERYPASS_URL}/api/v1/batteries/${batteryId}/lifecycle-events`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ type: 'inspection', occurred_on: '2026-10-22', description: 'Jahresinspektion: Gehäuse, Steckverbinder und BMS-Protokoll geprüft, ohne Befund.', evidence_ids: ['0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b'], }), }); const event = await res.json(); if (res.status !== 201) throw new Error(`${res.status} ${event.code} (request_id ${event.request_id})`); console.log(event.id, event.type, event.occurred_on); ``` ```python title="Python" import os import requests battery_id = "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f" res = requests.post( f"{os.environ['BATTERYPASS_URL']}/api/v1/batteries/{battery_id}/lifecycle-events", headers={"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"}, json={ "type": "inspection", "occurred_on": "2026-10-22", "description": "Jahresinspektion: Gehäuse, Steckverbinder und BMS-Protokoll geprüft, ohne Befund.", "evidence_ids": ["0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b"], }, timeout=30, ) event = res.json() if res.status_code != 201: raise SystemExit(f"{res.status_code} {event['code']} (request_id {event.get('request_id')})") print(event["id"], event["type"], event["occurred_on"]) ``` ```go title="Go" package main import ( "bytes" "encoding/json" "fmt" "log" "net/http" "os" ) func main() { batteryID := "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f" body, _ := json.Marshal(map[string]any{ "type": "inspection", "occurred_on": "2026-10-22", "description": "Jahresinspektion: Gehäuse, Steckverbinder und BMS-Protokoll geprüft, ohne Befund.", "evidence_ids": []string{"0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b"}, }) url := os.Getenv("BATTERYPASS_URL") + "/api/v1/batteries/" + batteryID + "/lifecycle-events" req, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) // do not resend blindly: check listLifecycleEvents first } defer res.Body.Close() var event map[string]any if err := json.NewDecoder(res.Body).Decode(&event); err != nil { log.Fatal(err) } if res.StatusCode != http.StatusCreated { log.Fatalf("%d %v (request_id %v)", res.StatusCode, event["code"], event["request_id"]) } fmt.Println(event["id"], event["type"], event["occurred_on"]) } ``` ```json title="201" { "id": "0192f8c2-4d5e-7f60-8a71-b2c3d4e5f6a7", "battery_id": "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f", "type": "inspection", "occurred_on": "2026-10-22", "description": "Jahresinspektion: Gehäuse, Steckverbinder und BMS-Protokoll geprüft, ohne Befund.", "source": "api", "api_key": { "id": "0192e4c7-8b9a-7c6d-8e5f-4a3b2c1d0e9f", "name": "Servicesystem Nord" }, "field_keys": [], "evidence": [ { "id": "0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b", "kind": "diagnostic_report", "title": "Prüfprotokoll 2026", "filename": "pruefprotokoll-2026-10-22.pdf", "access_tier": "internal", "revision": 1, "superseded": false } ], "created_at": "2026-10-24T08:15:02Z" } ``` Ein Ereignis, das mit einem Schlüssel gesendet wurde, hat `source: api` und nennt den Schlüssel in `api_key`; Ereignisse von Mitgliedern tragen `reported_by`, solche von Servicepartnern `partner`. ## Keine automatische Duplikaterkennung BatteryPass erkennt nicht, wenn dasselbe Ereignis zweimal ankommt: Jede erfolgreiche Anfrage legt ein Ereignis an, und `createLifecycleEvent` kennt keinen Idempotenzschlüssel. Das ist wichtig, wenn eine Anfrage in ein Timeout läuft und Sie nicht wissen, ob sie angekommen ist: - Wiederholen Sie einen `POST` nach einem Netzwerkfehler, einem Timeout oder einem `5xx` nicht automatisch. - Bevor Sie es erneut senden, suchen Sie Ihr Ereignis (gleicher `type`, gleiches `occurred_on` und gleiche `description`, `source: api`) in `listLifecycleEvents`. Die Liste ist nach `occurred_on` sortiert, das neueste zuerst; ein rückdatiertes Ereignis steht also nicht unbedingt auf der ersten Seite. Lesen Sie mit `next_cursor` weiter, bis die Seite Ereignisse enthält, die vor Ihrem lagen (das [Beispiel für Wiederholungen](https://passport.kabin.sh/docs/de/idempotency) macht genau das). - Speichern Sie die zurückgegebene `id` in Ihrem System, damit Sie wissen, welche Ereignisse bereits erfasst sind. Ein `429 rate_limited` dürfen Sie nach `Retry-After` gefahrlos wiederholen: Die Anfrage wurde abgelehnt, bevor etwas gespeichert wurde. Mehr dazu unter [Wiederholungen und Idempotenz](https://passport.kabin.sh/docs/de/idempotency). ## Den Lebenszyklus lesen ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/lifecycle-events?limit=50" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` Die Antwort ist eine Seite `{items, next_cursor}`, das jüngste `occurred_on` zuerst; ältere Ereignisse folgen über `next_cursor` ([Paginierung](https://passport.kabin.sh/docs/de/pagination)). Die Liste enthält jedes Ereignis der Einheit, gleich wer es gemeldet hat: Mitglieder, Servicepartner über ihre Links, Zustandsupdates (`type: condition_update` mit den aktualisierten `field_keys`), Schlüssel und das System. Beim zweiten Leben nennen `related_battery_id` und `related_serial_number` die jeweils andere Einheit. ## Fehler | Status und `code` | Typische Ursache | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400 validation_error` | `type` nicht erlaubt (auch `condition_update`); `occurred_on` fehlt, liegt in der Zukunft oder vor dem Herstellungsdatum; `description` zu lang; ein Eintrag in `evidence_ids` ist kein Dokument Ihrer Organisation oder kommt doppelt vor | | `401 unauthenticated` | Schlüssel fehlt, ist unbekannt oder widerrufen | | `403 forbidden` | Dem Schlüssel fehlt der Scope `condition` (für `listLifecycleEvents`: `read`) | | `404 not_found` | Die Einheit gibt es in der Organisation des Schlüssels nicht | | `409 conflict` | Das Batteriemodell der Einheit ist archiviert | | `429 rate_limited` | Zu viele Anfragen; `Retry-After` abwarten | --- # Pässe exportieren > Laden Sie BatteryPass-JSON, PDF und QR-Code einer Passversion sowie die Einheiten Ihrer Organisation als Tabelle herunter. Alle Exporte sind einfache `GET`-Anfragen mit einem Schlüssel, der den Scope `read` hat. Sie kommen als Dateien mit dem Header `Content-Disposition: attachment` zurück, der einen fertigen Dateinamen mitbringt. | Export | Operation | Content-Type | Dateiname | | ----------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------- | ----------------------------------------- | | BatteryPass-v2.0-JSON einer Version | [`exportPassportJson`](https://passport.kabin.sh/docs/de/api-reference/passports/export-passport-json) | `application/json` | `{gtin}-{serial}-batterypass-v2.json` | | PDF einer Version | [`exportPassportPdf`](https://passport.kabin.sh/docs/de/api-reference/passports/export-passport-pdf) | `application/pdf` | `{gtin}-{serial}-v{versionNo}-{lang}.pdf` | | QR-Code der Einheit | [`getPassportQr`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-qr) | `image/png` oder `image/svg+xml` | `qr-{gtin}-{serial}.png` oder `.svg` | | Einheiten als Tabelle | [`exportUnits`](https://passport.kabin.sh/docs/de/api-reference/catalog/export-units) | XLSX oder `text/csv` | `units-{YYYYMMDD}.xlsx` oder `.csv` | Ein `/` in einer Seriennummer wird in Dateinamen zu `_`. ## Passexporte Schritt für Schritt 1. **Version ermitteln** JSON und PDF gehören zu einer veröffentlichten Version. [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport) nennt die aktuelle in `current_version.version_no`; [`listPassportVersions`](https://passport.kabin.sh/docs/de/api-reference/passports/list-passport-versions) listet alle Versionen, die neueste zuerst. ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/passport" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" | jq '{status, passport_id, version: .current_version.version_no}' ``` Eine Einheit, die nie veröffentlicht wurde, hat keine Version: Die Versionsexporte antworten mit `404 not_found`. 2. **Dateien herunterladen** ```bash title="cURL" BATTERY_ID=0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f # -O -J speichert unter dem Namen aus Content-Disposition curl -sS -OJ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ "$BATTERYPASS_URL/api/v1/batteries/$BATTERY_ID/passport/versions/3/batterypass-v2" curl -sS -OJ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ "$BATTERYPASS_URL/api/v1/batteries/$BATTERY_ID/passport/versions/3/pdf?lang=de" curl -sS -OJ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ "$BATTERYPASS_URL/api/v1/batteries/$BATTERY_ID/passport/qr?format=svg" ``` Im Code lesen Sie zuerst die Version und übernehmen den Dateinamen aus `Content-Disposition`: ```ts title="TypeScript" // exports.mts: npx tsx exports.mts import { writeFile } from 'node:fs/promises'; import { basename } from 'node:path'; const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }; const batteryId = process.argv[2]; async function download(path: string) { const res = await fetch(BASE + path, { headers }); if (!res.ok) throw new Error(`${path}: ${res.status} ${(await res.json()).code}`); // Content-Disposition: attachment; filename=04012345000023-SV2027-000184-v3-de.pdf const match = /filename="?([^";]+)"?/.exec(res.headers.get('Content-Disposition') ?? ''); const file = basename(match?.[1] ?? 'download'); await writeFile(file, Buffer.from(await res.arrayBuffer())); console.log(file, res.headers.get('Content-Type')); } const passportRes = await fetch(`${BASE}/batteries/${batteryId}/passport`, { headers }); const passport = await passportRes.json(); const versionNo = passport.current_version?.version_no; if (!versionNo) throw new Error(`passport is ${passport.status}: no published version yet`); await download(`/batteries/${batteryId}/passport/versions/${versionNo}/batterypass-v2`); await download(`/batteries/${batteryId}/passport/versions/${versionNo}/pdf?lang=de`); await download(`/batteries/${batteryId}/passport/qr?format=svg`); ``` ```python title="Python" # exports.py: python exports.py import os import re import sys import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_API_KEY"] battery_id = sys.argv[1] def download(path, **params): res = session.get(BASE + path, params=params, timeout=60) res.raise_for_status() # Content-Disposition: attachment; filename=04012345000023-SV2027-000184-v3-de.pdf match = re.search(r'filename="?([^";]+)"?', res.headers.get("Content-Disposition", "")) name = os.path.basename(match.group(1) if match else "download") with open(name, "wb") as f: f.write(res.content) print(name, res.headers["Content-Type"]) passport = session.get(f"{BASE}/batteries/{battery_id}/passport", timeout=30).json() version_no = passport.get("current_version", {}).get("version_no") if not version_no: sys.exit(f"passport is {passport['status']}: no published version yet") download(f"/batteries/{battery_id}/passport/versions/{version_no}/batterypass-v2") download(f"/batteries/{battery_id}/passport/versions/{version_no}/pdf", lang="de") download(f"/batteries/{battery_id}/passport/qr", format="svg") ``` ```go title="Go" // exports: go run . package main import ( "encoding/json" "fmt" "io" "log" "mime" "net/http" "os" "path/filepath" ) var ( base = os.Getenv("BATTERYPASS_URL") + "/api/v1" key = os.Getenv("BATTERYPASS_API_KEY") ) func get(path string) (*http.Response, error) { req, err := http.NewRequest(http.MethodGet, base+path, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) res, err := http.DefaultClient.Do(req) if err == nil && res.StatusCode != http.StatusOK { res.Body.Close() return nil, fmt.Errorf("%s: %s", path, res.Status) } return res, err } func download(path string) error { res, err := get(path) if err != nil { return err } defer res.Body.Close() // Content-Disposition: attachment; filename=04012345000023-SV2027-000184-v3-de.pdf _, params, err := mime.ParseMediaType(res.Header.Get("Content-Disposition")) if err != nil { return err } name := filepath.Base(params["filename"]) f, err := os.Create(name) if err != nil { return err } defer f.Close() if _, err := io.Copy(f, res.Body); err != nil { return err } fmt.Println(name, res.Header.Get("Content-Type")) return nil } func main() { batteryID := os.Args[1] res, err := get("/batteries/" + batteryID + "/passport") if err != nil { log.Fatal(err) } var passport struct { Status string `json:"status"` CurrentVersion *struct { VersionNo int `json:"version_no"` } `json:"current_version"` } err = json.NewDecoder(res.Body).Decode(&passport) res.Body.Close() if err != nil { log.Fatal(err) } if passport.CurrentVersion == nil { log.Fatalf("passport is %s: no published version yet", passport.Status) } version := fmt.Sprintf("/batteries/%s/passport/versions/%d", batteryID, passport.CurrentVersion.VersionNo) for _, path := range []string{ version + "/batterypass-v2", version + "/pdf?lang=de", "/batteries/" + batteryID + "/passport/qr?format=svg", } { if err := download(path); err != nil { log.Fatal(err) } } } ``` ## BatteryPass-JSON `exportPassportJson` liefert den Export, der bei der Veröffentlichung der Version erzeugt und gespeichert wurde, im Format des BatteryPass-Ready Data Model v2.0. Er enthält alle Zugriffsstufen einschließlich eingeschränkter Werte; behandeln Sie die Datei vertraulich. Ob er das JSON-Schema des Datenmodells erfüllt, ist ein Hinweis und blockiert keine Veröffentlichung: Die Versionsübersicht trägt `batterypass_v2_valid` und `batterypass_v2_issue_count`, [`getPassportVersion`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-version) den vollständigen Bericht in `batterypass_v2`. ## PDF `exportPassportPdf` erzeugt eine Version mit allen Stufen (eingeschränkte Zeilen mit ihrer Stufe gekennzeichnet), der Signatur und dem QR-Code. `lang` ist `de` (Standard) oder `en`. Wie das JSON enthält auch das PDF eingeschränkte Daten. ## QR-Code `getPassportQr` erzeugt den QR-Code zum `gs1_digital_link` der Einheit (Fehlerkorrektur M, Ruhezone von vier Modulen). - **format** `string` (query, Standard: png): `png` oder `svg`. SVG skaliert verlustfrei und eignet sich für den Druck. - **size** `integer` (query, Standard: 512): Breite und Höhe des PNG in Pixeln, 128 bis 2048. Bei SVG ohne Wirkung. Den QR-Code gibt es, sobald die Kennungen gesperrt sind, vor oder mit der ersten Veröffentlichung. Davor lautet die Antwort `409 conflict`. Der Link darin ändert sich danach nie mehr; ein gedrucktes Etikett bleibt also auch für spätere Versionen gültig. Für viele Einheiten auf einmal siehe das Rezept [QR-Etiketten für eine Charge](https://passport.kabin.sh/docs/de/recipes/qr-labels-for-a-batch). ## Einheiten als Tabelle `exportUnits` liefert die Einheiten, die zu den Filtern passen, mit den Spalten des Tabellenimports: Modell, Version, die Felder der Einheit und jedes Feld auf Einheitsebene, das einen Wert hat. Ein Mitglied kann die Datei bearbeiten und im Portal wieder importieren. - **format** `string` (query, Standard: xlsx): `xlsx` oder `csv`. CSV ist UTF-8 mit Byte Order Mark, durch Kommas getrennt, mit CRLF-Zeilenenden. - **battery_model_id** `uuid` (query): Nur Einheiten dieses Modells. - **model_version_id** `uuid` (query): Nur Einheiten dieser Modellversion. - **passport_status** `string` (query): Nur Einheiten, deren Pass diesen Status hat, z. B. `published`. - **serial_prefix** `string` (query): Nur Seriennummern, die mit diesem Text beginnen. - **import_id** `uuid` (query): Nur Einheiten aus diesem Import. ```bash title="cURL" curl -sS -OJ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ "$BATTERYPASS_URL/api/v1/batteries/export?format=csv&passport_status=published&serial_prefix=SV2027-" ``` Eine Datei fasst höchstens 10 000 Einheiten. Bei mehr lautet die Antwort `400 validation_error` auf `filter` (`out_of_range`): Schränken Sie die Filter ein, etwa nach Modellversion oder Seriennummernpräfix. In CSV-Dateien bekommt eine Zelle, die mit `=`, `+` oder `@` beginnt oder mit `-` beginnt und keine Zahl ist, ein vorangestelltes `'`, damit Tabellenprogramme sie nicht als Formel ausführen; der Import entfernt es wieder. ## Fehler | Status und `code` | Typische Ursache | | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | | `400 validation_error` | `size` außerhalb von 128 bis 2048; unbekanntes `format` oder `lang`; mehr als 10 000 Einheiten bei `exportUnits` | | `401 unauthenticated` | Schlüssel fehlt, ist unbekannt oder widerrufen | | `403 forbidden` | Dem Schlüssel fehlt der Scope `read` | | `404 not_found` | Unbekannte Einheit oder keine Version mit dieser Nummer | | `409 conflict` | `getPassportQr`, bevor die Kennungen gesperrt sind | | `429 rate_limited` | Zu viele Anfragen; `Retry-After` abwarten | --- # Einheiten in Ihr System übernehmen > Übernehmen Sie die Einheiten Ihrer Organisation mit Cursor-Paginierung in ERP, MES oder Datenbank, setzen Sie nach einer Unterbrechung fort und erkennen Sie Änderungen. [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries) liefert die Einheiten (einzelne Batterien) Ihrer Organisation Seite für Seite. Diese Anleitung liest alle, speichert den Cursor, damit ein unterbrochener Lauf weitermachen kann, und zeigt, wie Sie erkennen, welche Einheiten sich seit dem letzten Lauf geändert haben. Sie brauchen einen Schlüssel mit dem Scope `read`. > [!TIP] > Um Änderungen zu bemerken, ohne jede Einheit erneut zu lesen, fragen Sie den Änderungsfeed > [`listChanges`](https://passport.kabin.sh/docs/de/api-reference/changes/list-changes) ab; der Ablauf > [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp) zeigt, wie. ## Die Liste ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/batteries?limit=200&passport_status=published" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` ```json title="200 (gekürzt)" { "items": [ { "id": "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f", "serial_number": "SV2027-000184", "battery_model_id": "0192f0a1-6c2e-7d4b-9a10-3e5f7b2c8d41", "model_version_id": "0192f0a2-1b3c-7e5d-8f60-7a8b9c0d1e2f", "battery_status": "original", "passport_status": "published", "manufacturing_date": "2026-06-18", "facility_id": "0192f09e-4a5b-7c6d-9e8f-0a1b2c3d4e5f", "identifiers_locked_at": "2026-07-02T10:15:00Z", "successor_battery_ids": [], "row_version": 4, "created_at": "2026-06-20T08:00:12Z", "updated_at": "2026-07-02T10:15:00Z" } ], "next_cursor": "eyJTZXJpYWxOdW1iZXIiOiJTVjIwMjctMDAwMTg0IiwiSUQiOiIwMTkyYTNmMC01YjdjLTdkMjEtOGU0Zi02YTJiOWMxZDNlNWYifQ" } ``` Die Einheiten sind nach Seriennummer, dann ID sortiert. `limit` liegt zwischen 1 und 200 (Standard 50). `next_cursor` ist auf der letzten Seite `null`. Diese Filter schränken die Liste ein und lassen sich kombinieren: | Parameter | Nur Einheiten, | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | | `battery_model_id` | die zu diesem Batteriemodell gehören | | `model_version_id` | die zu dieser Modellversion gehören | | `passport_status` | deren Pass diesen Status hat: `draft`, `qr_locked`, `published`, `update_needed`, `suspended`, `withdrawn`, `deleted_recycled` | | `serial_prefix` | deren Seriennummer mit diesem Text beginnt (1 bis 50 Zeichen) | | `import_id` | die dieser Tabellenimport angelegt hat | Einen Filter nach Änderungsdatum gibt es nicht. Änderungen erkennen Sie auf Ihrer Seite, wie weiter unten beschrieben. ## Alle Seiten lesen Behalten Sie in einem Lauf für jede Seite dieselben Filter bei, und speichern Sie den Cursor erst, nachdem Sie die zugehörige Seite gespeichert haben. Ist der Lauf vollständig, löschen Sie den Cursor, damit der nächste Lauf von vorn beginnt. ```ts title="TypeScript" // sync-units.mts: npx tsx sync-units.mts import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }; const STATE = 'units.cursor'; // lets an interrupted pass continue where it stopped type Unit = { id: string; serial_number: string; passport_status: string; row_version: number }; // Placeholder: insert or update the unit in your database, keyed by id. function upsert(unit: Unit) { console.log(unit.id, unit.serial_number, unit.passport_status, unit.row_version); } let cursor: string | null = existsSync(STATE) ? readFileSync(STATE, 'utf8') : null; do { const query = new URLSearchParams({ limit: '200' }); if (cursor) query.set('cursor', cursor); const res = await fetch(`${BASE}/batteries?${query}`, { headers }); if (!res.ok) throw new Error(`listBatteries: ${res.status} ${(await res.json()).code}`); const page: { items: Unit[]; next_cursor: string | null } = await res.json(); page.items.forEach(upsert); cursor = page.next_cursor; if (cursor) writeFileSync(STATE, cursor); // only after the page is stored } while (cursor); rmSync(STATE, { force: true }); // pass complete: the next one starts at the beginning ``` ```python title="Python" # sync_units.py import os import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" STATE = "units.cursor" # lets an interrupted pass continue where it stopped session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_API_KEY"] def upsert(unit): """Placeholder: insert or update the unit in your database, keyed by id.""" print(unit["id"], unit["serial_number"], unit["passport_status"], unit["row_version"]) cursor = None if os.path.exists(STATE): with open(STATE) as f: cursor = f.read() while True: params = {"limit": 200, **({"cursor": cursor} if cursor else {})} res = session.get(f"{BASE}/batteries", params=params, timeout=30) res.raise_for_status() page = res.json() for unit in page["items"]: upsert(unit) cursor = page["next_cursor"] if not cursor: break with open(STATE, "w") as f: # only after the page is stored f.write(cursor) if os.path.exists(STATE): os.remove(STATE) # pass complete: the next one starts at the beginning ``` ```go title="Go" // sync-units: go run . package main import ( "encoding/json" "errors" "fmt" "io/fs" "log" "net/http" "net/url" "os" ) const state = "units.cursor" // lets an interrupted pass continue where it stopped type unit struct { ID string `json:"id"` SerialNumber string `json:"serial_number"` PassportStatus string `json:"passport_status"` RowVersion int64 `json:"row_version"` } // upsert is a placeholder: insert or update the unit in your database, keyed by id. func upsert(u unit) { fmt.Println(u.ID, u.SerialNumber, u.PassportStatus, u.RowVersion) } func main() { base, key := os.Getenv("BATTERYPASS_URL")+"/api/v1", os.Getenv("BATTERYPASS_API_KEY") saved, err := os.ReadFile(state) if err != nil && !errors.Is(err, fs.ErrNotExist) { log.Fatal(err) } cursor := string(saved) for { q := url.Values{"limit": {"200"}} if cursor != "" { q.Set("cursor", cursor) } req, _ := http.NewRequest(http.MethodGet, base+"/batteries?"+q.Encode(), nil) req.Header.Set("Authorization", "Bearer "+key) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } if res.StatusCode != http.StatusOK { log.Fatalf("listBatteries: %s", res.Status) } var page struct { Items []unit `json:"items"` NextCursor *string `json:"next_cursor"` } err = json.NewDecoder(res.Body).Decode(&page) res.Body.Close() if err != nil { log.Fatal(err) } for _, u := range page.Items { upsert(u) } if page.NextCursor == nil { break } cursor = *page.NextCursor if err := os.WriteFile(state, []byte(cursor), 0o600); err != nil { // only after the page is stored log.Fatal(err) } } _ = os.Remove(state) // pass complete: the next one starts at the beginning } ``` ## Was der Cursor ist und was nicht - Der Cursor ist undurchsichtig: Speichern Sie ihn unverändert und senden Sie ihn so zurück. Er läuft nicht ab. - Er markiert eine Position in der Reihenfolge der Seriennummern, keinen Zeitpunkt. Eine Einheit, die während eines Laufs angelegt wird und deren Seriennummer vor dem Cursor einsortiert ist, fehlt im Rest dieses Laufs; der nächste Lauf findet sie. Fragen Sie mit einem gespeicherten Cursor also nicht „was ist seit dem letzten Mal neu“: Jeder Lauf ist ein vollständiger Durchgang, und der Cursor lässt nur einen unterbrochenen Durchgang fortsetzen. - Lehnt BatteryPass einen gespeicherten Cursor ab (`400 validation_error` auf `cursor`), löschen Sie ihn und beginnen einen neuen Durchgang. [Paginierung](https://passport.kabin.sh/docs/de/pagination) beschreibt die Regeln für alle Listen. ## Änderungen erkennen Vergleichen Sie jede Einheit mit dem, was Sie im vorigen Lauf gespeichert haben: | Was sich geändert hat | Woran Sie es erkennen | | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Eine neue Einheit | Eine `id`, die Sie noch nicht gespeichert haben | | Die eigenen Daten der Einheit (Seriennummer, Datumsangaben, Standort, `battery_status`, Modellversion, gesperrte Kennungen) | `row_version` ist höher und `updated_at` später | | Der Passstatus, z. B. `published` → `update_needed` | `passport_status` weicht ab; `row_version` ändert sich dadurch nicht | | Eine neue Passversion | `current_version.version_no` von [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport) ist höher | | Werte der Einheit | `revision` und `recorded_at` je Feld in [`listBatteryValues`](https://passport.kabin.sh/docs/de/api-reference/values/list-battery-values) | Über die API werden Einheiten nie gelöscht; eine Batterie am Ende ihres Lebens zeigt das in `battery_status` und `passport_status`. Die Werte jeder Einheit zu lesen kostet eine Anfrage pro Einheit: Beschränken Sie das auf Einheiten, deren Status oder Version sich geändert hat, oder verteilen Sie es auf mehrere Läufe. Das Rezept [Data-Warehouse-Abgleich](https://passport.kabin.sh/docs/de/recipes/data-warehouse-sync) schreibt Einheiten und Werte als JSON Lines. ## Limits Alle Schlüssel Ihrer Organisation teilen sich 600 Anfragen pro Minute. Ein vollständiger Durchgang über 10 000 Einheiten mit `limit=200` braucht 50 Anfragen; die Werte jeder Einheit zu lesen kostet weitere 10 000. Auf `429 rate_limited` reagieren Sie, indem Sie `Retry-After` abwarten ([Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits), [Fehler und Wiederholungen](https://passport.kabin.sh/docs/de/guides/handle-errors)). --- # Fehler behandeln und Anfragen wiederholen > Entscheiden Sie, welche Antworten Sie wiederholen und welche Sie beheben, warten Sie mit Backoff und Jitter, beachten Sie Retry-After und protokollieren Sie die request_id. Jeder Fehler der API ist ein Problem nach RFC 9457 (`application/problem+json`) mit einem stabilen `code` und einer `request_id`. Manche Fehler verschwinden, wenn Sie warten und es erneut versuchen; die meisten verlangen eine Änderung auf Ihrer Seite. Diese Anleitung ordnet sie und zeigt eine kleine Hilfsfunktion, die die richtigen wiederholt. Die Codes stehen unter [Fehler](https://passport.kabin.sh/docs/de/errors). ```json title="429" { "type": "about:blank", "title": "Too Many Requests", "status": 429, "code": "rate_limited", "detail": "Too many attempts. Try again later.", "request_id": "8b1e6f03c2d94a7e5f0a1b2c" } ``` Die `request_id` steht auch im Header `X-Request-Id` jeder Antwort, auch der erfolgreichen. ## Wiederholen oder beheben? | Antwort | Wiederholen? | Was zu tun ist | | ----------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `429 rate_limited` | Ja, immer | So viele Sekunden warten, wie `Retry-After` angibt. Die Anfrage wurde abgelehnt, bevor sie etwas bewirkt hat. | | `500 internal_error`, `502`, `503`, `504` | `GET` und `updateCondition`: ja, mit Backoff. `createLifecycleEvent`: nicht automatisch | Warten und erneut versuchen; hält der Fehler an, den Support mit der `request_id` kontaktieren. | | Netzwerkfehler oder Timeout | Wie bei `5xx` | Sie wissen nicht, ob die Anfrage angekommen ist. | | `400 validation_error` | Nein | Anfrage korrigieren; `errors[]` nennt jedes Feld (`field`, `code`, `message`). | | `401 unauthenticated` | Nein | Der Schlüssel fehlt, ist unbekannt oder widerrufen. Einen Menschen alarmieren, statt in einer Schleife zu wiederholen. | | `403 forbidden` | Nein | Dem Schlüssel fehlt der Scope der Operation (`read`, `condition` oder `import`). | | `404 not_found` | Nein | Falsche ID, oder die Ressource gehört zu einer anderen Organisation. | | `409 conflict` | Nein | Die Ressource ist in einem Zustand, der das nicht erlaubt, z. B. ein QR-Code, bevor die Kennungen gesperrt sind, oder Zustandsdaten für eine Einheit eines archivierten Modells. | Warum die Methoden sich unterscheiden: `GET` ändert nichts, und `updateCondition` überspringt eine Messung, die schon gespeichert ist; denselben Body erneut zu senden schadet also nicht. `createLifecycleEvent` erfasst bei jedem erfolgreichen Aufruf ein neues Ereignis; prüfen Sie nach einem Timeout [`listLifecycleEvents`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events), bevor Sie erneut senden. Einzelheiten unter [Wiederholungen und Idempotenz](https://passport.kabin.sh/docs/de/idempotency). > [!WARNING] > Wiederholen Sie `401` nicht in einer Schleife. Unbekannte und widerrufene Schlüssel sind auf 30 Versuche pro > 15 Minuten und Client-Adresse begrenzt; ein falsch konfigurierter Job bekommt also bald `429` statt `401`, und die > eigentliche Ursache gerät aus dem Blick. ## Backoff mit Jitter Wenn sich eine Wiederholung lohnt: 1. Ist `Retry-After` gesetzt, warten Sie so viele Sekunden. 2. Andernfalls warten Sie eine zufällige Zeit zwischen 0 und `min(30 s, 0.5 s × 2^attempt)` („Full Jitter“), damit viele Clients nicht im Gleichtakt wiederholen. 3. Geben Sie nach wenigen Versuchen auf (fünf reichen) und melden Sie den Fehler mit seiner `request_id`. 4. Setzen Sie ein Client-Timeout (zum Beispiel 30 Sekunden), damit eine hängende Verbindung als Netzwerkfehler zählt. In Batch-Jobs bedeutet ein `429`, dass die ganze Organisation über ihrem Limit von 600 Anfragen pro Minute liegt (alle Schlüssel zusammen). Bremsen Sie den Job, statt nur die einzelne Anfrage zu wiederholen; siehe [Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits). ## Eine Hilfsfunktion für jeden Aufruf Die Hilfsfunktion sendet eine Anfrage, gibt das geparste JSON zurück, wiederholt, was sich zu wiederholen lohnt, und wirft einen Fehler, der das Problem enthält. Jede Fassung läuft für sich; die letzten Zeilen zeigen die Verwendung. ```ts title="TypeScript" // batterypass.mts: npx tsx batterypass.mts const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const KEY = process.env.BATTERYPASS_API_KEY; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export class ApiError extends Error { status: number; problem: { code?: string; detail?: string; request_id?: string; errors?: unknown[] }; constructor(status: number, problem: ApiError['problem']) { super(`${status} ${problem.code}: ${problem.detail} (request_id ${problem.request_id})`); this.status = status; this.problem = problem; } } /** Sends a request; retries 429 always, and 5xx or network errors unless the method is POST. */ export async function call(method: string, path: string, body?: unknown, maxAttempts = 5): Promise { for (let attempt = 1; ; attempt++) { let res: Response | undefined; try { res = await fetch(BASE + path, { method, headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(30_000), }); } catch (err) { if (method === 'POST' || attempt === maxAttempts) throw err; // network error or timeout } if (res?.ok) return (await res.json()) as T; const retryable = !res || res.status === 429 || (res.status >= 500 && method !== 'POST'); if (res && (!retryable || attempt === maxAttempts)) { const requestId = res.headers.get('X-Request-Id') ?? undefined; const problem = await res.json().catch(() => ({ request_id: requestId })); throw new ApiError(res.status, problem); } // Retry-After (seconds) wins; otherwise exponential backoff with full jitter, at most 30 s. const retryAfter = Number(res?.headers.get('Retry-After')); await sleep(retryAfter > 0 ? retryAfter * 1000 : Math.random() * Math.min(30_000, 500 * 2 ** attempt)); } } // Usage try { const page = await call<{ items: { serial_number: string }[] }>('GET', '/batteries?limit=10'); console.log(page.items.map((u) => u.serial_number)); } catch (err) { if (err instanceof ApiError) console.error(err.message); // log the request_id, never the key else throw err; } ``` ```python title="Python" # batterypass.py import os import random import time import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_API_KEY"] class ApiError(Exception): def __init__(self, status, problem): super().__init__(f"{status} {problem.get('code')}: {problem.get('detail')} (request_id {problem.get('request_id')})") self.status, self.problem = status, problem def call(method, path, body=None, max_attempts=5): """Sends a request; retries 429 always, and 5xx or network errors unless the method is POST.""" for attempt in range(1, max_attempts + 1): try: res = session.request(method, BASE + path, json=body, timeout=30) except (requests.ConnectionError, requests.Timeout): # network error or timeout if method == "POST" or attempt == max_attempts: raise res = None if res is not None and res.ok: return res.json() retryable = res is None or res.status_code == 429 or (res.status_code >= 500 and method != "POST") if res is not None and (not retryable or attempt == max_attempts): try: problem = res.json() except ValueError: problem = {"request_id": res.headers.get("X-Request-Id")} raise ApiError(res.status_code, problem) # Retry-After (seconds) wins; otherwise exponential backoff with full jitter, at most 30 s. retry_after = res.headers.get("Retry-After") if res is not None else None time.sleep(int(retry_after) if retry_after else random.uniform(0, min(30, 0.5 * 2**attempt))) # Usage try: page = call("GET", "/batteries?limit=10") print([unit["serial_number"] for unit in page["items"]]) except ApiError as err: print(err) # log the request_id, never the key ``` ```go title="Go" // batterypass: go run . package main import ( "bytes" "encoding/json" "fmt" "io" "log" "math/rand/v2" "net/http" "os" "strconv" "time" ) var ( base = os.Getenv("BATTERYPASS_URL") + "/api/v1" key = os.Getenv("BATTERYPASS_API_KEY") client = &http.Client{Timeout: 30 * time.Second} ) // Problem is an RFC 9457 error response. type Problem struct { Status int `json:"status"` Code string `json:"code"` Detail string `json:"detail"` RequestID string `json:"request_id"` } func (p *Problem) Error() string { return fmt.Sprintf("%d %s: %s (request_id %s)", p.Status, p.Code, p.Detail, p.RequestID) } // call sends a request and decodes the response into out; it retries 429 always, and 5xx or // network errors unless the method is POST. func call(method, path string, body, out any) error { var payload []byte if body != nil { var err error if payload, err = json.Marshal(body); err != nil { return err } } const maxAttempts = 5 for attempt := 1; ; attempt++ { req, err := http.NewRequest(method, base+path, bytes.NewReader(payload)) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+key) req.Header.Set("Content-Type", "application/json") res, err := client.Do(req) if err != nil { if method == http.MethodPost || attempt == maxAttempts { return err // network error or timeout } } else { data, _ := io.ReadAll(res.Body) res.Body.Close() if res.StatusCode < 300 { return json.Unmarshal(data, out) } retryable := res.StatusCode == 429 || (res.StatusCode >= 500 && method != http.MethodPost) if !retryable || attempt == maxAttempts { p := &Problem{Status: res.StatusCode, RequestID: res.Header.Get("X-Request-Id")} _ = json.Unmarshal(data, p) return p } // Retry-After (seconds) wins. if s, _ := strconv.Atoi(res.Header.Get("Retry-After")); s > 0 { time.Sleep(time.Duration(s) * time.Second) continue } } // Exponential backoff with full jitter, at most 30 s. backoff := min(30*time.Second, 500*time.Millisecond< # QR-Code oder GS1 Digital Link auflösen > Vom gescannten GS1 Digital Link zum öffentlichen Pass-JSON, über den Resolver oder über GTIN und Seriennummer. Der QR-Code auf einer Batterie enthält einen GS1 Digital Link wie `https://batterypass.example.com/01/04012345000023/21/SV2027-000184`. Ein Browser, der ihn öffnet, landet auf der öffentlichen Passseite. Eine Scanner-App, das System eines Recyclers oder ein Marktplatz will dagegen meist die Daten: Diese Anleitung holt das öffentliche Pass-JSON über den Link. Ein Schlüssel ist nicht nötig. Den Hintergrund erklärt [GS1 Digital Link und Resolver](https://passport.kabin.sh/docs/de/concepts/digital-link). ## Drei Wege zum JSON | Weg | Anfrage | Antwort | | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | Dem Link folgen (empfohlen) | Der Link mit `?linkType=describedby` | `307` zum öffentlichen JSON | | Das Linkset abfragen | Der Link mit `?linkType=all` oder mit `Accept: application/json` ohne `linkType` | `200` mit `application/linkset+json`; der Eintrag `describedby` ist das JSON | | Über GTIN und Seriennummer nachschlagen | [`getPublicPassportByProductId`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-by-product-id) | `200` mit dem öffentlichen JSON | Dem Link zu folgen funktioniert für jede BatteryPass-Installation und jeden Resolver-Hostnamen, denn der Link sagt selbst, wohin es geht. Zum Nachschlagen müssen Sie die BatteryPass-Adresse kennen, die den Pass führt. ## Schritt für Schritt 1. **Link zerlegen** Ein Digital Link von BatteryPass hat den Pfad `/01/{gtin}/21/{serial}`: den Application Identifier `01` mit der GTIN des Modells (14 Ziffern) und `21` mit der Seriennummer der Einheit. Seriennummern bestehen aus Buchstaben, Ziffern, `.`, `-` und `/`; ein `/` reist prozentkodiert als `%2F`. Dekodieren Sie das Segment deshalb erst nach dem Zerlegen des Pfads. Übernehmen Sie den Ursprung des Links unverändert: Er kann ein eigener Resolver-Hostname sein und nicht die Adresse der API. 2. **JSON anfordern** Hängen Sie `linkType=describedby` an und senden Sie `Accept: application/json`, damit ein unbekannter Pass als JSON-Problem statt als HTML-Seite zurückkommt: ```bash title="cURL" curl -sS -i -H "Accept: application/json" \ "https://batterypass.example.com/01/04012345000023/21/SV2027-000184?linkType=describedby" ``` ```http title="307" HTTP/1.1 307 Temporary Redirect Location: https://batterypass.example.com/api/v1/public/passports/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10 Link: ; rel="https://gs1.org/voc/pip"; type="text/html", ; rel="describedby"; type="application/json", ; rel="linkset"; type="application/linkset+json" Cache-Control: public, max-age=300 Vary: Accept ``` HTTP-Clients folgen dem `307` von selbst (`curl -L`, `fetch`, `requests`, `http.Client` in Go). Das Ziel ist [`getPublicPassport`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport). 3. **Pass lesen** Das JSON enthält `passport_id` (`urn:dpp:{uuid}`), `status`, `links` und, solange der Pass öffentlich ist, das signierte `document` mit seiner `verification`. Ein Pass mit gesperrten Kennungen, der noch nicht veröffentlicht ist (`qr_locked`), oder einer im Status `suspended`, `withdrawn` oder `deleted_recycled` lässt sich weiterhin auflösen, sein JSON enthält aber nur den Status, ohne `document`. Ob die Daten echt sind, prüfen Sie mit der Signatur, wie in [Passsignatur offline prüfen](https://passport.kabin.sh/docs/de/guides/verify-signature) gezeigt. ## Vollständiger Code Jedes Programm erwartet den gescannten Link als Argument und gibt GTIN, Seriennummer, Pass-ID, Status und die URL des JSON aus. ```ts title="TypeScript" // resolve.mts: npx tsx resolve.mts const link = new URL(process.argv[2]); // e.g. https://batterypass.example.com/01/04012345000023/21/SV2027-000184 const match = /\/01\/(\d{8,14})\/21\/([^/]+)$/.exec(link.pathname); if (!match) throw new Error('not a GS1 Digital Link with GTIN and serial number'); const gtin = match[1]; const serial = decodeURIComponent(match[2]); // a "/" in the serial number arrives as %2F // linkType=describedby: the resolver answers 307 with the public JSON as Location; fetch follows it. link.searchParams.set('linkType', 'describedby'); const res = await fetch(link, { headers: { Accept: 'application/json' } }); const body = await res.json(); if (!res.ok) throw new Error(`${res.status} ${body.code}: ${body.detail}`); console.log(gtin, serial, body.passport_id, body.status, res.url); ``` ```python title="Python" # resolve.py: python resolve.py import re import sys from urllib.parse import unquote, urlsplit import requests link = sys.argv[1] # e.g. https://batterypass.example.com/01/04012345000023/21/SV2027-000184 match = re.search(r"/01/(\d{8,14})/21/([^/]+)$", urlsplit(link).path) if not match: sys.exit("not a GS1 Digital Link with GTIN and serial number") gtin, serial = match.group(1), unquote(match.group(2)) # a "/" in the serial number arrives as %2F # linkType=describedby: the resolver answers 307 with the public JSON as Location; requests follows it. res = requests.get(link, params={"linkType": "describedby"}, headers={"Accept": "application/json"}, timeout=10) body = res.json() if not res.ok: sys.exit(f"{res.status_code} {body['code']}: {body.get('detail')}") print(gtin, serial, body["passport_id"], body["status"], res.url) ``` ```go title="Go" // resolve: go run . package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" "regexp" ) var digitalLink = regexp.MustCompile(`/01/(\d{8,14})/21/([^/]+)$`) func main() { link, err := url.Parse(os.Args[1]) // e.g. https://batterypass.example.com/01/04012345000023/21/SV2027-000184 if err != nil { log.Fatal(err) } m := digitalLink.FindStringSubmatch(link.EscapedPath()) if m == nil { log.Fatal("not a GS1 Digital Link with GTIN and serial number") } gtin := m[1] serial, err := url.PathUnescape(m[2]) // a "/" in the serial number arrives as %2F if err != nil { log.Fatal(err) } // linkType=describedby: the resolver answers 307 with the public JSON as Location; the client follows it. q := link.Query() q.Set("linkType", "describedby") link.RawQuery = q.Encode() req, err := http.NewRequest(http.MethodGet, link.String(), nil) if err != nil { log.Fatal(err) } req.Header.Set("Accept", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() var body struct { PassportID string `json:"passport_id"` Status string `json:"status"` Code string `json:"code"` Detail string `json:"detail"` } if err := json.NewDecoder(res.Body).Decode(&body); err != nil { log.Fatal(err) } if res.StatusCode != http.StatusOK { log.Fatalf("%s %s: %s", res.Status, body.Code, body.Detail) } fmt.Println(gtin, serial, body.PassportID, body.Status, res.Request.URL) } ``` ## Das Linkset Mit `?linkType=all` oder mit `Accept: application/json` (auch `application/linkset+json` oder `application/ld+json`) ohne `linkType` antwortet der Resolver mit `200` und allen Links des Passes: ```json title="200" { "linkset": [ { "anchor": "https://batterypass.example.com/01/04012345000023/21/SV2027-000184", "https://gs1.org/voc/pip": [ { "href": "https://batterypass.example.com/pass/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10", "type": "text/html", "title": "Battery passport" } ], "describedby": [ { "href": "https://batterypass.example.com/api/v1/public/passports/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10", "type": "application/json", "title": "Public passport (JSON)" } ] } ] } ``` ## Über GTIN und Seriennummer nachschlagen Kennen Sie die BatteryPass-Adresse und haben GTIN und Seriennummer aus einer anderen Quelle (Etikett, Lieferschein, ERP-Datensatz), nutzen Sie [`getPublicPassportByProductId`](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-by-product-id). Der Pfadparameter lautet `{gtin}:{serial}`. Die GTIN darf 8, 12, 13 oder 14 Ziffern haben und braucht eine gültige Prüfziffer; die Seriennummer senden Sie URL-kodiert (Prozentkodierung). ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/public/dpps/by-product-id/04012345000023:SV2027-000184" ``` Die Antwort ist dasselbe öffentliche JSON wie oben. Ein fehlerhafter Wert ergibt `400 validation_error` auf `productId` (`invalid_format`), ein unbekanntes Paar `404 not_found`. ## Fehler und Limits | Antwort | Bedeutung | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `404` vom Resolver | Kein Pass für diese GTIN und Seriennummer: ein Problem `not_found` für JSON-Clients, eine kurze Seite für Browser. | | `200` ohne `document` | Den Pass gibt es, aber er ist gerade nicht öffentlich; `status` nennt den Grund. | | `400 validation_error` | `getPublicPassportByProductId` mit einem Wert, der nicht `{gtin}:{serial}` mit gültiger GTIN ist. | | `429 rate_limited` | Mehr als 300 öffentliche Anfragen pro Minute von Ihrer Adresse (Resolver und öffentliche Abrufe zusammen); `Retry-After` abwarten. | Antworten des Resolvers und das aktuelle öffentliche JSON dürfen fünf Minuten zwischengespeichert werden. Wenn Sie viele Codes scannen, speichern Sie die Ergebnisse auch auf Ihrer Seite zwischen. --- # API-Schlüssel sicher verwalten > Wo Schlüssel hingehören, welcher Scope für welches System, wie Sie ohne Ausfall wechseln und was zu tun ist, wenn ein Schlüssel bekannt wird. Ein API-Schlüssel (`bp_live_…`) gehört Ihrer Organisation, nicht einer Person. Wer ihn hat, liest alles, was ein Betrachter Ihrer Organisation lesen darf, einschließlich eingeschränkter Passdaten und Exporte, und meldet mit dem Scope `condition` Messwerte und Ereignisse im Namen Ihrer Organisation. Behandeln Sie jeden Schlüssel wie ein Passwort. Wie Schlüssel funktionieren, beschreibt [Authentifizierung](https://passport.kabin.sh/docs/de/authentication); den größeren Rahmen [Sicherheit und Datenschutz](https://passport.kabin.sh/docs/de/general/security). Eigentümer erstellen und widerrufen Schlüssel im Portal unter **Einstellungen → API-Schlüssel**. Eine API dafür gibt es nicht, und das Secret eines Schlüssels wird nur einmal angezeigt, beim Erstellen. ## Regeln | Regel | Warum | | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | | Schlüssel in einem Secrets Manager (oder dem Secret-Speicher Ihrer Plattform) halten und dem Prozess als Umgebungsvariable übergeben | So bleiben sie aus Code, Images und Backups heraus | | Nie in Quellcode, Repository, Container-Image, URL, Logzeile, Ticket oder Chat | All das wird kopiert, indiziert oder jahrelang aufbewahrt | | Nie an einen Browser, eine mobile App oder ein Gerät im Feld ausliefern | Alles auf einem Client lässt sich auslesen; öffentliche Passabrufe brauchen ohnehin keinen Schlüssel | | Schlüssel nur im Header `Authorization: Bearer` senden, nur über HTTPS | Header landen nicht wie URLs in Zugriffsprotokollen und Browserverläufen | | Ein Schlüssel je System | Sie können ein System widerrufen, ohne die anderen anzuhalten, und das Portal zeigt für jeden Schlüssel die letzte Nutzung | | Der kleinste Scope, der genügt | Ein bekannt gewordener Leseschlüssel kann keine Daten melden; ein Schlüssel für Zustandsdaten kann Ihre Daten nicht lesen | ## Welcher Scope für welches System | System | Scope | | --------------------------------------------------------------------- | ----------------------------------------------------- | | ERP oder MES, das Einheiten und Pässe übernimmt | `read` | | Data Warehouse oder Reporting | `read` | | Batteriemanagementsystem oder Gateway, das Messwerte sendet | `condition` | | System eines Servicepartners, das Inspektionen und Reparaturen meldet | `condition` | | Ein Werkzeug, das vor dem Senden die Zustandsfelder nachschlägt | `read` und `condition`, oder zwei getrennte Schlüssel | | Middleware, die Einheiten aus der Fertigung als Import bereitstellt | `import` (Mitglieder übernehmen und geben frei) | Ein Schlüssel ist nicht auf bestimmte Modelle oder Einheiten beschränkt: Ein Schlüssel mit `condition`, den Sie einem Servicepartner geben, kann für jede Einheit Ihrer Organisation melden. Geben Sie ihn nur Partnern, denen Sie das zutrauen. ## Den Schlüssel beim Start laden Lesen Sie den Schlüssel einmal aus der Umgebung, brechen Sie früh ab, wenn er fehlt, und geben Sie ihn nie aus: ```bash title="Shell" # In a systemd unit, a CI secret or your orchestrator's secret store, not in a script under version control export BATTERYPASS_API_KEY="$(cat /run/secrets/batterypass_api_key)" ``` ```ts title="TypeScript" const key = process.env.BATTERYPASS_API_KEY; if (!key?.startsWith('bp_live_')) throw new Error('BATTERYPASS_API_KEY is missing or malformed'); const headers = { Authorization: `Bearer ${key}` }; // pass this object along; never log it ``` ```python title="Python" import os import sys key = os.environ.get("BATTERYPASS_API_KEY", "") if not key.startswith("bp_live_"): sys.exit("BATTERYPASS_API_KEY is missing or malformed") headers = {"Authorization": f"Bearer {key}"} # pass this dict along; never log it ``` ```go title="Go" package main import ( "log" "net/http" "os" "strings" ) func main() { key := os.Getenv("BATTERYPASS_API_KEY") if !strings.HasPrefix(key, "bp_live_") { log.Fatal("BATTERYPASS_API_KEY is missing or malformed") } req, _ := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1/registry", nil) req.Header.Set("Authorization", "Bearer "+key) // never log req.Header res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } res.Body.Close() log.Println("getRegistryInfo:", res.Status) } ``` Wenn Ihr HTTP-Client oder Proxy Anfragen protokolliert, sorgen Sie dafür, dass er den Header `Authorization` maskiert. ## Ohne Ausfall wechseln Schlüssel laufen nicht ab. Wechseln Sie sie in einem Rhythmus, den Sie festlegen, und immer dann, wenn jemand geht, der einen Schlüssel kannte. Eine Organisation kann 25 aktive Schlüssel haben; alter und neuer Schlüssel passen während des Wechsels also nebeneinander. 1. **Neuen Schlüssel erstellen** Eine Eigentümerin oder ein Eigentümer erstellt einen Schlüssel mit denselben Scopes und einem Namen, der die beiden unterscheidet, etwa `ERP-Abgleich 2026-10`, und legt das Secret direkt im Secrets Manager ab. 2. **Ausrollen** Stellen Sie das System auf das neue Secret um und starten oder laden Sie es neu. Beide Schlüssel funktionieren gleichzeitig; dazwischen schlägt also nichts fehl. 3. **Prüfen, dass der neue Schlüssel genutzt wird** Unter **Einstellungen → API-Schlüssel** zeigt der neue Schlüssel eine aktuelle Angabe **Zuletzt genutzt**, und die des alten ändert sich nicht mehr. Die Angabe wird höchstens einmal pro Minute aktualisiert. 4. **Alten Schlüssel widerrufen** Widerrufen Sie den alten Schlüssel. Der Widerruf gilt sofort und endgültig; der Schlüssel bleibt als widerrufen in der Liste. ## Wenn ein Schlüssel bekannt wird Ein Schlüssel gilt als bekannt geworden, sobald er irgendwo außerhalb Ihres Secrets Managers aufgetaucht ist: in einem Commit, einem Log, einem Screenshot, einem Supportticket, den Notizen einer ehemaligen Mitarbeiterin. 1. **Sofort widerrufen** Eine Eigentümerin oder ein Eigentümer widerruft den Schlüssel unter **Einstellungen → API-Schlüssel**. Ab diesem Moment ist jede Anfrage damit `401 unauthenticated`. Tun Sie das zuerst, bevor Sie wissen, ob der Schlüssel missbraucht wurde. 2. **Ersetzen** Erstellen Sie einen neuen Schlüssel mit denselben Scopes und rollen Sie ihn aus wie beim Wechsel oben. Bis dahin steht das System, das den alten Schlüssel nutzte; das ist der Preis dafür, zuerst zu widerrufen. 3. **Prüfen, was geschehen ist** Schreibzugriffe mit einem Schlüssel werden mit dem Schlüssel als Akteur protokolliert: Sehen Sie im Änderungsprotokoll des Portals nach und in Lebenszyklus-Ereignissen mit `source: api` und dem Namen des Schlüssels in `api_key`. Lesezugriffe werden nicht protokolliert; gehen Sie also davon aus, dass alles gelesen wurde, was der Schlüssel lesen konnte. 4. **Die Kopie entfernen** Löschen Sie den Schlüssel dort, wo er bekannt wurde (Commit umschreiben, Log bereinigen, Ticket bearbeiten), auch wenn er nicht mehr funktioniert, damit Scanner ihn nicht weiter melden. > [!TIP] > **Schlüssel finden, bevor andere es tun** > > Secrets haben eine feste Form: `bp_live_` gefolgt von 43 base64url-Zeichen (Buchstaben, Ziffern, `-` und `_`). > Nehmen Sie das Muster `bp_live_[A-Za-z0-9_-]{43}` in Ihren Secret-Scanner und Ihre Pre-Commit-Hooks auf. Das > Portal zeigt die ersten 12 Zeichen jedes Schlüssels; das genügt, um einen Fund einem Schlüssel zuzuordnen, ohne das > Secret offenzulegen. Alle Schlüssel Ihrer Organisation teilen sich ein Limit von 600 Anfragen pro Minute. Ein bekannt gewordener, noch aktiver Schlüssel kann deshalb auch Ihre regulären Integrationen ausbremsen ([Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits)). --- # KI-Werkzeuge über MCP anbinden > Nutzen Sie die Integrations-API von BatteryPass aus Claude Code, Cursor, VS Code und anderen MCP-Clients mit einem API-Schlüssel Ihrer Organisation. BatteryPass betreibt einen **Model-Context-Protocol-Server (MCP)** unter `https:///mcp`. Ein KI-Werkzeug, das MCP spricht, verbindet sich mit einem API-Schlüssel Ihrer Organisation und erhält für jede Operation der Integrations-API ein Werkzeug (Tool): Es kann Batteriemodelle, Einheiten, Werte und deren Vollständigkeit abfragen, Pässe und ihre Versionen lesen, dem Änderungsfeed folgen, BatteryPass-JSON, PDFs und QR-Codes exportieren und, mit der passenden Berechtigung, Zustandsdaten und Lebenszyklusereignisse melden oder Tabellenimporte zuordnen und prüfen. Nutzen Sie ihn, wenn ein Assistent Fragen zu Ihren Daten beantworten soll („Bei welchen Einheiten von Modell X fehlen noch Angaben?“) oder Zustandsmeldungen für Sie vorbereiten und senden soll. Für zeitgesteuerte Integrationen ohne Aufsicht rufen Sie die REST-API direkt auf; die Werkzeuge sind dieselben Operationen. > [!NOTE] > Der MCP-Server gehört zu jeder BatteryPass-Installation und braucht keinen zusätzlichen Dienst. Er nutzt den > Transport „Streamable HTTP“ (zustandslos, Protokollversion 2026-07-28; ältere Clients mit dem > `initialize`-Handshake funktionieren ebenfalls). ## Bevor Sie beginnen 1. **API-Schlüssel erstellen** Ein Eigentümer Ihrer Organisation öffnet **Einstellungen → API-Schlüssel** und erstellt einen Schlüssel für genau dieses Werkzeug. Wählen Sie die Berechtigungen, die das Werkzeug braucht: `read` zum Lesen, `condition` zum Melden von Zustandsdaten und Lebenszyklusereignissen, `import` zum Zuordnen, Prüfen und Lesen von Tabellenimporten. Die Seite zeigt auch die folgenden Konfigurationen mit der Adresse Ihres Servers. 2. **Schlüssel als Umgebungsvariable bereitstellen** Der Schlüssel wird nur einmal angezeigt: Bewahren Sie ihn in Ihrem Passwortmanager auf. Die Konfigurationen unten lesen ihn beim Start des Werkzeugs aus der Umgebungsvariable `BATTERYPASS_API_KEY`. Setzen Sie die Variable in der Umgebung, aus der Ihr Werkzeug startet, zum Beispiel in der aktuellen Shell: ```bash title="macOS und Linux" export BATTERYPASS_API_KEY="bp_live_…" ``` ```powershell title="Windows (PowerShell)" $env:BATTERYPASS_API_KEY = "bp_live_…" ``` 3. **Server im Werkzeug eintragen** Folgen Sie unten dem Reiter Ihres Werkzeugs und lassen Sie sich danach die BatteryPass-Werkzeuge auflisten. ## Werkzeug einrichten **Claude Code** Tragen Sie den Server in die Projektdatei `.mcp.json` ein. Claude Code ersetzt `${BATTERYPASS_API_KEY}` bei jedem Start aus der Umgebung; so steht der Schlüssel weder in dieser Datei noch in der Konfiguration von Claude Code, und Sie können die Datei mit Ihrem Team teilen: ```json title=".mcp.json" { "mcpServers": { "batterypass": { "type": "http", "url": "https://passport.example.com/mcp", "headers": { "Authorization": "Bearer ${BATTERYPASS_API_KEY}" } } } } ``` Um den Server stattdessen für Ihren Benutzer in allen Projekten einzutragen, nutzen Sie die Kommandozeile mit `--scope user` (ohne diese Angabe gilt der Server nur für das aktuelle Projekt auf Ihrem Rechner): ```bash title="Terminal" claude mcp add --transport http --scope user batterypass https://passport.example.com/mcp \ --header "Authorization: Bearer bp_live_…" ``` > [!WARNING] > Die Kommandozeile speichert den Header samt Schlüssel im Klartext in der Konfiguration von Claude Code > (`~/.claude.json`), und Ihre Shell behält den Befehl womöglich im Verlauf. `$BATTERYPASS_API_KEY` im Befehl > ändert daran nichts: Die Shell ersetzt die Variable, bevor Claude Code sie sieht (und PowerShell braucht > `$env:BATTERYPASS_API_KEY`). Nutzen Sie `.mcp.json`, damit der Schlüssel in keiner Datei steht. Prüfen Sie die Verbindung mit `claude mcp list` oder mit `/mcp` in Claude Code. **Cursor** ```json title=".cursor/mcp.json" { "mcpServers": { "batterypass": { "url": "https://passport.example.com/mcp", "headers": { "Authorization": "Bearer ${env:BATTERYPASS_API_KEY}" } } } } ``` Starten Sie Cursor neu, nachdem Sie die Umgebungsvariable gesetzt haben; der Server erscheint dann unter **Settings → MCP**. **VS Code** VS Code fragt beim ersten Start des Servers nach dem Schlüssel und legt ihn in seinem Geheimnisspeicher ab: ```json title=".vscode/mcp.json" { "inputs": [ { "type": "promptString", "id": "batterypass-api-key", "description": "BatteryPass API key", "password": true } ], "servers": { "batterypass": { "type": "http", "url": "https://passport.example.com/mcp", "headers": { "Authorization": "Bearer ${input:batterypass-api-key}" } } } } ``` **Andere Clients** Ein Client, der nur lokale Server (stdio) startet, erreicht BatteryPass über die Brücke `mcp-remote`. Legen Sie den ganzen Header in eine Umgebungsvariable, weil manche Clients Argumente an Leerzeichen trennen: ```json title="Client-Konfiguration" { "mcpServers": { "batterypass": { "command": "npx", "args": ["-y", "mcp-remote@0.15.2", "https://passport.example.com/mcp", "--header", "Authorization:${AUTH_HEADER}"], "env": { "AUTH_HEADER": "Bearer bp_live_…" } } } } ``` > [!NOTE] > Connectoren in claude.ai, Claude Desktop und ChatGPT, die sich nur per OAuth anmelden können, lassen sich noch nicht > verbinden: Der MCP-Server akzeptiert ausschließlich API-Schlüssel. Nutzen Sie stattdessen Claude Code, Cursor, > VS Code oder `mcp-remote`. ## Die Werkzeuge Jede Operation der veröffentlichten Integrations-API ist ein Werkzeug, außer dem Datei-Upload `createImport`. Sein Name ist die `operationId` der Operation in snake_case: Aus `listBatteryModels` in der API-Referenz wird das Werkzeug `list_battery_models`. Die Argumente eines Werkzeugs sind die Pfad- und Query-Parameter der Operation unter ihren Namen und der JSON-Body unter `body`; die Beschreibungen stammen aus der API-Referenz und nennen die nötige Berechtigung. | Berechtigung | Werkzeuge | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `read` | Katalog: `list_battery_models`, `get_battery_model`, `list_model_versions`, `get_model_version`, `list_batteries`, `get_battery`, `export_units`, `list_facilities` | | `read` | Werte und Vollständigkeit: `list_model_version_values`, `list_model_version_value_history`, `get_model_version_completeness`, `list_battery_values`, `list_battery_value_history`, `get_battery_completeness` | | `read` | Pässe: `get_passport`, `list_passport_versions`, `get_passport_version`, `get_passport_version_diff`, `export_passport_json`, `export_passport_pdf`, `get_passport_qr`, `get_registry_submission_status` | | `read` | Lebenszyklus, Dokumente und Register: `list_lifecycle_events`, `get_condition`, `list_evidence`, `get_evidence`, `download_evidence`, `get_registry_info`, `get_registry_schema`, `get_registry_status` | | `read` | Änderungsfeed: `list_changes` | | `condition` | `update_condition` (gemessene Zustandsdaten melden), `create_lifecycle_event` | | `import` | `list_imports`, `get_import`, `update_import_mapping`, `validate_import` | | keine (öffentlich) | `get_public_passport`, `get_public_passport_version`, `get_public_passport_by_product_id`, `verify_passport`, `download_public_passport_document` | Ein Werkzeug sieht nur die Daten der Organisation, der der Schlüssel gehört, genau wie eine REST-Anfrage mit diesem Schlüssel. Ihr Werkzeug listet nur die Werkzeuge der Berechtigungen des Schlüssels auf; öffentliche Passabfragen stehen jedem Schlüssel zur Verfügung. Ein Werkzeug kann keine Datei übertragen. Eine Tabelle laden Sie daher mit demselben Schlüssel über REST mit [`createImport`](https://passport.kabin.sh/docs/de/api-reference/imports/create-import) hoch; danach ordnet der Assistent den Import mit den Werkzeugen zu und prüft ihn ([Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production)). Das Übernehmen des Imports und das Freigeben seiner Werte bleiben bei Mitgliedern im Portal. So kommen Ergebnisse zurück: - JSON-Antworten kommen als Text und als strukturierter Inhalt. - Listen liefern eine Seite. Die Beschreibungen bitten den Assistenten, ein kleines `limit` zu übergeben und mit `cursor` gleich `next_cursor` erneut abzufragen, bis es `null` ist. - Dateien (PDF, QR-Codes als PNG oder SVG, Tabellen, Dokumente) kommen bis 1 MiB als eingebettete Ressource. Größere Dateien scheitern mit `result_too_large`; laden Sie sie dann mit dem Schlüssel über REST herunter. - Ein Fehler der API ist ein Werkzeugfehler mit `code`, `title`, `detail`, den Feldfehlern `errors` und der `request_id` des Problems, zum Beispiel `not_found` oder `validation_error`. Argumente, die nicht zum Schema des Werkzeugs passen, scheitern mit `invalid_arguments`, bevor etwas gesendet wird. > [!TIP] > Schreibend sind nur die beiden `condition`-Werkzeuge sowie `update_import_mapping` und `validate_import`. Sie stehen > in Ihrem Audit-Log mit dem Schlüssel als Akteur und dem Vermerk `channel: mcp`. Nach einer Zustandsmeldung kann ein > veröffentlichter Pass je nach Einstellung Ihrer Organisation sofort neu veröffentlicht werden. Lassen Sie Ihr > Werkzeug vor dem Aufruf nachfragen. ## Sicherheit - **Ein Schlüssel je Werkzeug und Person.** So sehen Sie im Audit-Log, wer gehandelt hat, und widerrufen ein Werkzeug, ohne die anderen zu berühren. - **So wenig Berechtigungen wie möglich.** Geben Sie `condition` nur Werkzeugen, die Messwerte melden müssen, und `import` nur Werkzeugen, die Importe vorbereiten; ein `read`-Schlüssel kann nichts ändern. - **Widerrufen** Sie einen Schlüssel auf der Seite API-Schlüssel, sobald ein Werkzeug oder Laptop nicht mehr genutzt wird. Der Widerruf wirkt sofort. - **Ergebnisse sind Daten, keine Anweisungen.** Werte, Notizen und Dokumente in BatteryPass stammen von Ihrem Team, Ihren Lieferanten und Servicepartnern. Der Server weist den Assistenten an, sie als Daten zu behandeln; prüfen Sie trotzdem, was ein Assistent schreiben will. - Legen Sie einen Schlüssel nie in ein Repository, eine URL oder eine Chatnachricht. Die Konfigurationsdateien oben lesen ihn aus der Umgebung; nur der Befehl `claude mcp add` und die Konfiguration von `mcp-remote` enthalten ihn im Klartext. ## Fehlerbehebung | Antwort | Ursache und Abhilfe | | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `401 unauthenticated` | Kein Schlüssel, ein vertippter oder ein widerrufener Schlüssel: Die Antwort ist in allen drei Fällen gleich. Prüfen Sie, ob die Umgebungsvariable in der Umgebung gesetzt ist, aus der das Werkzeug startet, und ob der Header `Authorization: Bearer bp_live_…` lautet. | | `403 forbidden` beim Verbinden | Ein Client im Browser auf einem anderen Web-Origin hat einen `Origin`-Header gesendet. Browser-Clients auf anderen Origins werden nicht unterstützt; nutzen Sie einen Desktop-Client oder die Brücke `mcp-remote`. | | `400` mit dem JSON-RPC-Fehler `-32600` | Der Client hat einen Batch gesendet (mehrere Nachrichten in einer Anfrage). Senden Sie eine Nachricht je Anfrage; aktuelle Clients tun das. | | Werkzeugfehler `403 forbidden` | Das Werkzeug braucht eine Berechtigung, die Ihr Schlüssel nicht hat, zum Beispiel `condition`. Erstellen Sie einen Schlüssel mit dieser Berechtigung. | | `405 method not allowed` | Etwas hat ein `GET` (oder `DELETE`) an `/mcp` gesendet. Der Server beantwortet nur `POST`; aktualisieren Sie den Client oder wählen Sie den Transport Streamable HTTP (`--transport http`). | | `404 not found` | Die Adresse ist falsch, oder der Administrator hat MCP abgeschaltet (`MCP_ENABLED=false`). | | `429 rate_limited` oder Werkzeugfehler `rate_limited` | Alle Schlüssel Ihrer Organisation zusammen dürfen 600 Anfragen pro Minute stellen, und jeder Werkzeugaufruf zählt doppelt (die MCP-Anfrage und die API-Anfrage, die er auslöst). Warten Sie die Sekunden aus `Retry-After` ab (`retry_after_seconds` in Werkzeugfehlern). Wiederholt falsche Schlüssel von einer Adresse sind auf 30 Versuche in 15 Minuten begrenzt. | [**API-Referenz**](https://passport.kabin.sh/docs/de/api-reference): Jede Operation hinter den Werkzeugen, mit Parametern, Antworten und Fehlercodes. [**API-Schlüssel**](https://passport.kabin.sh/settings/api-keys): Schlüssel im BatteryPass-Portal erstellen und widerrufen (nur Eigentümer). --- # Rezepte > Kurze, lauffähige Programme für häufige Integrationsaufgaben, jeweils in cURL, TypeScript, Python und Go. Ein Rezept ist ein kleines Programm, das eine Aufgabe von Anfang bis Ende erledigt. Kopieren Sie es, setzen Sie `BATTERYPASS_URL` und `BATTERYPASS_API_KEY` (siehe [Schnellstart](https://passport.kabin.sh/docs/de/quickstart)), ersetzen Sie den Platzhalter, der Ihre eigenen Daten liest, und starten Sie es. Jedes Rezept gibt es als Shell-Skript mit `curl` und `jq` sowie in TypeScript (Node 20+), Python 3 mit `requests` und Go mit der Standardbibliothek. [**Nächtlicher Zustandsdaten-Push**](https://passport.kabin.sh/docs/de/recipes/nightly-condition-push): Die Messwerte der letzten Nacht für jede Einheit mit `updateCondition` senden, mit Backoff bei `429`. [**QR-Etiketten für eine Charge**](https://passport.kabin.sh/docs/de/recipes/qr-labels-for-a-batch): Für jede veröffentlichte Einheit einer Produktionscharge einen QR-Code als SVG herunterladen. [**Data-Warehouse-Abgleich**](https://passport.kabin.sh/docs/de/recipes/data-warehouse-sync): Jede Einheit mit ihren aktuellen Werten in eine JSON-Lines-Datei schreiben. [**Signaturen prüfen**](https://passport.kabin.sh/docs/de/recipes/verify-signatures): Die Signaturen einer Liste öffentlicher Pässe offline prüfen. [**Veröffentlichte Pässe eines Modells**](https://passport.kabin.sh/docs/de/recipes/published-passports-of-a-model): Pass-ID, Version und öffentlichen Link jeder veröffentlichten Einheit eines Modells als CSV ausgeben. Die [Anleitungen](https://passport.kabin.sh/docs/de/guides) erklären die Operationen hinter diesen Rezepten ausführlicher, samt der Fehler, mit denen Sie rechnen sollten. --- # Nächtlicher Zustandsdaten-Push > Ein geplanter Job, der die Messwerte der letzten Nacht aus Ihrer eigenen Quelle liest und je Einheit ein updateCondition sendet, mit Backoff bei 429. Der Job liest Messwerte aus Ihrer eigenen Quelle, hier ein Platzhalter, und sendet je Einheit ein [`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition). Er braucht einen Schlüssel mit dem Scope `condition`. Starten Sie ihn einmal pro Nacht per Cron, systemd-Timer oder Ihrem Scheduler. - Bei `429` oder `5xx` wartet er (`Retry-After`, sonst exponentieller Backoff) und sendet denselben Body erneut. Das ist unbedenklich: Bereits gespeicherte Werte kommen in `skipped` als `unchanged` zurück. - Andere Fehler (etwa `400 validation_error` für einen Wert außerhalb des Bereichs) protokolliert er je Einheit, macht mit der nächsten weiter und endet am Schluss mit dem Status 1. - Tragen Sie in `measured_at` den Zeitpunkt der Messung ein, nicht den Zeitpunkt, zu dem der Job läuft. ```bash title="cURL" #!/usr/bin/env bash # nightly-condition-push.sh: one updateCondition per line of readings.jsonl (needs curl and jq) # Each line, written by your BMS export: {"battery_id": "…", "measured_at": "…", "values": {"condition.…": 81.5}} set -uo pipefail failed=0 while IFS= read -r line; do id=$(jq -r .battery_id <<<"$line") body=$(jq -c '{measured_at, note: "Nightly BMS export", values: (.values | to_entries | map({field_key: .key, value: .value}))}' <<<"$line") # --retry repeats 429 and 5xx answers and waits as long as Retry-After says status=$(curl -sS --retry 5 -o response.json -w '%{http_code}' -X PATCH \ "$BATTERYPASS_URL/api/v1/batteries/$id/condition" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ -H "Content-Type: application/json" \ --data "$body") if [ "$status" = 200 ]; then jq -c --arg id "$id" '{id: $id, accepted: (.accepted | length), skipped, outcome: .publication.outcome}' response.json else failed=1 jq -c --arg id "$id" --arg status "$status" '{id: $id, status: $status, code, request_id}' response.json >&2 fi done < readings.jsonl exit $failed ``` ```ts title="TypeScript" // nightly-condition-push.mts: npx tsx nightly-condition-push.mts const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const KEY = process.env.BATTERYPASS_API_KEY; // a key with the scope "condition" const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); type Reading = { batteryId: string; measuredAt: string; values: Record }; // Placeholder: read last night's measurements from your BMS, historian or database. async function readMeasurements(): Promise { return [ { batteryId: '0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f', measuredAt: '2026-10-24T02:00:00Z', values: { 'condition.state_of_charge': 81.5, 'condition.number_of_full_cycles': 412 }, }, ]; } async function push(r: Reading): Promise { const body = JSON.stringify({ measured_at: r.measuredAt, note: 'Nightly BMS export', values: Object.entries(r.values).map(([field_key, value]) => ({ field_key, value })), }); for (let attempt = 1; attempt <= 6; attempt++) { const res = await fetch(`${BASE}/batteries/${r.batteryId}/condition`, { method: 'PATCH', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body, }); if (res.status === 429 || res.status >= 500) { // Sending the same readings again is safe: values already stored come back as skipped. const retryAfter = Number(res.headers.get('Retry-After')); await sleep(retryAfter > 0 ? retryAfter * 1000 : Math.random() * 1000 * 2 ** attempt); continue; } const result = await res.json(); if (!res.ok) throw new Error(`${res.status} ${result.code} (request_id ${result.request_id})`); return `accepted ${result.accepted.length}, skipped ${result.skipped.length}, ${result.publication.outcome}`; } throw new Error('gave up after 6 attempts'); } let failed = 0; for (const reading of await readMeasurements()) { try { console.log(reading.batteryId, await push(reading)); } catch (err) { failed++; console.error(reading.batteryId, String(err)); } } process.exitCode = failed > 0 ? 1 : 0; ``` ```python title="Python" # nightly_condition_push.py import os import random import sys import time import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_API_KEY"] # scope "condition" def read_measurements(): """Placeholder: read last night's measurements from your BMS, historian or database.""" return [ { "battery_id": "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f", "measured_at": "2026-10-24T02:00:00Z", "values": {"condition.state_of_charge": 81.5, "condition.number_of_full_cycles": 412}, }, ] def push(reading): body = { "measured_at": reading["measured_at"], "note": "Nightly BMS export", "values": [{"field_key": k, "value": v} for k, v in reading["values"].items()], } for attempt in range(1, 7): res = session.patch(f"{BASE}/batteries/{reading['battery_id']}/condition", json=body, timeout=30) if res.status_code == 429 or res.status_code >= 500: # Sending the same readings again is safe: values already stored come back as skipped. retry_after = res.headers.get("Retry-After") time.sleep(int(retry_after) if retry_after else random.uniform(0, 2**attempt)) continue result = res.json() if not res.ok: raise RuntimeError(f"{res.status_code} {result['code']} (request_id {result.get('request_id')})") return f"accepted {len(result['accepted'])}, skipped {len(result['skipped'])}, {result['publication']['outcome']}" raise RuntimeError("gave up after 6 attempts") failed = 0 for reading in read_measurements(): try: print(reading["battery_id"], push(reading)) except Exception as err: failed += 1 print(reading["battery_id"], err, file=sys.stderr) sys.exit(1 if failed else 0) ``` ```go title="Go" // nightly-condition-push: go run . package main import ( "bytes" "encoding/json" "errors" "fmt" "log" "math/rand/v2" "net/http" "os" "strconv" "time" ) var ( base = os.Getenv("BATTERYPASS_URL") + "/api/v1" key = os.Getenv("BATTERYPASS_API_KEY") // a key with the scope "condition" ) type reading struct { BatteryID string MeasuredAt string Values map[string]any } // readMeasurements is a placeholder: read last night's measurements from your BMS, historian or database. func readMeasurements() []reading { return []reading{{ BatteryID: "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f", MeasuredAt: "2026-10-24T02:00:00Z", Values: map[string]any{"condition.state_of_charge": 81.5, "condition.number_of_full_cycles": 412}, }} } type result struct { Accepted []json.RawMessage `json:"accepted"` Skipped []json.RawMessage `json:"skipped"` Publication struct { Outcome string `json:"outcome"` } `json:"publication"` Code string `json:"code"` RequestID string `json:"request_id"` } func push(r reading) (string, error) { values := []map[string]any{} for k, v := range r.Values { values = append(values, map[string]any{"field_key": k, "value": v}) } body, err := json.Marshal(map[string]any{"measured_at": r.MeasuredAt, "note": "Nightly BMS export", "values": values}) if err != nil { return "", err } for attempt := 1; attempt <= 6; attempt++ { req, err := http.NewRequest(http.MethodPatch, base+"/batteries/"+r.BatteryID+"/condition", bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+key) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { return "", err } var out result _ = json.NewDecoder(res.Body).Decode(&out) res.Body.Close() if res.StatusCode == http.StatusTooManyRequests || res.StatusCode >= 500 { // Sending the same readings again is safe: values already stored come back as skipped. wait := time.Duration(rand.Int64N(int64(time.Second << attempt))) if s, _ := strconv.Atoi(res.Header.Get("Retry-After")); s > 0 { wait = time.Duration(s) * time.Second } time.Sleep(wait) continue } if res.StatusCode != http.StatusOK { return "", fmt.Errorf("%d %s (request_id %s)", res.StatusCode, out.Code, out.RequestID) } return fmt.Sprintf("accepted %d, skipped %d, %s", len(out.Accepted), len(out.Skipped), out.Publication.Outcome), nil } return "", errors.New("gave up after 6 attempts") } func main() { failed := 0 for _, r := range readMeasurements() { summary, err := push(r) if err != nil { failed++ log.Println(r.BatteryID, err) continue } fmt.Println(r.BatteryID, summary) } if failed > 0 { os.Exit(1) } } ``` ```text title="Ausgabe" 0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f accepted 1, skipped 1, published ``` Die Shell-Fassung liest `readings.jsonl`, je Einheit ein JSON-Objekt wie in ihrem Kopf gezeigt. Felder, Maßeinheiten und die Regeln für `measured_at` erklärt [Zustandsdaten senden](https://passport.kabin.sh/docs/de/guides/push-condition-data). Denken Sie bei vielen Einheiten daran, dass sich alle Schlüssel Ihrer Organisation 600 Anfragen pro Minute teilen. --- # QR-Etiketten für eine Produktionscharge > Listen Sie die veröffentlichten Einheiten einer Charge über das Seriennummernpräfix und speichern Sie den QR-Code jeder Einheit als SVG-Datei für den Etikettendruck. Das Skript listet mit [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries) die Einheiten, deren Seriennummer mit einem Präfix beginnt (Ihre Charge, etwa `SV2027-0001`) und deren Pass veröffentlicht ist, und speichert den QR-Code jeder Einheit mit [`getPassportQr`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-qr) als `labels/.svg`. Es braucht einen Schlüssel mit dem Scope `read`. - Andere Wege, eine Charge auszuwählen: `import_id` (die Einheiten eines Tabellenimports) oder `model_version_id`. Ersetzen Sie dafür `serial_prefix` in der Abfrage. - Etiketten, die vor der Veröffentlichung gedruckt werden: Verwenden Sie stattdessen `passport_status=qr_locked`. Den QR-Code gibt es, sobald die Kennungen gesperrt sind, und der Link darin ändert sich danach nie mehr. - SVG skaliert für jede Etikettengröße. Für eine Rastergrafik fragen Sie `?format=png&size=1024` an (128 bis 2048 Pixel). - Ein `/` in einer Seriennummer wird im Dateinamen zu `_`. ```bash title="cURL" #!/usr/bin/env bash # qr-labels.sh SV2027-0001: an SVG QR code per published unit of a batch (needs curl and jq) set -euo pipefail prefix="$1" # the batch: serial numbers that start with this, e.g. SV2027-0001 auth="Authorization: Bearer $BATTERYPASS_API_KEY" mkdir -p labels cursor="" while :; do page=$(curl -sS --fail --retry 5 -G "$BATTERYPASS_URL/api/v1/batteries" -H "$auth" \ --data-urlencode "serial_prefix=$prefix" -d passport_status=published -d limit=200 \ ${cursor:+--data-urlencode "cursor=$cursor"}) jq -r '.items[] | [.id, (.serial_number | gsub("/"; "_"))] | @tsv' <<<"$page" | while IFS=$'\t' read -r id serial; do curl -sS --fail --retry 5 -H "$auth" -o "labels/$serial.svg" \ "$BATTERYPASS_URL/api/v1/batteries/$id/passport/qr?format=svg" echo "labels/$serial.svg" done cursor=$(jq -r '.next_cursor // empty' <<<"$page") [ -n "$cursor" ] || break done ``` ```ts title="TypeScript" // qr-labels.mts: npx tsx qr-labels.mts SV2027-0001 import { mkdir, writeFile } from 'node:fs/promises'; const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }; const prefix = process.argv[2]; // the batch: serial numbers that start with this, e.g. SV2027-0001 const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function get(path: string): Promise { for (let attempt = 1; ; attempt++) { const res = await fetch(BASE + path, { headers }); if (res.ok) return res; if (res.status !== 429 || attempt === 5) throw new Error(`${path}: ${res.status} ${(await res.json()).code}`); await sleep(Number(res.headers.get('Retry-After') ?? 1) * 1000); } } await mkdir('labels', { recursive: true }); let cursor: string | null = null; do { const query = new URLSearchParams({ serial_prefix: prefix, passport_status: 'published', limit: '200' }); if (cursor) query.set('cursor', cursor); const page = await (await get(`/batteries?${query}`)).json(); for (const unit of page.items) { const svg = await (await get(`/batteries/${unit.id}/passport/qr?format=svg`)).arrayBuffer(); const file = `labels/${unit.serial_number.replaceAll('/', '_')}.svg`; await writeFile(file, Buffer.from(svg)); console.log(file); } cursor = page.next_cursor; } while (cursor); ``` ```python title="Python" # qr_labels.py: python qr_labels.py SV2027-0001 import os import sys import time import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_API_KEY"] prefix = sys.argv[1] # the batch: serial numbers that start with this, e.g. SV2027-0001 def get(path, **params): for attempt in range(1, 6): res = session.get(BASE + path, params=params, timeout=30) if res.status_code != 429 or attempt == 5: res.raise_for_status() return res time.sleep(int(res.headers.get("Retry-After", "1"))) os.makedirs("labels", exist_ok=True) cursor = None while True: params = {"serial_prefix": prefix, "passport_status": "published", "limit": 200} if cursor: params["cursor"] = cursor page = get("/batteries", **params).json() for unit in page["items"]: svg = get(f"/batteries/{unit['id']}/passport/qr", format="svg").content path = os.path.join("labels", unit["serial_number"].replace("/", "_") + ".svg") with open(path, "wb") as f: f.write(svg) print(path) cursor = page["next_cursor"] if not cursor: break ``` ```go title="Go" // qr-labels: go run . SV2027-0001 package main import ( "encoding/json" "fmt" "io" "log" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" "time" ) var ( base = os.Getenv("BATTERYPASS_URL") + "/api/v1" key = os.Getenv("BATTERYPASS_API_KEY") ) // get returns the response body of a GET request and waits out 429 answers. func get(path string) ([]byte, error) { for attempt := 1; ; attempt++ { req, err := http.NewRequest(http.MethodGet, base+path, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+key) res, err := http.DefaultClient.Do(req) if err != nil { return nil, err } body, err := io.ReadAll(res.Body) res.Body.Close() switch { case err != nil: return nil, err case res.StatusCode == http.StatusOK: return body, nil case res.StatusCode != http.StatusTooManyRequests || attempt == 5: return nil, fmt.Errorf("%s: %s %s", path, res.Status, body) } wait, _ := strconv.Atoi(res.Header.Get("Retry-After")) time.Sleep(time.Duration(max(wait, 1)) * time.Second) } } func main() { prefix := os.Args[1] // the batch: serial numbers that start with this, e.g. SV2027-0001 if err := os.MkdirAll("labels", 0o755); err != nil { log.Fatal(err) } cursor := "" for { q := url.Values{"serial_prefix": {prefix}, "passport_status": {"published"}, "limit": {"200"}} if cursor != "" { q.Set("cursor", cursor) } data, err := get("/batteries?" + q.Encode()) if err != nil { log.Fatal(err) } var page struct { Items []struct { ID string `json:"id"` SerialNumber string `json:"serial_number"` } `json:"items"` NextCursor *string `json:"next_cursor"` } if err := json.Unmarshal(data, &page); err != nil { log.Fatal(err) } for _, unit := range page.Items { svg, err := get("/batteries/" + unit.ID + "/passport/qr?format=svg") if err != nil { log.Fatal(err) } file := filepath.Join("labels", strings.ReplaceAll(unit.SerialNumber, "/", "_")+".svg") if err := os.WriteFile(file, svg, 0o644); err != nil { log.Fatal(err) } fmt.Println(file) } if page.NextCursor == nil { break } cursor = *page.NextCursor } } ``` ```text title="Ausgabe" labels/SV2027-000182.svg labels/SV2027-000184.svg labels/SV2027-000185.svg ``` Jede Einheit kostet eine Anfrage, und alle Schlüssel Ihrer Organisation teilen sich 600 Anfragen pro Minute; bei `429` warten die Skripte. Mehr zum QR-Code und zu den anderen Exporten unter [Pässe exportieren](https://passport.kabin.sh/docs/de/guides/export-passports). --- # Data-Warehouse-Abgleich > Gehen Sie alle Einheiten und ihre aktuellen Werte seitenweise durch und schreiben Sie je Einheit eine JSON-Zeile, bereit zum Laden in ein Data Warehouse. Das Skript geht mit [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries) alle Einheiten seitenweise durch, liest die aktuellen Werte jeder Einheit mit [`listBatteryValues`](https://passport.kabin.sh/docs/de/api-reference/values/list-battery-values) und schreibt `units.jsonl`: je Einheit eine Zeile mit `synced_at`, der `unit` und ihren `values`. Die meisten Data Warehouses laden JSON Lines direkt. Es braucht einen Schlüssel mit dem Scope `read`. - `values` enthält den aktuellen Wert jedes Feldes auf Einheitsebene, das einen Wert hat (oder hatte), mit seiner Herkunft: `revision`, `source`, `review_status`, `recorded_at` und bei Zustandsdaten `measured_at`. Ein geleertes Feld hat `value: null`. - Daten auf Modellebene (Nennspannung, Nennkapazität und Ähnliches) gehören zur Modellversion, nicht zur Einheit: Lesen Sie sie einmal je Version mit [`listModelVersionValues`](https://passport.kabin.sh/docs/de/api-reference/values/list-model-version-values). - Das Skript stellt eine Anfrage je Einheit und eine je 200 Einheiten. Bei 10 000 Einheiten sind das am gemeinsamen Limit von 600 Anfragen pro Minute etwa 17 Minuten; bei `429` wartet es. ```bash title="cURL" #!/usr/bin/env bash # warehouse-sync.sh: every unit with its current values as one JSON line (needs curl and jq) set -euo pipefail auth="Authorization: Bearer $BATTERYPASS_API_KEY" synced_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) cursor="" : > units.jsonl while :; do page=$(curl -sS --fail --retry 5 -G "$BATTERYPASS_URL/api/v1/batteries" -H "$auth" \ -d limit=200 ${cursor:+--data-urlencode "cursor=$cursor"}) jq -c '.items[]' <<<"$page" | while IFS= read -r unit; do id=$(jq -r .id <<<"$unit") curl -sS --fail --retry 5 -H "$auth" "$BATTERYPASS_URL/api/v1/batteries/$id/values" | jq -c --argjson unit "$unit" --arg at "$synced_at" '{synced_at: $at, unit: $unit, values: .items}' >> units.jsonl done cursor=$(jq -r '.next_cursor // empty' <<<"$page") [ -n "$cursor" ] || break done wc -l < units.jsonl ``` ```ts title="TypeScript" // warehouse-sync.mts: npx tsx warehouse-sync.mts import { createWriteStream } from 'node:fs'; const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function get(path: string) { for (let attempt = 1; ; attempt++) { const res = await fetch(BASE + path, { headers }); if (res.ok) return res.json(); if (res.status !== 429 || attempt === 5) throw new Error(`${path}: ${res.status} ${(await res.json()).code}`); await sleep(Number(res.headers.get('Retry-After') ?? 1) * 1000); } } const out = createWriteStream('units.jsonl'); const syncedAt = new Date().toISOString(); let cursor: string | null = null; let count = 0; do { const page = await get(`/batteries?limit=200${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`); for (const unit of page.items) { const values = await get(`/batteries/${unit.id}/values`); out.write(JSON.stringify({ synced_at: syncedAt, unit, values: values.items }) + '\n'); count++; } cursor = page.next_cursor; } while (cursor); out.end(); console.log(`${count} units written to units.jsonl`); ``` ```python title="Python" # warehouse_sync.py import json import os import time from datetime import datetime, timezone import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_API_KEY"] def get(path, **params): for attempt in range(1, 6): res = session.get(BASE + path, params=params, timeout=30) if res.status_code != 429 or attempt == 5: res.raise_for_status() return res.json() time.sleep(int(res.headers.get("Retry-After", "1"))) synced_at = datetime.now(timezone.utc).isoformat() count, cursor = 0, None with open("units.jsonl", "w", encoding="utf-8") as out: while True: page = get("/batteries", limit=200, **({"cursor": cursor} if cursor else {})) for unit in page["items"]: values = get(f"/batteries/{unit['id']}/values") out.write(json.dumps({"synced_at": synced_at, "unit": unit, "values": values["items"]}) + "\n") count += 1 cursor = page["next_cursor"] if not cursor: break print(f"{count} units written to units.jsonl") ``` ```go title="Go" // warehouse-sync: go run . package main import ( "bufio" "encoding/json" "fmt" "log" "net/http" "net/url" "os" "strconv" "time" ) var ( base = os.Getenv("BATTERYPASS_URL") + "/api/v1" key = os.Getenv("BATTERYPASS_API_KEY") ) // get decodes the JSON answer of a GET request into out and waits out 429 answers. func get(path string, out any) error { for attempt := 1; ; attempt++ { req, err := http.NewRequest(http.MethodGet, base+path, nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+key) res, err := http.DefaultClient.Do(req) if err != nil { return err } if res.StatusCode == http.StatusOK { defer res.Body.Close() return json.NewDecoder(res.Body).Decode(out) } res.Body.Close() if res.StatusCode != http.StatusTooManyRequests || attempt == 5 { return fmt.Errorf("%s: %s", path, res.Status) } wait, _ := strconv.Atoi(res.Header.Get("Retry-After")) time.Sleep(time.Duration(max(wait, 1)) * time.Second) } } func main() { file, err := os.Create("units.jsonl") if err != nil { log.Fatal(err) } defer file.Close() out := bufio.NewWriter(file) defer out.Flush() enc := json.NewEncoder(out) // one JSON document per line syncedAt := time.Now().UTC().Format(time.RFC3339) count, cursor := 0, "" for { path := "/batteries?limit=200" if cursor != "" { path += "&cursor=" + url.QueryEscape(cursor) } var page struct { Items []map[string]any `json:"items"` NextCursor *string `json:"next_cursor"` } if err := get(path, &page); err != nil { log.Fatal(err) } for _, unit := range page.Items { var values struct { Items []map[string]any `json:"items"` } if err := get(fmt.Sprintf("/batteries/%s/values", unit["id"]), &values); err != nil { log.Fatal(err) } line := map[string]any{"synced_at": syncedAt, "unit": unit, "values": values.Items} if err := enc.Encode(line); err != nil { log.Fatal(err) } count++ } if page.NextCursor == nil { break } cursor = *page.NextCursor } fmt.Printf("%d units written to units.jsonl\n", count) } ``` ```json title="units.jsonl (eine Zeile, hier umbrochen)" {"synced_at":"2026-10-24T02:00:00Z", "unit":{"id":"0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f","serial_number":"SV2027-000184","passport_status":"published","row_version":4,"updated_at":"2026-07-02T10:15:00Z"}, "values":[{"field_key":"condition.state_of_charge","value":81.5,"unit":"%","revision":9,"source":"api","review_status":"approved","measured_at":"2026-10-24T06:00:00Z","recorded_at":"2026-10-24T06:00:03Z","registry_version":"0.5.0","checks":[]}]} ``` Laden Sie die Datei in eine Staging-Tabelle und führen Sie sie über `unit.id` zusammen. Um bei späteren Läufen weniger zu lesen, vergleichen Sie `row_version`, `passport_status` und die `revision` der Werte mit dem, was Sie gespeichert haben, wie unter [Einheiten abgleichen](https://passport.kabin.sh/docs/de/guides/sync-units) beschrieben. Eine vollständige Kopie aller Daten Ihrer Organisation kann ein Admin zusätzlich im Portal als Organisationsexport herunterladen. --- # Signaturen mehrerer Pässe prüfen > Prüfen Sie die Ed25519-Signaturen einer Liste öffentlicher Pässe offline, laden Sie das Schlüsselset nur einmal und geben Sie je Pass ein Ergebnis aus. Das Skript erwartet Pass-UUIDs (den Teil nach `urn:dpp:`) als Argumente, lädt das Schlüsselset von `/.well-known/batterypass/keys.json` einmal und prüft jeden Pass wie in [Passsignatur offline prüfen](https://passport.kabin.sh/docs/de/guides/verify-signature) beschrieben: `document` nach RFC 8785 kanonisieren, den SHA-256 mit `verification.hash` vergleichen und die Ed25519-Signatur mit dem Schlüssel prüfen, den `verification.key_id` nennt. Einen API-Schlüssel braucht es nicht. Ist ein Pass nicht `valid`, endet es mit dem Status 1; es passt also in einen Monitoring-Job oder eine Wareneingangsprüfung. Der Tab cURL nutzt stattdessen [`verifyPassport`](https://passport.kabin.sh/docs/de/api-reference/public/verify-passport): Dort prüft der Server die Signaturen für Sie. Das geht schnell, verlässt sich aber auf die Antwort des Servers. ```bash title="cURL" #!/usr/bin/env bash # verify-many.sh ...: the server-side check with verifyPassport (needs curl and jq) for id in "$@"; do curl -sS "$BATTERYPASS_URL/api/v1/public/passports/$id/verify" | jq -r --arg id "$id" '"\($id)\t\(if .valid == true then "valid (version \(.version_no))" elif .code then .code else "INVALID" end)"' done ``` ```ts title="TypeScript" // verify-many.mts: npm install canonicalize, then: npx tsx verify-many.mts ... import { createHash, createPublicKey, verify, type KeyObject } from 'node:crypto'; import canonicalize from 'canonicalize'; const base = process.env.BATTERYPASS_URL; const ids = process.argv.slice(2); // passport UUIDs const getJSON = async (url: string) => { const res = await fetch(url); if (!res.ok) throw new Error(`${res.status}`); return res.json(); }; // Load the key set once. const keys = new Map(); for (const jwk of (await getJSON(`${base}/.well-known/batterypass/keys.json`)).keys) { keys.set(jwk.kid, createPublicKey({ key: jwk, format: 'jwk' })); } async function check(id: string): Promise { const { status, document, verification: v } = await getJSON(`${base}/api/v1/public/passports/${id}`); if (!document || !v) return `not checked (${status})`; const canonical = Buffer.from(canonicalize(document) ?? '', 'utf8'); if (`sha256:${createHash('sha256').update(canonical).digest('hex')}` !== v.hash) return 'HASH MISMATCH'; const key = keys.get(v.key_id); if (!key) return `UNKNOWN KEY ${v.key_id}`; return verify(null, canonical, key, Buffer.from(v.signature, 'base64url')) ? 'valid' : 'INVALID SIGNATURE'; } let bad = 0; for (const id of ids) { const result = await check(id).catch((err) => `error ${err.message}`); if (result !== 'valid') bad++; console.log(`${id}\t${result}`); } process.exitCode = bad > 0 ? 1 : 0; ``` ```python title="Python" # verify_many.py: pip install requests cryptography rfc8785, then: python verify_many.py ... import base64 import hashlib import os import sys import requests import rfc8785 from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey base = os.environ["BATTERYPASS_URL"] def b64url(text): return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4)) def get_json(url): res = requests.get(url, timeout=10) res.raise_for_status() return res.json() # Load the key set once. keys = { jwk["kid"]: Ed25519PublicKey.from_public_bytes(b64url(jwk["x"])) for jwk in get_json(f"{base}/.well-known/batterypass/keys.json")["keys"] } def check(passport_id): passport = get_json(f"{base}/api/v1/public/passports/{passport_id}") document, v = passport.get("document"), passport.get("verification") if not document or not v: return f"not checked ({passport['status']})" canonical = rfc8785.dumps(document) if "sha256:" + hashlib.sha256(canonical).hexdigest() != v["hash"]: return "HASH MISMATCH" if v["key_id"] not in keys: return f"UNKNOWN KEY {v['key_id']}" try: keys[v["key_id"]].verify(b64url(v["signature"]), canonical) return "valid" except InvalidSignature: return "INVALID SIGNATURE" bad = 0 for passport_id in sys.argv[1:]: # passport UUIDs try: result = check(passport_id) except requests.RequestException as err: result = f"error {err}" bad += result != "valid" print(f"{passport_id}\t{result}") sys.exit(1 if bad else 0) ``` ```go title="Go" // verify-many: go get github.com/gowebpki/jcs, then: go run . ... package main import ( "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "net/http" "os" "github.com/gowebpki/jcs" ) var base = os.Getenv("BATTERYPASS_URL") func getJSON(url string, v any) error { res, err := http.Get(url) if err != nil { return err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return fmt.Errorf("%s", res.Status) } return json.NewDecoder(res.Body).Decode(v) } func check(id string, keys map[string]ed25519.PublicKey) string { var p struct { Status string `json:"status"` Document json.RawMessage `json:"document"` Verification *struct { Hash, Signature string KeyID string `json:"key_id"` } `json:"verification"` } if err := getJSON(base+"/api/v1/public/passports/"+id, &p); err != nil { return "error " + err.Error() } if p.Document == nil || p.Verification == nil { return "not checked (" + p.Status + ")" } canonical, err := jcs.Transform(p.Document) if err != nil { return "error " + err.Error() } sum := sha256.Sum256(canonical) if "sha256:"+hex.EncodeToString(sum[:]) != p.Verification.Hash { return "HASH MISMATCH" } pub, ok := keys[p.Verification.KeyID] if !ok { return "UNKNOWN KEY " + p.Verification.KeyID } sig, err := base64.RawURLEncoding.DecodeString(p.Verification.Signature) if err != nil || !ed25519.Verify(pub, canonical, sig) { return "INVALID SIGNATURE" } return "valid" } func main() { // Load the key set once. var set struct { Keys []struct{ Kid, X string } `json:"keys"` } if err := getJSON(base+"/.well-known/batterypass/keys.json", &set); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } keys := map[string]ed25519.PublicKey{} for _, k := range set.Keys { if x, err := base64.RawURLEncoding.DecodeString(k.X); err == nil && len(x) == ed25519.PublicKeySize { keys[k.Kid] = x } } bad := 0 for _, id := range os.Args[1:] { // passport UUIDs result := check(id, keys) if result != "valid" { bad++ } fmt.Printf("%s\t%s\n", id, result) } if bad > 0 { os.Exit(1) } } ``` ```text title="Ausgabe" 0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10 valid 0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b11 not checked (qr_locked) 0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b12 error 404 ``` `not checked` heißt, dass der Pass gerade nicht öffentlich ist (sein `status` steht dabei); ein signiertes Dokument gibt es dann nicht. Öffentliche Abrufe sind auf 300 Anfragen pro Minute und Client-Adresse begrenzt; bei langen Listen legen Sie eine Pause ein oder behandeln `429` wie unter [Fehler und Wiederholungen](https://passport.kabin.sh/docs/de/guides/handle-errors). Um Pässe eines anderen Herstellers zu prüfen, setzen Sie `BATTERYPASS_URL` auf die Adresse, von der dessen Pässe stammen. --- # Veröffentlichte Pässe eines Modells > Listen Sie jede Einheit eines Batteriemodells mit veröffentlichtem Pass auf und geben Sie Pass-ID, aktuelle Version und öffentlichen Link als CSV aus. Das Skript listet mit [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries) und den Filtern `battery_model_id` und `passport_status=published` die Einheiten eines Batteriemodells, deren Pass veröffentlicht ist, liest jeden Pass mit [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport) und gibt je Einheit eine CSV-Zeile aus: Seriennummer, Pass-ID, aktuelle Version, Zeitpunkt der Veröffentlichung und öffentliche Seite. Es braucht einen Schlüssel mit dem Scope `read`. - Die ID des Modells liefert [`listBatteryModels`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-battery-models). - Pässe im Status `update_needed` sind ebenfalls online (ihre aktuelle Version bleibt öffentlich, bis sie erneut veröffentlicht werden). Starten Sie das Skript ein zweites Mal mit `passport_status=update_needed`, um sie einzuschließen. - Für eine einzelne Modellversion verwenden Sie `model_version_id` statt `battery_model_id`. ```bash title="cURL" #!/usr/bin/env bash # model-passports.sh : the published passports of one battery model as CSV (needs curl and jq) set -euo pipefail model_id="$1" # the id from listBatteryModels auth="Authorization: Bearer $BATTERYPASS_API_KEY" echo "serial_number,passport_id,version_no,published_at,public_url" cursor="" while :; do page=$(curl -sS --fail --retry 5 -G "$BATTERYPASS_URL/api/v1/batteries" -H "$auth" \ -d "battery_model_id=$model_id" -d passport_status=published -d limit=200 \ ${cursor:+--data-urlencode "cursor=$cursor"}) for id in $(jq -r '.items[].id' <<<"$page"); do curl -sS --fail --retry 5 -H "$auth" "$BATTERYPASS_URL/api/v1/batteries/$id/passport" | jq -r '[.serial_number, .passport_id, .current_version.version_no, .current_version.published_at, .public_url] | @csv' done cursor=$(jq -r '.next_cursor // empty' <<<"$page") [ -n "$cursor" ] || break done ``` ```ts title="TypeScript" // model-passports.mts: npx tsx model-passports.mts const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` }; const modelId = process.argv[2]; // the id from listBatteryModels const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function get(path: string) { for (let attempt = 1; ; attempt++) { const res = await fetch(BASE + path, { headers }); if (res.ok) return res.json(); if (res.status !== 429 || attempt === 5) throw new Error(`${path}: ${res.status} ${(await res.json()).code}`); await sleep(Number(res.headers.get('Retry-After') ?? 1) * 1000); } } console.log('serial_number,passport_id,version_no,published_at,public_url'); let cursor: string | null = null; do { const query = new URLSearchParams({ battery_model_id: modelId, passport_status: 'published', limit: '200' }); if (cursor) query.set('cursor', cursor); const page = await get(`/batteries?${query}`); for (const unit of page.items) { const p = await get(`/batteries/${unit.id}/passport`); const v = p.current_version; console.log([p.serial_number, p.passport_id, v?.version_no, v?.published_at, p.public_url].join(',')); } cursor = page.next_cursor; } while (cursor); ``` ```python title="Python" # model_passports.py: python model_passports.py import csv import os import sys import time import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_API_KEY"] model_id = sys.argv[1] # the id from listBatteryModels def get(path, **params): for attempt in range(1, 6): res = session.get(BASE + path, params=params, timeout=30) if res.status_code != 429 or attempt == 5: res.raise_for_status() return res.json() time.sleep(int(res.headers.get("Retry-After", "1"))) out = csv.writer(sys.stdout) out.writerow(["serial_number", "passport_id", "version_no", "published_at", "public_url"]) cursor = None while True: params = {"battery_model_id": model_id, "passport_status": "published", "limit": 200} if cursor: params["cursor"] = cursor page = get("/batteries", **params) for unit in page["items"]: p = get(f"/batteries/{unit['id']}/passport") v = p.get("current_version", {}) out.writerow([p["serial_number"], p.get("passport_id"), v.get("version_no"), v.get("published_at"), p.get("public_url")]) cursor = page["next_cursor"] if not cursor: break ``` ```go title="Go" // model-passports: go run . package main import ( "encoding/csv" "encoding/json" "fmt" "log" "net/http" "net/url" "os" "strconv" "time" ) var ( base = os.Getenv("BATTERYPASS_URL") + "/api/v1" key = os.Getenv("BATTERYPASS_API_KEY") ) // get decodes the JSON answer of a GET request into out and waits out 429 answers. func get(path string, out any) error { for attempt := 1; ; attempt++ { req, err := http.NewRequest(http.MethodGet, base+path, nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+key) res, err := http.DefaultClient.Do(req) if err != nil { return err } if res.StatusCode == http.StatusOK { defer res.Body.Close() return json.NewDecoder(res.Body).Decode(out) } res.Body.Close() if res.StatusCode != http.StatusTooManyRequests || attempt == 5 { return fmt.Errorf("%s: %s", path, res.Status) } wait, _ := strconv.Atoi(res.Header.Get("Retry-After")) time.Sleep(time.Duration(max(wait, 1)) * time.Second) } } type passport struct { SerialNumber string `json:"serial_number"` PassportID string `json:"passport_id"` PublicURL string `json:"public_url"` CurrentVersion struct { VersionNo int `json:"version_no"` PublishedAt string `json:"published_at"` } `json:"current_version"` } func main() { modelID := os.Args[1] // the id from listBatteryModels out := csv.NewWriter(os.Stdout) defer out.Flush() _ = out.Write([]string{"serial_number", "passport_id", "version_no", "published_at", "public_url"}) cursor := "" for { q := url.Values{"battery_model_id": {modelID}, "passport_status": {"published"}, "limit": {"200"}} if cursor != "" { q.Set("cursor", cursor) } var page struct { Items []struct { ID string `json:"id"` } `json:"items"` NextCursor *string `json:"next_cursor"` } if err := get("/batteries?"+q.Encode(), &page); err != nil { log.Fatal(err) } for _, unit := range page.Items { var p passport if err := get("/batteries/"+unit.ID+"/passport", &p); err != nil { log.Fatal(err) } v := p.CurrentVersion _ = out.Write([]string{p.SerialNumber, p.PassportID, strconv.Itoa(v.VersionNo), v.PublishedAt, p.PublicURL}) } if page.NextCursor == nil { break } cursor = *page.NextCursor } } ``` ```text title="Ausgabe" serial_number,passport_id,version_no,published_at,public_url SV2027-000184,urn:dpp:0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10,4,2026-10-24T06:00:03Z,https://batterypass.example.com/pass/0192f0c4-7d1e-7a3b-9c55-2f1e8d4a6b10 SV2027-000185,urn:dpp:0192f0d7-3e2a-7b1c-8d4e-5f6a7b8c9d0e,2,2026-09-30T14:22:10Z,https://batterypass.example.com/pass/0192f0d7-3e2a-7b1c-8d4e-5f6a7b8c9d0e ``` Die Shell-Fassung setzt jedes CSV-Feld in Anführungszeichen (`@csv`), die anderen nur, wo es nötig ist. Jede Einheit kostet eine Anfrage, und alle Schlüssel Ihrer Organisation teilen sich 600 Anfragen pro Minute. Pass und Versionen erklärt [Pässe und Versionen](https://passport.kabin.sh/docs/de/concepts/passports). --- # Ihr ERP sicher anbinden > Wie ein ERP, seine Middleware und BatteryPass zusammenarbeiten, wer was tut und was Ihre Systeme nie verlässt. Ihr ERP kennt Produkte, Seriennummern, Fertigungsaufträge, Lieferungen und Retouren. BatteryPass hält die Batteriepässe, die aus diesen Daten entstehen. Dieser Bereich zeigt, wie Sie beides verbinden: so, dass Daten fließen, ohne dass jemand Tabellen von Hand kopiert, und so, dass deklarierte Passdaten trotzdem nur über eine Person Ihrer Organisation in BatteryPass gelangen, die für sie einsteht. Die Seiten beschreiben **Integrationsmuster**, keine Produkte: BatteryPass liefert keinen ERP-Konnektor, hat keine Zertifizierung eines Herstellers und keine Partnerschaft mit einem ERP-Anbieter. Die Verbindung bauen Sie mit Ihrer Middleware oder iPaaS auf der veröffentlichten API. ## Die Architektur ```mermaid flowchart LR ERP["ERP
(Fertigung, Seriennummern,
Lieferungen, Retouren)"] -->|"Outbox oder Ereignis"| MW["Middleware / iPaaS"] MW -->|"API-Schlüssel, Scope import:
Tabelle bereitstellen"| API["BatteryPass-API"] MW -->|"API-Schlüssel, Scope condition:
Messwerte, Ereignisse"| API API -->|"API-Schlüssel, Scope read:
Änderungsfeed, Pässe, QR"| MW MW -->|"Digital Link, Status"| ERP API --> PORTAL["Portal: Mitglieder prüfen,
übernehmen, geben frei"] PORTAL -->|"Mitglieder veröffentlichen"| PUB["Veröffentlichter Pass
(signiert, öffentlicher Teil online)"] ``` 1. Das **ERP** hält fest, was geschieht (eine Fertigungsrückmeldung, ein Wareneingang, eine zurückgegebene Batterie), und legt die Änderung in einer Ausgangstabelle (Outbox) ab oder löst ein Ereignis aus. 2. Die **Middleware** (ein eigener Dienst oder eine iPaaS) holt die Änderung ab, bringt sie in die Form, die BatteryPass erwartet, und ruft die [API](https://passport.kabin.sh/docs/de/api-reference) mit einem Schlüssel Ihrer Organisation auf. 3. **BatteryPass** prüft jede Anfrage gegen den Scope des Schlüssels, validiert die Daten nach denselben Regeln wie das Portal und hält jeden Schreibvorgang im Audit-Log fest. 4. **Mitglieder** Ihrer Organisation prüfen im Portal: Sie übernehmen bereitgestellte Importe, geben Werte frei und veröffentlichen Pässe. Ein Schlüssel kann nichts davon. 5. Die Middleware liest über den [Änderungsfeed](https://passport.kabin.sh/docs/de/api-reference/changes/list-changes) zurück, was sie braucht (Passstatus, GS1 Digital Link, QR-Code), und schreibt es an den Satz im ERP. ## Wer was tut | Beteiligte | Kann | Kann nicht | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | | Schlüssel mit Scope `import` | Eine CSV- oder XLSX-Datei hochladen, zuordnen (auch mit einer Importvorlage), prüfen lassen und das Ergebnis lesen | Den Import übernehmen, Werte freigeben, veröffentlichen | | Schlüssel mit Scope `condition` | Gemessene Zustandsdaten und Lebenszyklus-Ereignisse melden | Deklarierte Daten wie Kapazität oder Zellchemie ändern | | Schlüssel mit Scope `read` | Modelle, Einheiten, Werte, Pässe, Exporte und den Änderungsfeed lesen | Etwas schreiben | | Bearbeiter (Mitglied) | Einen geprüften Import übernehmen; die Einheiten entstehen als Entwürfe | Werte freigeben oder veröffentlichen | | Prüfer, Admin, Inhaber (Mitglied) | Importierte Werte freigeben, Pässe veröffentlichen | Selbst erfasste Werte freigeben, solange das Vier-Augen-Prinzip eingeschaltet ist; veröffentlichen, solange Pflichtangaben fehlen | | Inhaber (Mitglied) | API-Schlüssel unter **Einstellungen → API-Schlüssel** anlegen und widerrufen | | Das Vier-Augen-Prinzip ist eine Einstellung Ihrer Organisation. Zustandsdaten, die ein Schlüssel meldet, werden als freigegebene Messungen ohne Prüfer gespeichert; so sind Messdaten gedacht (siehe [Zustandsdaten und Lebenszyklus](https://passport.kabin.sh/docs/de/concepts/condition-and-lifecycle)). ## Was nie fließt BatteryPass braucht Daten über Batterien, nicht über Menschen. Behalten Sie diese Daten in Ihrem ERP: - Namen, Adressen, Kontaktdaten und Auftragsnummern von Kundinnen und Kunden; - Namen von Beschäftigten, Bedienpersonal oder Servicetechnikern; - Preise, Margen und Lieferantenkonditionen; - Fahrzeug-, Halter- oder Standortdaten einer Batterie im Betrieb. Die Seriennummer genügt, um einen Pass, ein Ereignis oder eine Messung mit dem Satz in Ihrem ERP zu verbinden. Die Seite [Sicherheit](https://passport.kabin.sh/docs/de/integrations/security) erklärt Datenminimierung und das übrige Sicherheitsmodell. ## Die fünf Abläufe [**Einheiten aus der Fertigung**](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production): Seriennummern werden als Import bereitgestellt; Mitglieder übernehmen, geben frei und veröffentlichen. [**Pass-Links zurück ins ERP**](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp): Dem Änderungsfeed folgen und Digital Link und Status am Seriennummernsatz speichern. [**Zustandsdaten aus BMS und Service**](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms): Messwerte mit ihrem Zeitpunkt melden; ältere werden übersprungen. [**Rücksendungen und Reparaturen**](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs): Abgeschlossene Serviceaufträge werden Lebenszyklus-Ereignisse, ohne Kundendaten. [**Produktänderung**](https://passport.kabin.sh/docs/de/integrations/workflows/product-change): Eine Änderung am Produktstamm benachrichtigt eine Person, die eine neue Modellversion anlegt. [**Sicherheitsmodell**](https://passport.kabin.sh/docs/de/integrations/security): Schlüssel, Scopes, Wiederholungen, Outbox, Datenminimierung und die OWASP API Top 10. ## Middleware und Standards - [iPaaS und Middleware](https://passport.kabin.sh/docs/de/integrations/ipaas): n8n, Make, Power Automate, MuleSoft, Boomi, Workato, Zapier und SAP Integration Suite mit einem BatteryPass-Schlüssel, und wie jede davon den Änderungsfeed abfragt. - [Standards](https://passport.kabin.sh/docs/de/integrations/standards): wie BatteryPass-Daten zu DIN DKE SPEC 99100, den IDTA-Teilmodellen des Batteriepasses und Catena-X stehen. ## ERP-Systeme Das ERP-Register nennt je System die Schnittstellen, die eine Integration nutzen kann, wie sie sich anmelden, ob sie Änderungen melden, und wie und wann wir jede Angabe geprüft haben. Es sind dieselben Daten, die das Portal unter **Einstellungen → Integrationen** zeigt. | System | Hersteller | Betriebsarten | Prüfstand | | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ----------------- | ----------------------------------------------------- | | [SAP S/4HANA Cloud Public Edition](https://passport.kabin.sh/docs/de/integrations/erp/sap-s4hana-cloud.md) | SAP | Cloud | geprüft in der Herstellerdokumentation | | [SAP S/4HANA (on-premise and Private Edition)](https://passport.kabin.sh/docs/de/integrations/erp/sap-s4hana-on-premise.md) | SAP | On-Premise | geprüft in der Herstellerdokumentation | | [SAP Business One](https://passport.kabin.sh/docs/de/integrations/erp/sap-business-one.md) | SAP | On-Premise | geprüft in der Herstellerdokumentation | | [Microsoft Dynamics 365 Business Central](https://passport.kabin.sh/docs/de/integrations/erp/dynamics-365-business-central.md) | Microsoft | Cloud, On-Premise | geprüft in der Herstellerdokumentation | | [Microsoft Dynamics NAV 2018](https://passport.kabin.sh/docs/de/integrations/erp/dynamics-nav-2018.md) | Microsoft | On-Premise | geprüft in der Herstellerdokumentation | | [Microsoft Dynamics 365 Finance and Supply Chain Management](https://passport.kabin.sh/docs/de/integrations/erp/dynamics-365-finance-operations.md) | Microsoft | Cloud | geprüft in der Herstellerdokumentation | | [Oracle NetSuite](https://passport.kabin.sh/docs/de/integrations/erp/oracle-netsuite.md) | Oracle | Cloud | geprüft in der Herstellerdokumentation | | [Oracle Fusion Cloud Supply Chain Management](https://passport.kabin.sh/docs/de/integrations/erp/oracle-fusion-scm.md) | Oracle | Cloud | geprüft in der Herstellerdokumentation | | [Odoo](https://passport.kabin.sh/docs/de/integrations/erp/odoo.md) | Odoo S.A. | Cloud, On-Premise | geprüft in der Herstellerdokumentation | | [Infor CloudSuite (M3, LN)](https://passport.kabin.sh/docs/de/integrations/erp/infor-cloudsuite.md) | Infor | Cloud, On-Premise | geprüft in der Herstellerdokumentation; nicht geprüft | | [IFS Cloud](https://passport.kabin.sh/docs/de/integrations/erp/ifs-cloud.md) | IFS | Cloud, On-Premise | nur Community- oder Werbequelle | | [Epicor Kinetic](https://passport.kabin.sh/docs/de/integrations/erp/epicor-kinetic.md) | Epicor | Cloud, On-Premise | nur Community- oder Werbequelle | | [Sage X3](https://passport.kabin.sh/docs/de/integrations/erp/sage-x3.md) | Sage | Cloud, On-Premise | nicht geprüft | | [abas ERP](https://passport.kabin.sh/docs/de/integrations/erp/abas-erp.md) | abas Software (Forterro) | On-Premise, Cloud | nur Community- oder Werbequelle | | [proALPHA ERP](https://passport.kabin.sh/docs/de/integrations/erp/proalpha.md) | proALPHA | On-Premise | nicht geprüft | | [weclapp](https://passport.kabin.sh/docs/de/integrations/erp/weclapp.md) | weclapp | Cloud | geprüft in der Herstellerdokumentation | | [Xentral](https://passport.kabin.sh/docs/de/integrations/erp/xentral.md) | Xentral ERP Software | Cloud | geprüft in der Herstellerdokumentation | > [!NOTE] > „Geprüft in der Herstellerdokumentation“ heißt: Wir haben die öffentliche Dokumentation des Herstellers am > angegebenen Tag gelesen. Das ist kein Test gegen ein laufendes System und keine Bestätigung durch den Hersteller. > Einträge mit „nur Community- oder Werbequelle“ oder „nicht geprüft“ müssen Sie im Projekt bestätigen. --- # Das Sicherheitsmodell einer ERP-Integration > Schlüssel und Scopes, Geheimnisse und Rotation, Transport, sichere Wiederholungen, Outbox, dünne Ereignisse, veraltete Schreibvorgänge, Datenminimierung, Audit-Trail und die OWASP API Top 10. Eine Integration zwischen Ihrem ERP und BatteryPass bewegt Daten, die in signierten, öffentlichen Batteriepässen landen. Diese Seite sammelt die Regeln, die diesen Weg sicher machen. Die meisten liegen auf Ihrer Seite der Verbindung; wo BatteryPass etwas selbst durchsetzt, steht es dabei. Den Umgang mit Schlüsseln im Alltag beschreibt [Schlüssel sicher verwalten](https://passport.kabin.sh/docs/de/guides/secure-key-handling). ## Schlüssel und Scopes **Ein Schlüssel je Integration und je Umgebung.** Eine Test-Middleware und die produktive Middleware bekommen verschiedene Schlüssel, ebenso zwei Integrationen in derselben Middleware. So können Sie einen widerrufen, ohne die anderen anzuhalten, und die Spalte **Zuletzt verwendet** im Portal zeigt, welcher aktiv ist. **So wenig Scope wie möglich.** Ein Schlüssel hat einen oder mehrere von drei Scopes: | Scope | Erlaubt dem Schlüssel | Abläufe | | ----------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `read` | Modelle, Einheiten, Werte, Pässe, Exporte und den Änderungsfeed lesen | [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp), [Produktänderung](https://passport.kabin.sh/docs/de/integrations/workflows/product-change), Abfragen in den übrigen Abläufen | | `condition` | Gemessene Zustandsdaten und Lebenszyklus-Ereignisse melden | [Zustandsdaten](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms), [Rücksendungen und Reparaturen](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs) | | `import` | Tabellenimporte von Einheiten hochladen, zuordnen, prüfen lassen und lesen | [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) | Kein Scope erlaubt es, einen Import zu übernehmen, einen Wert freizugeben, zu veröffentlichen, zurückzuziehen, Mitglieder oder Schlüssel zu verwalten oder deklarierte Daten zu ändern. Das bleibt bei Mitgliedern Ihrer Organisation im Portal. Ein Schlüssel handelt mit den Rechten eines Betrachters plus seinen Scopes und ist nie auf einzelne Einheiten beschränkt: Ein `condition`-Schlüssel kann für jede Einheit Ihrer Organisation melden. ## Geheimnisse - Bewahren Sie jeden Schlüssel in einem Secrets-Manager oder Vault auf (HashiCorp Vault, Azure Key Vault, AWS Secrets Manager, der Anmeldedatenspeicher der iPaaS) und übergeben Sie ihn dem Prozess beim Start. Nie in Code, versionierte Konfiguration, Container-Images, URLs, Logs oder Tickets. Das [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) beschreibt die Praxis im Detail. - Senden Sie den Schlüssel nur im Header `Authorization: Bearer bp_live_…`. Eine Anfrage, die zusätzlich ein Sitzungs-Cookie des Portals trägt, wird abgelehnt; so ist nie unklar, wer handelt. - Maskieren Sie den Header `Authorization` in jedem Log Ihrer Middleware und Ihres HTTP-Clients. **Rotation.** Schlüssel laufen nicht ab. Rotieren Sie nach eigenem Plan und immer dann, wenn jemand ausscheidet, der einen Schlüssel kannte: neuen Schlüssel mit denselben Scopes anlegen, die Middleware darauf umstellen, prüfen, dass der neue Schlüssel eine aktuelle Zeit unter **Zuletzt verwendet** zeigt, dann den alten widerrufen. Während der Umstellung funktionieren beide, es fällt also nichts aus. Der Widerruf wirkt sofort und endgültig. ## Transport - Rufen Sie BatteryPass nur über HTTPS (TLS) auf und prüfen Sie das Serverzertifikat. Schalten Sie die Prüfung auch in Testumgebungen nicht ab: Middleware-Konfigurationen wandern gern von Test nach Produktion. - **Egress-Allowlisting** als zweite Linie: Erlauben Sie der Middleware nur die Adresse von BatteryPass und das ERP. Das ersetzt weder Schlüssel noch TLS, begrenzt aber, mit wem eine kompromittierte Middleware sprechen kann. - Umgekehrt genauso: Machen Sie die APIs des ERP nur der Middleware zugänglich, nie direkt aus dem Internet. ## Wiederholungen ohne Duplikate Netze fallen aus. Eine Anfrage mit Zeitüberschreitung wurde vielleicht ausgeführt, vielleicht nicht; Wiederholungen müssen also sicher sein. **Zurückhaltend wiederholen.** Wiederholen Sie Netzfehler und `5xx`-Antworten mit exponentiellem Backoff und Jitter (etwa 1, 2, 4, 8 Sekunden plus ein Zufallsanteil, höchstens fünf Versuche). Auf `429 rate_limited` warten Sie die Sekunden aus dem Header `Retry-After` ab; alle Schlüssel Ihrer Organisation teilen sich 600 Anfragen pro Minute. Andere `4xx`-Antworten wiederholen Sie nicht: Die Anfrage wurde als Ganzes abgelehnt, korrigieren Sie sie. Einzelheiten stehen in [Fehler und Wiederholungen behandeln](https://passport.kabin.sh/docs/de/guides/handle-errors) und [Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits). **Jede Wiederholung sicher machen.** Einen Header `Idempotency-Key` nimmt BatteryPass nur bei Sammel- und Veröffentlichungsoperationen des Portals an, die Schlüssel nicht aufrufen können. Bei den Operationen eines Schlüssels kommt die Sicherheit aus den Operationen selbst und aus Ihrer Middleware: | Operation | Wiederholbar | Warum, oder was Sie tun | | ------------------------------------------------------------------------------------------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Jedes `GET` (Lesen, Änderungsfeed, `getImport`) | Ja | Sie ändern nichts. | | [`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition) | Ja, mit demselben `measured_at` | Eine gleich alte oder ältere Messung als die gespeicherte wird übersprungen. | | [`createImport`](https://passport.kabin.sh/docs/de/api-reference/imports/create-import) | Ja, mit Bedacht | Ein wiederholter Upload legt einen zweiten Import an. Seine Zeilen werden zu Duplikaten, sobald der erste übernommen ist, sodass keine Einheit doppelt entsteht; Mitglieder übernehmen den überzähligen Import einfach nicht. | | [`updateImportMapping`](https://passport.kabin.sh/docs/de/api-reference/imports/update-import-mapping) | Ja | Mit demselben `If-Match` wird die Wiederholung als veraltet abgelehnt (`409`); lesen Sie den Import, sehen Sie die Zuordnung bereits angewandt. | | [`validateImport`](https://passport.kabin.sh/docs/de/api-reference/imports/validate-import) | Ja | Ein zweiter Start, während die erste Prüfung läuft, wird abgelehnt (`409`); fragen Sie stattdessen den Import ab. | | [`createLifecycleEvent`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/create-lifecycle-event) | Nein | Jeder Aufruf legt ein neues Ereignis an. Merken Sie sich, welchen Quellsatz (etwa den Serviceauftrag) Sie schon gemeldet haben, und sehen Sie nach einer Zeitüberschreitung in [`listLifecycleEvents`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events) nach, bevor Sie erneut senden. | Die IETF arbeitet an einem allgemeinen Header `Idempotency-Key` ([draft-ietf-httpapi-idempotency-key-header](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/), Entwurf 08, noch kein RFC). Für Ihre eigenen APIs ist das ein nützlicher Hintergrund; BatteryPass nimmt ihn bei den Operationen oben nicht an. ## Die Outbox auf ERP-Seite Das ERP darf keine Änderung verlieren, während die Middleware oder BatteryPass nicht erreichbar ist, und es darf BatteryPass nicht innerhalb seiner eigenen Geschäftstransaktion aufrufen. Die **transaktionale Outbox** löst beides: In derselben Datenbanktransaktion, die den Wareneingang bucht, schreibt das ERP (oder eine Erweiterung) eine Zeile in eine Ausgangstabelle. Die Middleware liest die Outbox, ruft BatteryPass auf und markiert die Zeile erst nach einer erfolgreichen Antwort als erledigt. Das ergibt **Zustellung mindestens einmal**: Eine Zeile kann zweimal gesendet werden (bei einem Absturz zwischen Aufruf und Markierung), nie keinmal. Kombinieren Sie das mit den Regeln oben und führen Sie eine kleine Tabelle dessen, was Sie schon gesendet haben (Quellsatz, BatteryPass-ID, Zeitpunkt), damit Sie auf Ihrer Seite Duplikate erkennen. ## Dünne Ereignisse, dann abholen Bewegen Sie keine Daten in Benachrichtigungen. Beide Richtungen funktionieren gleich: - Vom ERP: Viele ERP-Systeme senden dünne Ereignisse (eine Produktnummer und die Art der Änderung). Die Middleware liest danach den aktuellen Satz über die API des ERP, prüft Signatur, Handshake oder geheime URL des Herstellers und verwirft Ereignisse, die sie nicht prüfen kann. - Von BatteryPass: Der [Änderungsfeed](https://passport.kabin.sh/docs/de/api-reference/changes/list-changes) nennt nur `{id, occurred_at, entity_type, entity_id, action}`, nie Werte, Handelnde oder personenbezogene Daten. Die Middleware liest die Entität danach mit demselben Schlüssel; der Feed verrät für sich allein also nichts und ist immer aktuell. Der Feed liefert eine Änderung erst, wenn jede ältere Transaktion in BatteryPass beendet ist; ein gespeicherter Cursor überspringt also nie eine Änderung, die spät festgeschrieben wurde. Speichern Sie den Cursor erst, wenn Ihre Seite die Seite verarbeitet hat. Ausgehende Webhooks sendet BatteryPass noch nicht; sie sind geplant und brauchen eine eigene Entscheidung (Signatur, Verwaltung der Abonnements, Schutz der Zieladressen). Bis dahin fragen Sie ab. ## Veraltete Schreibvorgänge Änderbare Ressourcen tragen eine `row_version`. [`updateImportMapping`](https://passport.kabin.sh/docs/de/api-reference/imports/update-import-mapping) verlangt `If-Match: ""`; hat jemand anderes den Import inzwischen geändert, lautet die Antwort `409 conflict`, und nichts wird überschrieben. Lesen Sie den Import neu und entscheiden Sie. Zustandsdaten haben eine eigene Regel: Eine ältere Messung ersetzt nie eine neuere. ## Datenminimierung Die DSGVO verlangt Daten, die „dem Zweck angemessen und erheblich sowie auf das notwendige Maß beschränkt“ sind (Art. 5 Abs. 1 lit. c). Für einen Batteriepass heißt das: Daten über die Batterie, also Seriennummer, Modell, Daten, Messwerte, Ereignisse. Namen und Adressen von Kundinnen und Kunden, Auftragsnummern, Namen von Beschäftigten, Preise, Fahrzeug- und Standortdaten bleiben im ERP. Jeder [Ablauf](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) nennt, was er sendet und was er weglässt; der [Überblick](https://passport.kabin.sh/docs/de/integrations) nennt, was nie fließt. Freitext ist die Stelle, an der personenbezogene Daten hineinrutschen: Eine Ereignisbeschreibung wie „repariert für Herrn X in Y“ landet in der Historie Ihrer Batterie. Füllen Sie Beschreibungen aus festen Codes oder Vorlagen, nicht aus dem Freitext eines Serviceauftrags. ## Der Audit-Trail - Jeder Schreibvorgang mit einem Schlüssel steht im Audit-Log Ihrer Organisation, mit dem Handelnden-Typ `api_key` und dem Namen des Schlüssels: Bereitstellen und Prüfen eines Imports, eine Zustandsmeldung, ein Lebenszyklus-Ereignis. - Importe, die ein Schlüssel bereitgestellt hat, nennen den Schlüssel statt eines Mitglieds (`created_by_api_key`), in der API und im Portal. - Werte, die ein Schlüssel schreibt, tragen den Schlüssel nicht selbst. Eine Zustandsmeldung legt ein Lebenszyklus-Ereignis `condition_update` an, und dieses Ereignis nennt den Schlüssel (`api_key`). - Zustandsdaten werden als freigegebene Messungen ohne Prüfer gespeichert. Deklarierte Daten nehmen den geprüften Weg: Importierte Werte gibt ein Prüfer frei, und ob eine zweite Person freigeben muss, legt die Vier-Augen-Einstellung Ihrer Organisation fest. - Lesezugriffe werden nicht festgehalten. Gehen Sie davon aus, dass die Daten eines abhandengekommenen Schlüssels gelesen wurden. ## OWASP API Security Top 10 Wie die Risiken der [OWASP API Security Top 10 (2023)](https://api-security.owasp.org/editions/2023/en/0x11-t10), die für eine ERP-Integration am meisten zählen, behandelt werden, und was Ihr Teil bleibt: | Risiko | In BatteryPass | Ihr Teil | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | API1:2023 Broken Object Level Authorization | Jede Abfrage ist auf die Organisation des Schlüssels gefiltert; die ID einer anderen Organisation ergibt `404`. | Ein Schlüssel je angebundener Organisation; nie Mandanten in einem Middleware-Ablauf mischen. | | API4:2023 Unrestricted Resource Consumption | 600 Anfragen pro Minute für alle Schlüssel einer Organisation, 30 Versuche je 15 Minuten für unbekannte Schlüssel, Request-Bodys bis 1 MiB, Uploads bis 10 MB und 10 000 Zeilen, Listen mit höchstens 200 Einträgen. | Bei `429` zurückhalten, mit `limit` und `cursor` blättern, Dateien in Stapeln senden. | | API5:2023 Broken Function Level Authorization | Jede Operation nennt ihren Scope (`x-api-key-scope`); ein Schlüssel ohne ihn erhält `403`, Operationen des Portals nehmen gar keine Schlüssel an (`401`). | Jedem Schlüssel nur die Scopes geben, die sein Ablauf nennt. | | API7:2023 Server Side Request Forgery | BatteryPass ruft keine Adressen auf, die eine Integration liefert; Webhooks gibt es noch nicht. | Holt die Middleware URLs aus ERP-Ereignissen ab, die Ziele per Allowlist begrenzen. | | API10:2023 Unsafe Consumption of APIs | BatteryPass prüft jeden Wert nach den Regeln des Registers, gleich aus welcher Quelle. | ERP-Ereignisse und Antworten von BatteryPass als nicht vertrauenswürdige Eingaben behandeln: prüfen, Signaturen kontrollieren, wo der Hersteller sie anbietet, und empfangene Inhalte nie ausführen. | --- # iPaaS und Middleware > Die BatteryPass-API aus n8n, Make, Power Automate, MuleSoft, Boomi, Workato, Zapier und SAP Integration Suite mit einem Schlüssel aus dem Geheimnisspeicher der Plattform aufrufen und den Änderungsfeed abfragen. Die meisten ERP-Integrationen laufen auf einer Integrationsplattform statt in eigenem Code. Jede Plattform unten kann BatteryPass aufrufen, denn die API braucht nur zwei Dinge: eine HTTPS-Anfrage mit dem Header `Authorization: Bearer ` und JSON (oder `multipart/form-data` für Importe). Diese Seite zeigt, wo der Schlüssel auf jeder Plattform hingehört und wie jede dem [Änderungsfeed](https://passport.kabin.sh/docs/de/api-reference/changes/list-changes) folgt. Die Plattformen sind nach ihrer öffentlichen Dokumentation beschrieben. BatteryPass bietet auf keiner davon einen eigenen Konnektor an und hat keine Partnerschaft mit ihren Herstellern. ## Was alle Plattformen gleich machen - **Der Schlüssel liegt im Anmeldedatenspeicher der Plattform**, nie in einer Flow-Definition, einem Skript oder einer Variablen, die in Ausführungsprotokollen erscheint. Ein Schlüssel je Flow und Umgebung (siehe [Sicherheit](https://passport.kabin.sh/docs/de/integrations/security)). - **Der Vertrag.** BatteryPass veröffentlicht seinen Integrationsvertrag als OpenAPI 3.0.3 unter `/api/v1/openapi.json` und `/api/v1/openapi.yaml` auf Ihrer BatteryPass-Adresse und zum Herunterladen in der [API-Referenz](https://passport.kabin.sh/docs/de/api-reference). Plattformen, die OpenAPI 3 importieren, bauen daraus ihre Operationen. - **Abfragen statt Webhooks.** Ausgehende Webhooks sendet BatteryPass noch nicht; sie sind geplant und brauchen eine eigene Entscheidung. Planen Sie einen Flow (alle paar Minuten), der `GET /api/v1/changes` mit dem gespeicherten Cursor aufruft, die Einträge der Reihe nach verarbeitet und `next_cursor` speichert, wenn er nicht `null` ist. Das Muster ist überall gleich: ```text title="Abfrageschleife" cursor = aus dem Datenspeicher der Plattform laden (beim ersten Lauf leer) wiederholen: GET /api/v1/changes?types=passport.published,passport.republished&limit=100&cursor={cursor} für jeden Eintrag: Entität lesen, ERP aktualisieren wenn next_cursor null ist: aufhören (eine leere Seite behält den alten Cursor) cursor = next_cursor, speichern (erst, nachdem das ERP die Seite angenommen hat) ``` - **Fehler.** `429` nach `Retry-After` und `5xx` mit Backoff wiederholen, andere `4xx` nicht (siehe [Fehler und Wiederholungen behandeln](https://passport.kabin.sh/docs/de/guides/handle-errors)). ## Plattformen **n8n** Nutzen Sie den Knoten **HTTP Request** mit einer Anmeldung vom Typ **Header Auth**: Name `Authorization`, Wert `Bearer bp_live_…` ([n8n: HTTP-Request-Anmeldedaten](https://docs.n8n.io/integrations/builtin/credentials/httprequest/)). Die Anmeldung wird verschlüsselt gespeichert und in Ausführungen nicht angezeigt. Abfragen: Ein **Schedule Trigger** startet den Flow; den Cursor halten Sie in den statischen Daten des Workflows oder in einer kleinen Datenbanktabelle, rufen `GET /changes` in einem HTTP-Request-Knoten auf, teilen die Einträge auf und wiederholen, solange `next_cursor` gesetzt ist. Für Importe senden Sie `multipart/form-data` mit einer binären Eigenschaft für `file` und einem Formularfeld `preset`. **Make** Nutzen Sie die App **HTTP** und legen Sie den Schlüssel in einem Keychain vom Typ **API key** ab, der den Header `Authorization` mit `Bearer bp_live_…` setzt ([Make: HTTP](https://apps.make.com/http)). Keychains halten das Geheimnis aus dem Szenario-Export heraus. Abfragen: ein geplantes Szenario, der Cursor in einem **Data Store**; der **Repeater** oder ein Iterator geht die Seiten durch, bis `next_cursor` leer ist. **Power Automate und Azure Logic Apps** Benutzerdefinierte Konnektoren akzeptieren **nur Definitionen in OpenAPI 2.0 (Swagger)**, und die Datei muss kleiner als 1 MB sein; Authentifizierung per API-Schlüssel wird unterstützt ([Microsoft: FAQ zu benutzerdefinierten Konnektoren](https://learn.microsoft.com/en-us/connectors/custom-connectors/faq)). BatteryPass veröffentlicht heute OpenAPI 3.0.3; unseren Vertrag können Sie also nicht direkt als benutzerdefinierten Konnektor importieren. Ein Export des Integrationsvertrags in Swagger 2.0 ist als nächster Schritt geplant. Bis dahin nutzen Sie die eingebaute Aktion **HTTP**: Legen Sie den Schlüssel in Azure Key Vault ab, lesen Sie ihn mit dem Key-Vault-Konnektor (markieren Sie die Ausgabe als sicher, damit sie nicht im Ausführungsverlauf erscheint) und setzen Sie den Header `Authorization: Bearer @{…}`. Abfragen: ein Trigger **Wiederholung**, der Cursor in einer Tabelle in Dataverse oder Azure Table Storage und eine Schleife **Do until** über die Seiten. **MuleSoft Anypoint** **REST Connect** in Anypoint Exchange erzeugt einen Konnektor aus einer Spezifikation in OpenAPI 2.0 oder 3.0 ([MuleSoft: REST Connect](https://docs.mulesoft.com/exchange/to-deploy-using-rest-connect)); das veröffentlichte `openapi.json` kann also die Quelle sein. Alternativ rufen Sie die API mit dem Konnektor HTTP Request auf. Halten Sie den Schlüssel in verschlüsselten Konfigurationseigenschaften oder einem Secrets-Manager und setzen Sie ihn als Header `Authorization` der Konnektor-Konfiguration. Abfragen: eine Quelle **Scheduler** mit dem Cursor in einem Object Store. **Boomi** Der Konnektor **OpenAPI** importiert OpenAPI 3.0 und neuer ([Boomi: OpenAPI-Konnektor](https://help.boomi.com/docs/Atomsphere/Integration/Connectors/int-OpenAPI_connector_6766251b-b601-41f5-8c13-86d79faf52b9)). Importieren Sie den veröffentlichten Vertrag oder nutzen Sie den Konnektor HTTP Client. Speichern Sie den Schlüssel als verschlüsseltes Verbindungsfeld oder in einer als Passwort markierten Extension und senden Sie ihn als Header `Authorization`. Abfragen: ein geplanter Prozess mit dem Cursor in einer persistenten Prozesseigenschaft. **Workato** Nutzen Sie den Konnektor **HTTP** mit einer Verbindung, die den Header `Authorization: Bearer …` setzt; Workato verschlüsselt Verbindungsfelder. Trigger, die Webhooks empfangen, brauchen einen eigenen Konnektor aus dem Connector-SDK ([Workato: dynamische Webhook-Trigger](https://docs.workato.com/en/developing-connectors/sdk/guides/building-triggers/dynamic-webhook.html)); weil BatteryPass noch keine Webhooks hat, nutzen Sie ein **geplantes Rezept**, das `GET /changes` abfragt und den Cursor in einer Lookup-Tabelle hält. **Zapier** Die Sofort-Trigger von Zapier nutzen **REST Hooks**, die auf API-Seite Endpunkte zum An- und Abmelden brauchen ([Zapier: REST-Hook-Trigger](https://docs.zapier.com/integrations/build/hook-trigger)). BatteryPass bietet sie noch nicht an. Nutzen Sie stattdessen einen abfragenden Trigger: **Webhooks by Zapier** (Retrieve Poll) oder den Polling-Trigger einer privaten App gegen `GET /changes`, mit dem Schlüssel in einem Authentifizierungsfeld für API-Schlüssel. Zapier erkennt abgefragte Einträge an ihrer `id` wieder, die jede Änderung trägt. **SAP Integration Suite (Cloud Integration)** Nutzen Sie den **HTTP-Receiver-Adapter**. Legen Sie den Schlüssel als sicheren Parameter im Security Material des Tenants ab und setzen Sie den Header `Authorization` daraus in einem Content Modifier oder Skript, nie als einfachen externalisierten Parameter. Achtung: Cloud Integration kann eigene Header aus der Nachricht entfernen, wenn sie nicht erlaubt sind. Tragen Sie `Authorization` (oder den Header, den Sie setzen) unter **Allowed Header(s)** in der Laufzeitkonfiguration des Integrationsflusses ein; der SAP-Hinweis 3075702 beschreibt das Symptom nicht gesendeter Header. Abfragen: ein Start-Ereignis **Timer**, der Cursor in einem Data Store oder einer globalen Variablen und ein Looping Process Call über die Seiten. ## Importe von einer Plattform hochladen [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) braucht `multipart/form-data`: die Datei im Teil `file` (mit Dateiname und Inhaltstyp `text/csv` oder dem XLSX-Typ) und die Kennung der Importvorlage im Teil `preset`. Jede Plattform oben kann Multipart-Anfragen aus ihrem HTTP-Konnektor senden; bauen Sie die CSV im Flow und hängen Sie sie als binären Inhalt an. Fragen Sie danach `GET /imports/{id}` ab, bis `status` den Wert `validated` oder `failed` hat. --- # Wie BatteryPass zu Standards für den Batteriepass steht > DIN DKE SPEC 99100, die IDTA-Teilmodelle des digitalen Batteriepasses, Catena-X CX-0160 und das BatteryPass-Datenmodell, und was BatteryPass heute abbildet und was nicht. Rechtsgrundlage des Batteriepasses ist die Verordnung (EU) 2023/1542, Artikel 77 und Anhang XIII. Mehrere Standards und Datenmodelle beschreiben, wie die Daten des Passes gegliedert und ausgetauscht werden können. Diese Seite sagt klar, wie die Daten in BatteryPass zu jedem davon stehen, was heute abgebildet ist und was nicht. Nichts davon sagt zu, dass Ihre Pässe die Anforderungen erfüllen: Das hängt von Ihren Daten ab und bleibt Ihre Verantwortung. ## Überblick | Standard oder Modell | Was es ist | Stand (geprüft am 24.10.2026) | BatteryPass heute | | ----------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | DIN DKE SPEC 99100 | Anforderungen an die Datenattribute des Batteriepasses | Veröffentlicht 2025-02 | Das Feldregister folgt seiner Gliederung; jeder Registerabschnitt nennt sein Kapitel | | IDTA 02035, Teile 1 bis 7 | Teilmodelle des digitalen Batteriepasses für die Verwaltungsschale | Version 1.0, Februar 2026 | Noch kein AAS-Export; Felder lassen sich über die gemeinsame Gliederung der DIN DKE SPEC zuordnen | | Catena-X CX-0160 | Batteriepass-Standard des Catena-X-Datenraums | Version 1.0.0 nur in der unveröffentlichten Vorschau CX-Neptune | Kein Catena-X-Konnektor; Kennungen werden wie unten beschrieben zugeordnet | | BatteryPass-Ready-Datenmodell | JSON-Schemas des Konsortiums BatteryPass-Ready | Version 2.0 (Schemas 2.0.1); das ältere Konsortialmodell 1.2.0 wird nicht mehr gepflegt | Export jeder veröffentlichten Version als BatteryPass-v2.0-JSON, mit beratender Schemaprüfung | | GBA Battery Passport | Pass der Global Battery Alliance | Freiwillig, außerhalb der EU-Verordnung | Wird nicht erzeugt | ## DIN DKE SPEC 99100 [DIN DKE SPEC 99100](https://www.dinmedia.de/de/technische-regel/din-dke-spec-99100/385692321) (2025-02) listet die Datenattribute des Batteriepasses und ordnet sie in Kapitel. Das [Feldregister](https://passport.kabin.sh/docs/de/general/glossary) von BatteryPass ist entlang derselben Kapitel aufgebaut: Jeder Abschnitt, den [`getRegistrySchema`](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-schema) liefert, trägt in `din_spec` das Kapitel, das er abdeckt, und die Felder tragen ihre rechtlichen Bezüge. | Registerabschnitt | DIN DKE SPEC 99100 | | ------------------------------------------------------------------------------- | ------------------ | | `identification` (Identifikation und Produktdaten) | 6.1 | | `labeling` (Kennzeichnung und Konformität) | 6.2 | | `carbon_footprint` (CO₂-Fußabdruck) | 6.3 | | `due_diligence` (Sorgfaltspflichten) | 6.4 | | `materials`, `hazardous` (Materialien und Zusammensetzung, gefährliche Stoffe) | 6.5 | | `circularity` (Kreislauffähigkeit) | 6.6 | | `performance`, `condition` (Leistung und Haltbarkeit, dynamische Zustandsdaten) | 6.7 | Welche Felder eine Batteriekategorie ab wann braucht, entscheidet das Register; wo Register und Spezifikation im Detail abweichen, folgt das Register der Verordnung. ## IDTA Digital Battery Passport (Verwaltungsschale) Die Industrial Digital Twin Association hat im Februar 2026 die Teilmodellvorlagen IDTA 02035-1 bis 02035-7 „Digital Battery Passport“ in Version 1.0 veröffentlicht, gemeinsam mit Catena-X erarbeitet und an DIN DKE SPEC 99100 ausgerichtet. Sie beschreiben den Pass als Teilmodelle einer Verwaltungsschale (Asset Administration Shell, AAS); Teil 1 ist das digitale Typenschild, Teil 2 die Übergabedokumentation, die weiteren Teile folgen den Kapiteln der Spezifikation. BatteryPass exportiert heute keine AAS-Teilmodelle. Weil beide DIN DKE SPEC 99100 folgen, kann eine Middleware die Werte, die sie mit [`listBatteryValues`](https://passport.kabin.sh/docs/de/api-reference/values/list-battery-values) und [`listModelVersionValues`](https://passport.kabin.sh/docs/de/api-reference/values/list-model-version-values) liest, Kapitel für Kapitel den Teilmodell-Elementen zuordnen. Ein AAS-Export aus BatteryPass ist ein möglicher nächster Schritt, keine heutige Funktion. ## Catena-X CX-0160 Der Catena-X-Standard CX-0160 „Battery Passport“ (Version 1.0.0) gibt es nur in der Vorschau des Release CX-Neptune; er ist nicht freigegeben ([Vorschau CX-0160](https://catenax-ev.github.io/docs/next/standards/CX-0160-BatteryPassport)). In Catena-X ist ein Pass ein digitaler Zwilling, registriert in einem Digital Twin Registry (CX-0002) und über einen Eclipse Dataspace Connector (EDC) zwischen Partnern ausgetauscht, die über Geschäftspartnernummern ausgewiesen sind (BPNL für die juristische Person, BPNS für einen Standort). **BatteryPass ist kein Catena-X-Konnektor**: Es registriert keine digitalen Zwillinge, betreibt keinen EDC und hat keine Geschäftspartnernummern. Ein Hersteller, der an Catena-X teilnimmt, kann seinen eigenen Konnektor aus der BatteryPass-API speisen. Die Kennungen entsprechen sich so: | Catena-X | BatteryPass | | ----------------------------------------- | -------------------------------------------------------------------- | | `manufacturerPartId` (das Katalogteil) | das Batteriemodell: seine Modellkennung (`sku`) oder GTIN | | `partInstanceId` (das serialisierte Teil) | die `serial_number` der Einheit | | die eigene Kennung des Passes | die `passport_id` der Einheit (`urn:dpp:…`) und ihr GS1 Digital Link | ## BatteryPass-Ready-Datenmodell Das Datenmodell 1.2.0 des BatteryPass-Konsortiums wird nicht mehr gepflegt; sein Nachfolger ist das BatteryPass-Ready Data Model, dessen JSON-Schemas BatteryPass in Version 2.0.1 mitbringt. Für jede veröffentlichte Passversion liefert [`exportPassportJson`](https://passport.kabin.sh/docs/de/api-reference/passports/export-passport-json) die Version als BatteryPass-v2.0-JSON, erzeugt bei der Veröffentlichung. Die Schemaprüfung ist beratend: Die Version meldet `batterypass_v2_valid` und die Zahl der Befunde, und eine fehlschlagende Prüfung hält die Veröffentlichung nicht auf. Einzelheiten stehen in [Pässe exportieren](https://passport.kabin.sh/docs/de/guides/export-passports). ## GBA Battery Passport Die Global Battery Alliance betreibt ein freiwilliges Batteriepass-Programm mit eigenen Regeln und Kennzahlen. Es ist nicht der EU-Batteriepass, und BatteryPass erzeugt keine GBA-Pässe. --- # Einheiten aus der Fertigung > Seriennummern fertiger Batterien mit einem Schlüssel mit Scope import als Import bereitstellen; Mitglieder prüfen, übernehmen, geben frei und veröffentlichen im Portal. Wenn die Fertigung fertige Batterien zurückmeldet, sollen ihre Seriennummern zu Einheiten in BatteryPass werden, ohne dass jemand sie abtippt. In diesem Ablauf stellt die Middleware mit einem Schlüssel mit Scope `import` einen Tabellenimport bereit. **Das Übernehmen des Imports und das Freigeben seiner Werte bleiben bei Mitgliedern Ihrer Organisation im Portal**: Ein Schlüssel kann die Daten vorbereiten, deklarierte Daten gelangen aber nur durch die Entscheidung einer Person in BatteryPass (und, wo Ihre Organisation es eingeschaltet hat, im Vier-Augen-Prinzip). ```mermaid sequenceDiagram participant ERP participant MW as Middleware participant BP as BatteryPass-API participant M as Mitglied im Portal ERP->>ERP: Wareneingang buchen, Outbox-Zeile schreiben MW->>ERP: Outbox-Zeilen lesen (Seriennummer, GTIN, Datum) MW->>BP: createImport mit Datei und Preset (Scope import) BP-->>MW: 201 Import, Status uploaded MW->>BP: validateImport (Scope import) BP-->>MW: 202 Prüfauftrag loop alle paar Sekunden MW->>BP: getImport (Scope import) end BP-->>MW: Status validated, Zusammenfassung MW->>ERP: Zeilen als erledigt markieren, Zusammenfassung melden M->>BP: Prüfbericht lesen, Import übernehmen M->>BP: importierte Werte freigeben M->>BP: Pässe veröffentlichen ``` **Auslöser:** Die Fertigungsrückmeldung oder der Wareneingang aus der Fertigung bucht die Seriennummern fertiger Batterien. **Scopes des Schlüssels:** import | # | Wer | Was | Operation | | - | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | 1 | ERP | Bucht Rückmeldung oder Wareneingang mit den Seriennummern und hält die Änderung in einer Ausgangstabelle (Outbox) fest. | | | 2 | Middleware | Sammelt die Seriennummern einer Charge mit Modellkennung oder GTIN und Herstelldatum und schreibt eine CSV- oder XLSX-Datei mit den Spaltennamen der Vorlage (Preset). | | | 3 | Middleware | Lädt die Datei mit dem Preset hoch und merkt sich die Import-ID. | [`createImport`](https://passport.kabin.sh/docs/de/api-reference/imports/create-import.md) (Scope: import) | | 4 | Middleware | Startet die Prüfung aller Zeilen. | [`validateImport`](https://passport.kabin.sh/docs/de/api-reference/imports/validate-import.md) (Scope: import) | | 5 | Middleware | Fragt den Import ab, bis er validated oder failed ist, und meldet die Zusammenfassung (gültig, Warnungen, Fehler, Duplikate) an die Fertigung zurück. | [`getImport`](https://passport.kabin.sh/docs/de/api-reference/imports/get-import.md) (Scope: import) | | 6 | Mitglied im Portal | Prüft den Prüfbericht im Portal und übernimmt den Import; die Einheiten entstehen als Entwürfe. | | | 7 | Mitglied im Portal | Gibt die importierten Werte in einem Schritt frei (ADR-015); ist das Vier-Augen-Prinzip der Organisation eingeschaltet, bleiben Werte offen, die die freigebende Person selbst erfasst hat. | | | 8 | Mitglied im Portal | Veröffentlicht die Pässe, sobald keine Pflichtangabe mehr fehlt. | | **Sicherheit** - Ein eigener Schlüssel nur mit dem Scope import; er kann Importe bereitstellen, aber weder übernehmen noch freigeben. - Ein wiederholter Upload legt einen zweiten Import an; seine Zeilen werden zu Duplikaten, sobald der erste übernommen ist, sodass keine Einheit doppelt entsteht. - Die Outbox im ERP sorgt dafür, dass keine Buchung verloren geht, wenn die Middleware oder BatteryPass gerade nicht erreichbar ist. **Datenminimierung** - Die Datei enthält Seriennummer, Modellkennung oder GTIN, Herstelldatum und höchstens Produktionsstätte und Modellversion. - Kunden, Aufträge, Preise und Namen von Beschäftigten bleiben im ERP. ## Vorbereitung - Eine Inhaberin oder ein Inhaber legt unter **Einstellungen → API-Schlüssel** einen Schlüssel nur mit dem Scope `import` an, zum Beispiel `MES Linie 2 Importe`. Er kann weder Ihre Pässe lesen noch Zustandsdaten melden noch etwas übernehmen. - Die Batteriemodelle gibt es in BatteryPass mit GTIN oder Modellkennung, und eine Modellversion deckt die Herstelldaten ab, die Sie senden. Der Import findet das Modell jeder Zeile über GTIN, Modellkennung oder Produktname. - Wählen Sie eine [Importvorlage (Preset)](https://passport.kabin.sh/docs/de/integrations#erp-systeme), die zu dem passt, was Ihre Middleware schreibt. Die Vorlage `generic-csv` nimmt die Spalten `GTIN`, `Seriennummer`, `Herstelldatum`, `Produktionsstätte` (Name oder GLN), `Modellversion` und `Batteriestatus`, auf Deutsch oder Englisch. ERP-spezifische Vorlagen (SAP, Business Central, Odoo, NetSuite) stehen auf der jeweiligen [ERP-Seite](https://passport.kabin.sh/docs/de/integrations/erp/sap-s4hana-cloud). ## 1. Datei schreiben Die Middleware sammelt die Seriennummern einer Charge aus ihrer Outbox und schreibt eine CSV-Datei (UTF-8, getrennt durch `;`, `,`, Tabulator oder `|`) oder eine XLSX-Datei mit den Spaltennamen der Vorlage. Höchstens 10 MB, 10 000 Zeilen und 200 Spalten je Datei. Spalten, die die Vorlage nicht kennt, bleiben ohne Zuordnung; zusätzliche Spalten schaden also nicht. ```text title="charge-2026-10-24.csv" GTIN;Seriennummer;Herstelldatum;Produktionsstätte 04012345000023;SV2027-000901;2026-10-23;Werk Landshut 04012345000023;SV2027-000902;2026-10-23;Werk Landshut ``` ## 2. Mit der Vorlage hochladen [`createImport`](https://passport.kabin.sh/docs/de/api-reference/imports/create-import) nimmt die Datei als `multipart/form-data` im Feld `file` und die Vorlage im Feld `preset`. Die Antwort ist der Import mit der Zuordnung aus der Vorlage: ```bash title="cURL" curl -sS -X POST "$BATTERYPASS_URL/api/v1/imports" \ -H "Authorization: Bearer $BATTERYPASS_IMPORT_KEY" \ -F "file=@charge-2026-10-24.csv;type=text/csv" \ -F "preset=generic-csv" ``` ```ts title="TypeScript" import { readFile } from 'node:fs/promises'; const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const auth = { Authorization: `Bearer ${process.env.BATTERYPASS_IMPORT_KEY}` }; const csv = await readFile('charge-2026-10-24.csv', 'utf8'); const form = new FormData(); form.append('file', new Blob([csv], { type: 'text/csv' }), 'charge-2026-10-24.csv'); form.append('preset', 'generic-csv'); const res = await fetch(`${BASE}/imports`, { method: 'POST', headers: auth, body: form }); const imp = await res.json(); if (res.status !== 201) throw new Error(`createImport: ${res.status} ${imp.code} (request_id ${imp.request_id})`); console.log(imp.id, imp.status, imp.preset); ``` ```python title="Python" import os import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_IMPORT_KEY"] with open("charge-2026-10-24.csv", "rb") as f: res = session.post(f"{BASE}/imports", files={"file": ("charge-2026-10-24.csv", f, "text/csv")}, data={"preset": "generic-csv"}, timeout=60) imp = res.json() if res.status_code != 201: raise SystemExit(f"createImport: {res.status_code} {imp['code']} (request_id {imp.get('request_id')})") print(imp["id"], imp["status"], imp["preset"]) ``` ```json title="201 (gekürzt)" { "id": "0192f3c1-7a2b-7c3d-8e4f-5a6b7c8d9e0f", "status": "uploaded", "filename": "charge-2026-10-24.csv", "format": "csv", "row_count": 2, "headers": ["GTIN", "Seriennummer", "Herstelldatum", "Produktionsstätte"], "mapping": [ { "column": 0, "header": "GTIN", "target": "identification.gtin" }, { "column": 1, "header": "Seriennummer", "target": "identification.serial_number" }, { "column": 2, "header": "Herstelldatum", "target": "identification.manufacturing_date" }, { "column": 3, "header": "Produktionsstätte", "target": "identification.manufacturing_place" } ], "options": {}, "preset": "generic-csv", "created_by_api_key": { "id": "0192f0aa-1b2c-7d3e-8f40-5a6b7c8d9e01", "name": "MES Linie 2 Importe" }, "created_at": "2026-10-24T06:05:11Z", "updated_at": "2026-10-24T06:05:11Z", "row_version": 1 } ``` Der Import nennt den Schlüssel in `created_by_api_key`; Mitglieder sehen ihn in der Importliste des Portals als von diesem Schlüssel bereitgestellt. ## 3. Optional: Optionen setzen Hat die Datei keine Modell- oder keine Produktionsstätten-Spalte, setzen Sie den Wert für alle Zeilen mit [`updateImportMapping`](https://passport.kabin.sh/docs/de/api-reference/imports/update-import-mapping). Die Operation ersetzt die Zuordnung; senden Sie also die Vorlage erneut (oder ein vollständiges `mapping`, nie beides) und `If-Match` mit der `row_version` des Imports: ```bash title="cURL" curl -sS -X PUT "$BATTERYPASS_URL/api/v1/imports/0192f3c1-7a2b-7c3d-8e4f-5a6b7c8d9e0f/mapping" \ -H "Authorization: Bearer $BATTERYPASS_IMPORT_KEY" \ -H 'If-Match: "1"' \ -H "Content-Type: application/json" \ -d '{"preset": "generic-csv", "options": {"facility_id": "0192f09e-4a5b-7c6d-9e8f-0a1b2c3d4e5f"}}' ``` Wurde der Import inzwischen geändert, lautet die Antwort `409 conflict`: Lesen Sie ihn mit [`getImport`](https://passport.kabin.sh/docs/de/api-reference/imports/get-import) neu und wiederholen Sie mit der neuen `row_version`. ## 4. Prüfen lassen und warten [`validateImport`](https://passport.kabin.sh/docs/de/api-reference/imports/validate-import) startet einen Auftrag, der jede Zeile mit den Prüfregeln des Portals prüft und Seriennummern findet, die sich in der Datei wiederholen oder schon existieren. Fragen Sie [`getImport`](https://passport.kabin.sh/docs/de/api-reference/imports/get-import) ab, bis `status` den Wert `validated` hat (oder `failed`, wenn die Datei nicht gelesen werden konnte): ```bash title="cURL" curl -sS -X POST "$BATTERYPASS_URL/api/v1/imports/0192f3c1-7a2b-7c3d-8e4f-5a6b7c8d9e0f/validate" \ -H "Authorization: Bearer $BATTERYPASS_IMPORT_KEY" curl -sS "$BATTERYPASS_URL/api/v1/imports/0192f3c1-7a2b-7c3d-8e4f-5a6b7c8d9e0f" \ -H "Authorization: Bearer $BATTERYPASS_IMPORT_KEY" | jq '{status, summary}' ``` ```ts title="TypeScript" const res = await fetch(`${BASE}/imports/${imp.id}/validate`, { method: 'POST', headers: auth }); if (res.status !== 202) throw new Error(`validateImport: ${res.status} ${(await res.json()).code}`); let current = imp; for (let attempt = 0; attempt < 120 && !['validated', 'failed'].includes(current.status); attempt++) { await new Promise((r) => setTimeout(r, 5000)); current = await (await fetch(`${BASE}/imports/${imp.id}`, { headers: auth })).json(); } // An die Fertigung melden: gültige Zeilen und Zeilen mit Warnungen werden importiert, Fehler und Duplikate nicht. console.log(current.status, current.summary); ``` ```python title="Python" import time res = session.post(f"{BASE}/imports/{imp['id']}/validate", timeout=30) if res.status_code != 202: raise SystemExit(f"validateImport: {res.status_code} {res.json()['code']}") current = imp for _ in range(120): if current["status"] in ("validated", "failed"): break time.sleep(5) current = session.get(f"{BASE}/imports/{imp['id']}", timeout=30).json() # An die Fertigung melden: gültige Zeilen und Zeilen mit Warnungen werden importiert, Fehler und Duplikate nicht. print(current["status"], current.get("summary")) ``` ```json title="200 (gekürzt)" { "status": "validated", "summary": { "rows": 2, "valid": 2, "warnings": 0, "errors": 0, "duplicates": 0, "created": 0, "approved": 0, "not_approved": 0 } } ``` Markieren Sie die Outbox-Zeilen als erledigt, sobald der Import geprüft ist, und senden Sie die Zusammenfassung an die Fertigung. Zeilen mit Fehlern oder Duplikaten werden nicht importiert; Mitglieder sehen jede mit Zeile, Spalte und Grund im Portal. ## 5. Mitglieder übernehmen, geben frei und veröffentlichen Ab hier geschieht die Arbeit im Portal, nie mit einem Schlüssel: 1. Eine Bearbeiterin oder ein Bearbeiter öffnet den Import, liest den Prüfbericht und **übernimmt** ihn. Die Einheiten entstehen als Entwürfe, ihre Werte tragen die Quelle `import`. 2. Eine Prüferin oder ein Prüfer **gibt** die importierten Werte in einem Schritt **frei**. Werte mit einer fehlschlagenden Prüfung, ohne geforderten Nachweis oder mit einer Importwarnung bleiben zur Einzelprüfung offen; mit eingeschaltetem Vier-Augen-Prinzip auch Werte, die die prüfende Person selbst erfasst hat. 3. Eine Prüferin oder ein Prüfer **veröffentlicht** die Pässe, sobald keine Pflichtangabe mehr fehlt. Der [Änderungsfeed](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp) meldet `import.committed`, `battery.created`, `import.approved` und später `passport.published`; das ERP erfährt so von jedem Schritt, ohne die Menschen zu fragen. ## Fehlerbehandlung | Antwort | Bedeutung | Was tun | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400 validation_error` | Feld `file` (leer, weder CSV noch XLSX), `preset` (unbekannte Vorlage), `mapping` (keine Seriennummernspalte oder kein Weg zum Modell) | Datei oder Vorlage korrigieren; nicht unverändert wiederholen. | | `401 unauthenticated` | Schlüssel fehlt, ist unbekannt oder widerrufen | Geheimnis prüfen; Alarm auslösen. | | `403 forbidden` | Dem Schlüssel fehlt der Scope `import` | Den Import-Schlüssel verwenden. | | `404 not_found` | Den Import gibt es in Ihrer Organisation nicht | Einen neuen Import beginnen. | | `409 conflict` | Der Import wurde geändert (`If-Match`) oder wird gerade geprüft oder übernommen | Import lesen und entscheiden. | | `413 payload_too_large` | Die Datei überschreitet die Upload-Grenze | Die Charge aufteilen. | | `429 rate_limited` | Zu viele Anfragen | `Retry-After` Sekunden warten. | | `5xx`, Zeitüberschreitung | Ergebnis unbekannt | Mit Backoff wiederholen. Ein wiederholter Upload legt einen zweiten Import an, dessen Zeilen zu Duplikaten werden, sobald der erste übernommen ist; Mitglieder übernehmen nur einen. | Der Status `failed` bedeutet, dass die Datei bei der Prüfung nicht gelesen werden konnte: Laden Sie eine korrigierte Datei als neuen Import hoch. --- # Pass-Links zurück ins ERP > Mit einem Schlüssel mit Scope read dem Änderungsfeed folgen und GS1 Digital Link, Passstatus und QR-Code jeder Einheit an ihrem Seriennummernsatz im ERP speichern. Etiketten, Lieferscheine und Servicewerkzeuge in Ihrem ERP brauchen den Link zum Pass jeder Batterie. In diesem Ablauf folgt die Middleware mit einem Schlüssel mit Scope `read` dem [Änderungsfeed](https://passport.kabin.sh/docs/de/api-reference/changes/list-changes) von BatteryPass, liest Einheit und Pass und schreibt GS1 Digital Link und Passstatus an den Seriennummernsatz im ERP. In BatteryPass wird nichts geschrieben. ```mermaid sequenceDiagram participant MW as Middleware participant BP as BatteryPass-API participant ERP loop alle paar Minuten MW->>BP: listChanges mit gespeichertem Cursor und Passtypen (Scope read) BP-->>MW: Änderungen, älteste zuerst, next_cursor MW->>BP: getBattery (Scope read) MW->>BP: getPassport (Scope read) MW->>BP: getPassportQr, wenn ein Etikett nötig ist (Scope read) MW->>ERP: Digital Link und Status am Seriennummernsatz speichern MW->>MW: next_cursor speichern, nachdem das ERP angenommen hat end ``` **Auslöser:** Ein Pass wird veröffentlicht, neu veröffentlicht oder zurückgezogen. **Scopes des Schlüssels:** read | # | Wer | Was | Operation | | - | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | 1 | Middleware | Fragt den Änderungsfeed mit dem gespeicherten Cursor ab, etwa alle fünf Minuten, gefiltert auf Passänderungen. | [`listChanges`](https://passport.kabin.sh/docs/de/api-reference/changes/list-changes.md) (Scope: read) | | 2 | Middleware | Liest die Einheit, um Seriennummer und Modell zu erhalten, mit denen sie den Satz im ERP findet. | [`getBattery`](https://passport.kabin.sh/docs/de/api-reference/catalog/get-battery.md) (Scope: read) | | 3 | Middleware | Liest den Pass der Einheit mit Status, aktueller Version und GS1 Digital Link. | [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport.md) (Scope: read) | | 4 | Middleware | Lädt den QR-Code (PNG oder SVG), wenn ein Etikett gedruckt werden soll. | [`getPassportQr`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-qr.md) (Scope: read) | | 5 | ERP | Speichert Digital Link und Passstatus am Seriennummern- oder Equipmentsatz; der Etikettendruck liest sie dort. | | | 6 | Middleware | Speichert den Cursor erst, nachdem das ERP die Änderung angenommen hat; nach einem Absturz wiederholt sie Änderungen, statt sie zu verlieren. | | **Sicherheit** - Ein Schlüssel nur mit dem Scope read; er kann nichts verändern. - Der Feed liefert dünne Ereignisse ohne Werte; die Daten holt die Middleware danach mit demselben Schlüssel, sodass ein abgefangener Feed nichts preisgibt. - Änderungen kommen mindestens einmal an; das Schreiben ins ERP ist idempotent (gleicher Link, gleicher Status). **Datenminimierung** - Ins ERP gehen nur Digital Link, Passstatus und Versionsnummer, keine Passinhalte. ## Der Änderungsfeed `GET /api/v1/changes` liefert die Änderungen Ihrer Organisation, älteste zuerst. Jede Änderung nennt nur, was geschah und welche Entität betroffen ist; den aktuellen Stand lesen Sie an der Entität. - **types** `string[]` (query): Kommagetrennte Änderungsarten; ohne Angabe alle. Für diesen Ablauf: `passport.published`, `passport.republished`, `passport.withdrawn`. * **cursor** `string` (query): Der `next_cursor` Ihrer letzten Seite; ohne ihn beginnt der Feed am Anfang der Geschichte Ihrer Organisation. * **limit** `integer` (query, Standard: 50): Höchstens 200. ```json title="200" { "items": [ { "id": "0192f6d2-3c4e-7a5b-8c9d-0e1f2a3b4c5d", "occurred_at": "2026-10-24T09:12:40Z", "entity_type": "battery", "entity_id": "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f", "action": "passport.published" } ], "next_cursor": "eyJ4IjoxODQ0MiwiaSI6IjAxOTJmNmQyLTNjNGUtN2E1Yi04YzlkLTBlMWYyYTNiNGM1ZCJ9" } ``` | `action` | `entity_type` | Danach lesen | | ------------------------------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `battery.created` | `battery` | [`getBattery`](https://passport.kabin.sh/docs/de/api-reference/catalog/get-battery) | | `passport.published`, `passport.republished`, `passport.withdrawn` | `battery` (die Einheit) | [`getPassport`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport) | | `condition.updated`, `lifecycle_event.recorded` | `battery` | [`getCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/get-condition), [`listLifecycleEvents`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events) | | `model_version.approved` | `model_version` | [`getModelVersion`](https://passport.kabin.sh/docs/de/api-reference/catalog/get-model-version) | | `import.committed`, `import.approved` | `import` | ein Schlüssel mit Scope `import`: [`getImport`](https://passport.kabin.sh/docs/de/api-reference/imports/get-import) | Drei Regeln machen das Abfragen verlässlich: - **`next_cursor` ist gesetzt, sobald die Seite Einträge hat, auch auf der letzten Seite.** Speichern Sie ihn und senden Sie ihn beim nächsten Mal. Eine leere Seite hat `next_cursor: null`: Behalten Sie den gesendeten Cursor. - **Eine Änderung erscheint erst, wenn jede ältere Transaktion beendet ist.** Ein Cursor springt deshalb nie über eine Änderung, die spät festgeschrieben wurde. Eine Änderung kann einige Sekunden nach dem Ereignis erscheinen. - **Speichern Sie den Cursor erst, nachdem das ERP die Änderungen der Seite angenommen hat.** Nach einem Absturz sehen Sie dieselben Änderungen erneut (mindestens einmal); denselben Link und Status zweimal zu schreiben, schadet nicht. ## Abfragen und zurückschreiben ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/changes?types=passport.published,passport.republished,passport.withdrawn&limit=100&cursor=$CURSOR" \ -H "Authorization: Bearer $BATTERYPASS_READ_KEY" curl -sS "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/passport" \ -H "Authorization: Bearer $BATTERYPASS_READ_KEY" | jq '{status, passport_id, gs1_digital_link, version: .current_version.version_no}' curl -sS -o etikett.svg "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/passport/qr?format=svg" \ -H "Authorization: Bearer $BATTERYPASS_READ_KEY" ``` ```ts title="TypeScript" // passport-links.mts: alle paar Minuten ausführen import { existsSync, readFileSync, writeFileSync } from 'node:fs'; const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_READ_KEY}` }; const STATE = 'passport-links.cursor'; type Change = { id: string; entity_id: string; action: string }; // Platzhalter: Link und Status an den Seriennummernsatz im ERP schreiben (idempotent). async function updateErp(serial: string, gtin: string | undefined, link: string | undefined, status: string) { console.log(serial, gtin, link, status); } const get = async (path: string) => { const res = await fetch(`${BASE}${path}`, { headers }); if (!res.ok) throw new Error(`${path}: ${res.status} ${(await res.json()).code}`); return res.json(); }; let cursor = existsSync(STATE) ? readFileSync(STATE, 'utf8') : ''; for (;;) { const query = new URLSearchParams({ types: 'passport.published,passport.republished,passport.withdrawn', limit: '100' }); if (cursor) query.set('cursor', cursor); const page: { items: Change[]; next_cursor: string | null } = await get(`/changes?${query}`); for (const change of page.items) { const passport = await get(`/batteries/${change.entity_id}/passport`); await updateErp(passport.serial_number, passport.gtin, passport.gs1_digital_link, passport.status); } if (!page.next_cursor) break; // leere Seite: gespeicherten Cursor behalten cursor = page.next_cursor; writeFileSync(STATE, cursor); // erst, nachdem das ERP die Seite angenommen hat } ``` ```python title="Python" # passport_links.py: alle paar Minuten ausführen import os import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" STATE = "passport-links.cursor" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_READ_KEY"] def update_erp(serial, gtin, link, status): """Platzhalter: Link und Status an den Seriennummernsatz im ERP schreiben (idempotent).""" print(serial, gtin, link, status) def get(path, **params): res = session.get(BASE + path, params=params, timeout=30) res.raise_for_status() return res.json() cursor = open(STATE).read() if os.path.exists(STATE) else None while True: params = {"types": "passport.published,passport.republished,passport.withdrawn", "limit": 100} if cursor: params["cursor"] = cursor page = get("/changes", **params) for change in page["items"]: passport = get(f"/batteries/{change['entity_id']}/passport") update_erp(passport["serial_number"], passport.get("gtin"), passport.get("gs1_digital_link"), passport["status"]) if not page["next_cursor"]: break # leere Seite: gespeicherten Cursor behalten cursor = page["next_cursor"] with open(STATE, "w") as f: # erst, nachdem das ERP die Seite angenommen hat f.write(cursor) ``` Der Pass trägt, was das ERP braucht: `serial_number` und `gtin`, um den Satz zu finden, `gs1_digital_link` für das Etikett (`{resolver}/01/{gtin}/21/{serial}`), `status` (`published`, `update_needed`, `withdrawn`, …), `passport_id` und `current_version.version_no`. Ein Etikettendrucker holt den QR-Code mit [`getPassportQr`](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-qr) (`format=png` mit `size` oder `format=svg`); er enthält denselben Digital Link. [`getBattery`](https://passport.kabin.sh/docs/de/api-reference/catalog/get-battery) ergänzt Modell, Modellversion und Daten, wenn Ihr ERP-Satz sie braucht. Der erste Lauf ohne Cursor geht durch Ihre gesamte Geschichte; filtern Sie mit `types` und lassen Sie ihn einmal laufen, bevor Sie ihn einplanen. Wie der Digital Link aufgelöst wird, beschreibt [Digital Link auflösen](https://passport.kabin.sh/docs/de/guides/resolve-digital-link). ## Was ins ERP gehört Nur Digital Link, Passstatus und Versionsnummer. Passinhalte bleiben in BatteryPass: Die öffentliche Seite und das öffentliche JSON hinter dem Digital Link sind immer aktuell, und eingeschränkte Daten dürfen nicht in Systeme mit größerem Leserkreis kopiert werden. ## Fehlerbehandlung | Antwort | Bedeutung | Was tun | | ----------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `400 validation_error` auf `cursor` | Der gespeicherte Cursor ist nicht verwendbar | Löschen; der nächste Lauf beginnt am Anfang und wiederholt Änderungen, was nicht schadet, weil derselbe Link zweimal nichts ändert. | | `400 validation_error` auf `types` | Eine unbekannte Änderungsart | Liste korrigieren. | | `401 unauthenticated` | Schlüssel fehlt, ist unbekannt oder widerrufen | Geheimnis prüfen; Alarm auslösen. | | `403 forbidden` | Dem Schlüssel fehlt der Scope `read` | Den Lese-Schlüssel verwenden. | | `404 not_found` bei einer Einheit | Die Einheit gehört nicht zu Ihrer Organisation | Überspringen und die ID der Änderung protokollieren. | | `409 conflict` bei `getPassportQr` | Die Kennungen sind noch nicht festgelegt | Nach dem nächsten `passport.published` erneut fragen. | | `429 rate_limited` | Zu viele Anfragen | `Retry-After` Sekunden warten; Cursor behalten. | | `5xx`, Zeitüberschreitung | Ergebnis unbekannt | Mit Backoff wiederholen; der Cursor zeigt noch vor die Seite. | --- # Zustandsdaten aus BMS und Service > Ein Batteriemanagementsystem oder ein Servicewerkzeug meldet Messwerte je Einheit mit updateCondition; neuere Messungen werden angenommen, ältere übersprungen, und der Pass wird nach Ihrer Richtlinie neu veröffentlicht. Dynamische Batteriedaten (Gesundheitszustand, Kapazitätsverlust, Vollzyklen, Temperaturen) kommen aus dem Batteriemanagementsystem (BMS), einer Monitoring-Plattform oder einem Servicewerkzeug, manchmal über das Servicemodul des ERP. In diesem Ablauf meldet die Middleware sie je Einheit mit [`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition). Die Einzelheiten der Operation stehen in [Zustandsdaten senden](https://passport.kabin.sh/docs/de/guides/push-condition-data); diese Seite zeigt den Ablauf drumherum. ```mermaid sequenceDiagram participant BMS as BMS oder Servicewerkzeug participant MW as Middleware participant BP as BatteryPass-API BMS->>MW: neue Messwerte je Seriennummer MW->>MW: BatteryPass-ID in der eigenen Zuordnung nachschlagen alt Seriennummer noch nicht zugeordnet MW->>BP: listBatteries mit serial_prefix (Scope read) BP-->>MW: die Einheit end MW->>BP: updateCondition mit measured_at (Scope condition) BP-->>MW: accepted, skipped, publication BP->>BP: Ereignis condition_update mit dem Schlüssel, Neuveröffentlichung nach Richtlinie ``` **Auslöser:** Das BMS oder das Servicewerkzeug hat neue Messwerte einer Einheit. **Scopes des Schlüssels:** read, condition | # | Wer | Was | Operation | | - | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | 1 | Middleware | Sammelt die neuen Messwerte je Einheit mit dem Zeitpunkt der Messung. | | | 2 | Middleware | Findet die Einheit über ihre Seriennummer (Filter serial_prefix) und merkt sich die BatteryPass-ID in einer eigenen Zuordnungstabelle. | [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries.md) (Scope: read) | | 3 | Middleware | Meldet die Werte mit measured_at; ältere oder gleich alte Messungen werden übersprungen, eine Wiederholung schadet also nicht. | [`updateCondition`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition.md) (Scope: condition) | | 4 | BatteryPass | Speichert die angenommenen Werte als Messungen (ohne Prüfer freigegeben, ADR-016), legt ein Lebenszyklus-Ereignis condition_update an, das den Schlüssel nennt, und veröffentlicht sofort oder täglich neu, wie die Organisation es eingestellt hat. | | **Sicherheit** - Ein eigener Schlüssel je BMS-Plattform oder Servicepartner mit den Scopes condition und read. - Bei 429 wartet die Middleware die Sekunden aus Retry-After ab, sonst wiederholt sie mit exponentiellem Backoff und Jitter. **Datenminimierung** - Nur die dynamischen Felder der Einheit; keine Standortdaten, keine Fahrzeug- oder Halterdaten. ## Der Schlüssel Ein Schlüssel je BMS-Plattform oder Servicepartner, mit den Scopes `condition` (zum Melden) und `read` (zum Finden der Einheiten). Zwei Schlüssel mit je einem Scope gehen genauso. Ein `condition`-Schlüssel kann für jede Einheit Ihrer Organisation melden; geben Sie einem Servicepartner also nur dann einen Schlüssel, wenn Sie ihm das zutrauen. ## Die Einheit einmal finden Die Middleware führt eine Zuordnungstabelle von Seriennummer zu BatteryPass-ID. Für eine Seriennummer, die sie noch nicht kennt, fragt sie [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries) mit `serial_prefix` und wählt die Einheit, deren `serial_number` genau passt (Seriennummern sind je Modell eindeutig; geben Sie `battery_model_id` mit, wenn zwei Modelle dasselbe Nummernschema teilen): ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/batteries?serial_prefix=SV2027-000184&limit=10" \ -H "Authorization: Bearer $BATTERYPASS_BMS_KEY" | jq '.items[] | select(.serial_number == "SV2027-000184") | .id' ``` ## Messwerte melden ```bash title="cURL" curl -sS -X PATCH "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/condition" \ -H "Authorization: Bearer $BATTERYPASS_BMS_KEY" \ -H "Content-Type: application/json" \ -d '{"measured_at": "2026-10-24T06:00:00Z", "note": "BMS-Tagesbericht", "values": [{"field_key": "condition.capacity_fade", "value": 3.5}, {"field_key": "condition.number_of_full_cycles", "value": 412}]}' ``` ```ts title="TypeScript" const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_BMS_KEY}`, 'Content-Type': 'application/json' }; type Reading = { fieldKey: string; value: number; measuredAt: string }; export async function report(batteryId: string, readings: Reading[]) { const body = { note: 'BMS-Tagesbericht', values: readings.map((r) => ({ field_key: r.fieldKey, value: r.value, measured_at: r.measuredAt })), }; for (let attempt = 0; ; attempt++) { const res = await fetch(`${BASE}/batteries/${batteryId}/condition`, { method: 'PATCH', headers, body: JSON.stringify(body) }); if (res.ok) return res.json(); // { accepted, skipped, publication, event } const problem = await res.json().catch(() => ({})); const retry = res.status === 429 || res.status >= 500; if (!retry || attempt >= 4) throw new Error(`updateCondition: ${res.status} ${problem.code} (request_id ${problem.request_id})`); const wait = res.status === 429 ? Number(res.headers.get('Retry-After') ?? 1) * 1000 : 2 ** attempt * 1000; await new Promise((r) => setTimeout(r, wait + Math.random() * 500)); // Backoff mit Jitter } } ``` ```python title="Python" import os import random import time import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_BMS_KEY"] def report(battery_id, readings): """readings: [(field_key, value, measured_at)]; liefert accepted, skipped, publication und event.""" body = { "note": "BMS-Tagesbericht", "values": [{"field_key": k, "value": v, "measured_at": at} for k, v, at in readings], } for attempt in range(5): res = session.patch(f"{BASE}/batteries/{battery_id}/condition", json=body, timeout=30) if res.ok: return res.json() if res.status_code != 429 and res.status_code < 500: problem = res.json() raise SystemExit(f"updateCondition: {res.status_code} {problem['code']} (request_id {problem.get('request_id')})") wait = int(res.headers.get("Retry-After", "1")) if res.status_code == 429 else 2**attempt time.sleep(wait + random.random() / 2) # Backoff mit Jitter raise SystemExit("updateCondition: nach 5 Versuchen aufgegeben") ``` Weil eine gleich alte oder ältere Messung als die gespeicherte übersprungen wird, ändert derselbe Messwert nach einer Zeitüberschreitung nichts mehr: Deshalb ist die Wiederholung oben sicher. Senden Sie die Messungen einer Einheit in der Reihenfolge, in der sie entstanden sind. ## Was BatteryPass damit macht - Angenommene Werte werden als **freigegebene Messungen ohne Prüfer** gespeichert (Messdaten haben keinen Prüfschritt), mit ihrem `measured_at`. - Die Meldung legt ein Lebenszyklus-Ereignis `condition_update` an; dieses Ereignis nennt den Schlüssel (`api_key`) und hat die Quelle `api`. Die Werte selbst tragen den Schlüssel nicht. Das Audit-Log nennt den Schlüssel als Handelnden. - Ein veröffentlichter Pass wird so neu veröffentlicht, wie Ihre Organisation es eingestellt hat: sofort (`publication.outcome` ist `published` oder `unchanged`) oder durch den täglichen Auftrag (`scheduled`). `blocked` heißt, dass jetzt eine Pflichtangabe fehlt; der Pass steht auf `update_needed`, und ein Mitglied muss handeln. ## Was nie fließt Nur die dynamischen Felder der Einheit (Registerabschnitt `condition`). Keine Standort- oder GPS-Daten, keine Fahrzeug- oder Halterdaten, keine Namen von Bedienpersonal. Eine Notiz ist Freitext, der in der Historie der Einheit landet: Füllen Sie sie aus einer festen Vorlage. ## Fehlerbehandlung | Antwort | Bedeutung | Was tun | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `400 validation_error` | Ein Feld, das kein dynamisches Feld der Einheit ist (`unknown_field`), ein Wert außerhalb seines Bereichs, ein Zeitpunkt in der Zukunft oder vor der Herstellung, oder eine fehlschlagende feldübergreifende Prüfung | Messwert korrigieren; die ganze Meldung wurde abgelehnt. | | `401 unauthenticated` | Schlüssel fehlt, ist unbekannt oder widerrufen | Geheimnis prüfen; Alarm auslösen. | | `403 forbidden` | Dem Schlüssel fehlt der Scope `condition` | Den richtigen Schlüssel verwenden. | | `404 not_found` | Die Einheit gehört nicht zu Ihrer Organisation | Zuordnungstabelle auffrischen. | | `409 conflict` | Das Modell der Einheit ist archiviert | Für diese Einheit nicht mehr melden. | | `429 rate_limited` | Zu viele Anfragen | `Retry-After` Sekunden warten; tägliche Meldungen zeitlich verteilen. | | `5xx`, Zeitüberschreitung | Ergebnis unbekannt | Denselben Body mit Backoff wiederholen; wiederholte Messungen werden übersprungen. | --- # Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse > Abgeschlossene Service-, Reparatur- und Retourenaufträge im ERP werden Lebenszyklus-Ereignisse der Einheit, ohne Kundendaten und ohne Duplikate. Kommt eine Batterie zur Prüfung, Reparatur oder für einen Komponententausch zurück, schließt das ERP oder sein Servicemodul einen Auftrag ab. In diesem Ablauf macht die Middleware aus dem abgeschlossenen Auftrag ein Lebenszyklus-Ereignis der Einheit, mit [`createLifecycleEvent`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/create-lifecycle-event). Ereignisse sind Aufzeichnungen: Sie erscheinen in der Historie der Einheit und ändern keine Passdaten. Wie Ereignisse funktionieren, beschreibt [Lebenszyklus-Ereignisse erfassen](https://passport.kabin.sh/docs/de/guides/record-lifecycle-events). ```mermaid sequenceDiagram participant ERP participant MW as Middleware participant BP as BatteryPass-API participant M as Mitglied im Portal ERP->>ERP: Auftrag abschließen, Outbox-Zeile schreiben MW->>ERP: Outbox-Zeile lesen (Seriennummer, Art der Arbeit, Datum) MW->>MW: Auftrag schon gemeldet? Dann aufhören MW->>BP: listBatteries mit serial_prefix (Scope read) MW->>BP: createLifecycleEvent (Scope condition) BP-->>MW: 201 Ereignis MW->>MW: Auftrag und Ereignis-ID merken M->>BP: Historie prüfen, bei Bedarf ein zweites Leben anlegen ``` **Auslöser:** Ein Service-, Reparatur- oder Retourenauftrag wird im ERP abgeschlossen. **Scopes des Schlüssels:** read, condition | # | Wer | Was | Operation | | - | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | 1 | ERP | Schließt den Auftrag mit Seriennummer und Art der Arbeit ab und hält die Änderung in der Outbox fest. | | | 2 | Middleware | Findet die Einheit über ihre Seriennummer. | [`listBatteries`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries.md) (Scope: read) | | 3 | Middleware | Legt das Ereignis an (zum Beispiel repair, inspection oder component_replaced) mit Datum und kurzer Beschreibung. | [`createLifecycleEvent`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/create-lifecycle-event.md) (Scope: condition) | | 4 | Mitglied im Portal | Prüft die Historie im Portal; ein zweites Leben (Umwidmung, Wiederaufbereitung) legt ein Mitglied als neue Einheit an. | | **Sicherheit** - Ereignisse sind Aufzeichnungen und ändern keine Passdaten; der Schlüssel braucht nur condition und read. - Die Middleware merkt sich die Auftragsnummer, für die sie ein Ereignis angelegt hat, und legt es kein zweites Mal an. **Datenminimierung** - Keine Kundennamen, Adressen, Auftragsnummern oder Fehlerbeschreibungen mit Personenbezug in der Beschreibung; die Seriennummer verbindet Ereignis und Auftrag. ## Auftrag einem Ereignistyp zuordnen | ERP-Auftrag | `type` | | --------------------------------------------- | -------------------- | | Prüfung oder Diagnose ohne Arbeit | `inspection` | | Reparatur | `repair` | | Ein Modul, eine Zelle oder ein Teil getauscht | `component_replaced` | | Batterie zur Entsorgung zurückgenommen | `waste` | | Batterie recycelt | `recycled` | | Alles andere, das festgehalten werden soll | `custom` | Im Service gemessene Zustandswerte gehören nicht ins Ereignis: Melden Sie sie mit [`updateCondition`](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms), das sein eigenes Ereignis `condition_update` anlegt. Eine umgewidmete oder wiederaufbereitete Batterie bekommt eine neue Einheit mit eigenem Pass, die ein Mitglied im Portal anlegt (zweites Leben). ## Ereignis anlegen `occurred_on` ist der Tag der Arbeit, zwischen Herstelldatum und heute. `description` nimmt höchstens 2 000 Zeichen; `evidence_ids` kann bis zu 20 Dokumente Ihrer Nachweisbibliothek nennen, etwa ein Reparaturprotokoll. ```bash title="cURL" curl -sS -X POST "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/lifecycle-events" \ -H "Authorization: Bearer $BATTERYPASS_SERVICE_KEY" \ -H "Content-Type: application/json" \ -d '{"type": "component_replaced", "occurred_on": "2026-10-21", "description": "Modul 3 getauscht (Servicecode R-12)"}' ``` ```ts title="TypeScript" const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_SERVICE_KEY}`, 'Content-Type': 'application/json' }; // reported: Ihre eigene Tabelle schon gesendeter Aufträge (Auftragsnummer → Ereignis-ID). export async function recordOrder(order: { number: string; batteryId: string; type: string; doneOn: string; code: string }, reported: Map) { if (reported.has(order.number)) return reported.get(order.number); // nie einen Auftrag zweimal senden const res = await fetch(`${BASE}/batteries/${order.batteryId}/lifecycle-events`, { method: 'POST', headers, body: JSON.stringify({ type: order.type, occurred_on: order.doneOn, description: `Servicecode ${order.code}` }), }); const body = await res.json(); if (res.status !== 201) throw new Error(`createLifecycleEvent: ${res.status} ${body.code} (request_id ${body.request_id})`); reported.set(order.number, body.id); return body.id; } ``` ```python title="Python" import os import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_SERVICE_KEY"] def record_order(order, reported): """reported: Ihre eigene Tabelle schon gesendeter Aufträge (Auftragsnummer -> Ereignis-ID).""" if order["number"] in reported: return reported[order["number"]] # nie einen Auftrag zweimal senden res = session.post( f"{BASE}/batteries/{order['battery_id']}/lifecycle-events", json={"type": order["type"], "occurred_on": order["done_on"], "description": f"Servicecode {order['code']}"}, timeout=30, ) body = res.json() if res.status_code != 201: raise SystemExit(f"createLifecycleEvent: {res.status_code} {body['code']} (request_id {body.get('request_id')})") reported[order["number"]] = body["id"] return body["id"] ``` Das Ereignis hat die Quelle `api` und nennt den Schlüssel in `api_key`; das Audit-Log nennt den Schlüssel als Handelnden. ## Keine Duplikate `createLifecycleEvent` ist nicht idempotent: Jeder erfolgreiche Aufruf legt ein neues Ereignis an. Führen Sie eine Tabelle der gemeldeten Aufträge (Auftragsnummer, Ereignis-ID in BatteryPass). Sehen Sie nach einer Zeitüberschreitung in [`listLifecycleEvents`](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events) der Einheit nach (dafür braucht es `read`), bevor Sie erneut senden: Ein Ereignis desselben Typs und Tages mit Ihrem Servicecode heißt, dass der erste Aufruf durchging. ## Was nie fließt Keine Namen, Adressen oder Kontaktdaten von Kundinnen und Kunden, keine Auftrags- oder Rechnungsnummern, keine Namen von Technikern und keine Fehlerbeschreibungen, die Personen erkennen lassen. Die Seriennummer verbindet das Ereignis mit dem Auftrag in Ihrem ERP. Bauen Sie die `description` wie oben aus festen Codes, nicht aus dem Freitext des Auftrags. ## Fehlerbehandlung | Antwort | Bedeutung | Was tun | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | `400 validation_error` | Unbekannter `type` (auch `condition_update`, das nur `updateCondition` anlegt), `occurred_on` vor der Herstellung oder in der Zukunft, ein Dokument, das nicht Ihnen gehört | Zuordnung korrigieren; nicht unverändert wiederholen. | | `401 unauthenticated` | Schlüssel fehlt, ist unbekannt oder widerrufen | Geheimnis prüfen; Alarm auslösen. | | `403 forbidden` | Dem Schlüssel fehlt der Scope `condition` | Den Service-Schlüssel verwenden. | | `404 not_found` | Die Einheit gehört nicht zu Ihrer Organisation | Seriennummer prüfen. | | `409 conflict` | Das Modell der Einheit ist archiviert | Ereignis im Portal erfassen oder gar nicht. | | `429 rate_limited` | Zu viele Anfragen | `Retry-After` Sekunden warten. | | `5xx`, Zeitüberschreitung | Ergebnis unbekannt | Erst `listLifecycleEvents` prüfen, dann wiederholen. | --- # Produktänderung führt zu neuer Modellversion > Eine Änderung am Produktstamm im ERP benachrichtigt die verantwortliche Person, die in BatteryPass eine neue Modellversion anlegt; deklarierte Daten werden nie automatisch geschrieben. Ändert die Entwicklung eine Batterie (ein anderer Zelllieferant, eine neue Kapazität, eine andere Masse), ändert sich der Produktstamm im ERP, und der Batteriepass braucht eine neue **Modellversion** mit eigenem Gültigkeitsdatum. Diese Änderung betrifft deklarierte Daten mit rechtlichem Gewicht. In diesem Ablauf **wird in BatteryPass nichts automatisch geschrieben**: Die Middleware bemerkt die Änderung und sagt einer Person Bescheid, die die neue Version im Portal anlegt und ihre Werte freigeben lässt. Schlüssel können keine deklarierten Daten schreiben; das Muster ist also zugleich das, was BatteryPass durchsetzt. ```mermaid sequenceDiagram participant ERP participant MW as Middleware participant BP as BatteryPass-API participant P as Verantwortliche Person participant M as Mitglied im Portal ERP->>MW: dünnes Ereignis: Produktnummer, Art der Änderung MW->>ERP: geändertes Produkt lesen, Ereignis prüfen MW->>MW: Passdaten betroffen? sonst verwerfen MW->>BP: listBatteryModels (Scope read) BP-->>MW: das Modell nach GTIN oder Modellkennung MW->>P: Ticket oder Chatnachricht mit Modell und Änderung P->>M: entscheidet über eine neue Modellversion M->>BP: Version anlegen, Werte erfassen, freigeben lassen M->>BP: Auswirkung auf bestehende Einheiten prüfen ``` **Auslöser:** Das ERP meldet eine Änderung am Produkt- oder Materialstamm, etwa ein Business Event oder einen Webhook. **Scopes des Schlüssels:** read | # | Wer | Was | Operation | | - | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | 1 | ERP | Sendet ein dünnes Ereignis mit Produktnummer und Art der Änderung. | | | 2 | Middleware | Liest das geänderte Produkt aus dem ERP und entscheidet, ob Passdaten betroffen sind (etwa Zellchemie, Kapazität, Masse); Preis- oder Textänderungen verwirft sie. | | | 3 | Middleware | Findet das Batteriemodell über GTIN oder Modellkennung. | [`listBatteryModels`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-battery-models.md) (Scope: read) | | 4 | Middleware | Benachrichtigt die verantwortliche Person mit Modell und Änderung, etwa per Ticket oder Chat. | | | 5 | Mitglied im Portal | Legt im Portal eine neue Modellversion mit Gültigkeitsdatum an, erfasst die geänderten Werte und lässt sie freigeben; BatteryPass zeigt vorher, welche Einheiten betroffen sind. | | **Sicherheit** - Die Middleware braucht nur den Scope read; Schlüssel können deklarierte Daten nicht schreiben. - Webhooks des ERP prüft die Middleware nach dem Verfahren des Herstellers (Signatur, Validierungs-Handshake oder geheime URL). **Datenminimierung** - Die Benachrichtigung nennt Modell und geänderte Merkmale, keine Lieferanten- oder Preisdaten. ## Der Auslöser ERP-Systeme melden Produktänderungen auf verschiedene Weise; die [ERP-Seiten](https://passport.kabin.sh/docs/de/integrations/erp/sap-s4hana-cloud) nennen, was jedes System anbietet. Beispiele: Business Events von SAP S/4HANA über SAP Event Mesh, Webhook-Abonnements von Business Central, Business Events von Dynamics 365 Finance und Supply Chain Management, Automatisierungsregeln in Odoo. Die meisten sind dünn: Sie sagen, _dass_ sich etwas geändert hat, nicht _wie_. Die Middleware 1. prüft das Ereignis so, wie der Hersteller es vorsieht (eine Signatur, ein Validierungs-Handshake, ein Client-State oder eine geheime URL), und verwirft, was sie nicht prüfen kann; 2. liest das aktuelle Produkt über die API des ERP; 3. vergleicht die Merkmale, die für den Pass zählen (etwa Zellchemie, Nennkapazität oder -energie, Masse, Zell- oder Modullieferant), mit dem zuletzt gesehenen Stand und verwirft Änderungen an Preisen, Texten oder Logistikdaten. Ohne Ereignisse vergleichen Sie einen nächtlichen Produktexport mit dem vorigen: Das Ergebnis ist dasselbe. ## Das Batteriemodell finden [`listBatteryModels`](https://passport.kabin.sh/docs/de/api-reference/catalog/list-battery-models) liefert Ihre Modelle mit `gtin` und `sku` (der Modellkennung). Einen Filter nach GTIN gibt es nicht: Blättern Sie einmal durch die Modelle und halten Sie in der Middleware eine Zuordnung. ```bash title="cURL" curl -sS "$BATTERYPASS_URL/api/v1/battery-models?limit=200" \ -H "Authorization: Bearer $BATTERYPASS_READ_KEY" | jq '.items[] | select(.gtin == "04012345000023") | {id, name, sku}' ``` ```ts title="TypeScript" const BASE = `${process.env.BATTERYPASS_URL}/api/v1`; const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_READ_KEY}` }; type Model = { id: string; name: string; gtin?: string; sku?: string }; export async function modelsByGtin(): Promise> { const out = new Map(); let cursor: string | null = null; do { const query = new URLSearchParams({ limit: '200', ...(cursor ? { cursor } : {}) }); const res = await fetch(`${BASE}/battery-models?${query}`, { headers }); if (!res.ok) throw new Error(`listBatteryModels: ${res.status} ${(await res.json()).code}`); const page: { items: Model[]; next_cursor: string | null } = await res.json(); for (const m of page.items) if (m.gtin) out.set(m.gtin, m); cursor = page.next_cursor; } while (cursor); return out; } // Platzhalter: ein Ticket für die verantwortliche Person anlegen. Hier nie in BatteryPass schreiben. export function notify(model: Model, changed: string[]) { console.log(`Neue Modellversion nötig für ${model.name} (${model.id}): im ERP geändert: ${changed.join(', ')}`); } ``` ```python title="Python" import os import requests BASE = os.environ["BATTERYPASS_URL"] + "/api/v1" session = requests.Session() session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_READ_KEY"] def models_by_gtin(): out, cursor = {}, None while True: params = {"limit": 200, **({"cursor": cursor} if cursor else {})} res = session.get(f"{BASE}/battery-models", params=params, timeout=30) res.raise_for_status() page = res.json() out.update({m["gtin"]: m for m in page["items"] if m.get("gtin")}) cursor = page["next_cursor"] if not cursor: return out def notify(model, changed): """Platzhalter: ein Ticket für die verantwortliche Person anlegen. Hier nie in BatteryPass schreiben.""" print(f"Neue Modellversion nötig für {model['name']} ({model['id']}): im ERP geändert: {', '.join(changed)}") ``` ## Die Person entscheidet Die Benachrichtigung nennt das Modell, was sich im ERP geändert hat und ab welchem Tag es gilt. Im Portal 1. legt ein Mitglied eine neue **Modellversion** mit Gültigkeitsdatum an, meist mit den Werten der aktuellen Version als Ausgangspunkt; 2. erfasst die geänderten Werte mit ihren Nachweisen und lässt sie freigeben (das Vier-Augen-Prinzip gilt, wo Ihre Organisation es eingeschaltet hat); 3. prüft die **Auswirkung der Änderung**: welche bestehenden Einheiten nach ihrem Herstelldatum zur neuen Version wechseln würden und welche veröffentlichten Pässe dann eine neue Version brauchen; 4. gibt die Modellversion frei. Der [Änderungsfeed](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp) meldet `model_version.approved` und später `passport.republished` für Einheiten, deren Pässe erneut veröffentlicht wurden; das ERP kann so folgen, ohne nachzufragen. ## Was nie fließt Die Benachrichtigung trägt das Modell und die geänderten Merkmale. Lieferantennamen und -konditionen, Preise und die Personen, die die Änderung vorgenommen haben, bleiben im ERP und im Ticketsystem. ## Fehlerbehandlung | Antwort | Bedeutung | Was tun | | ----------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------- | | `400 validation_error` auf `cursor` | Ein veralteter Cursor | Die Liste neu beginnen. | | `401 unauthenticated` | Schlüssel fehlt, ist unbekannt oder widerrufen | Geheimnis prüfen; Alarm auslösen. | | `403 forbidden` | Dem Schlüssel fehlt der Scope `read` | Den Lese-Schlüssel verwenden. | | Kein Modell mit der GTIN | Das Produkt ist noch kein Batteriemodell in BatteryPass | Trotzdem benachrichtigen: Das Modell muss vielleicht angelegt werden. | | `429 rate_limited` | Zu viele Anfragen | `Retry-After` Sekunden warten. | | `5xx`, Zeitüberschreitung | Ergebnis unbekannt | Mit Backoff wiederholen; Lesen lässt sich gefahrlos wiederholen. | Ein ERP-Ereignis, das zweimal ankommt, ergibt höchstens zwei Benachrichtigungen; fassen Sie sie im Ticketsystem nach Produktnummer und Änderungsdatum zusammen. --- # abas ERP > Was über die Anbindung von abas ERP an BatteryPass bekannt ist und was noch zu klären ist. > [!WARNING] > Die Angaben auf dieser Seite stammen nur aus Werbematerial des Herstellers und sind nicht gegen eine > Entwicklerdokumentation geprüft; eine öffentliche gab es nicht. Klären Sie Schnittstelle und Anmeldung vor einem Projekt > mit dem Hersteller oder Ihrem Partner. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für abas ERP und hat keine > Partnerschaft mit dem Hersteller; nichts hier ist eine Freigabe des Herstellers. Der Hersteller nennt eine REST-API und abas Connect. Was Ihre Installation auch anbietet: eine Middleware liest die Seriennummern fertiggemeldeter Fertigungsaufträge mit einem technischen Benutzer, der nur lesen darf, schreibt eine Datei mit dem Preset `generic-csv` und stellt sie bereit wie unter [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) beschrieben. Änderungsereignisse sind nicht geprüft; fragen Sie bis dahin zeitgesteuert ab. ## Schnittstellen und Prüfstand - **Hersteller:** abas Software (Forterro) - **On-Premise:** abas ERP - **Cloud:** abas ERP (hosted) Nur aus Herstellerwerbung: genannt werden eine REST-API und abas Connect; eine öffentliche Entwicklerdokumentation gab es nicht. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | ------------------------------------------------------------------------- | --------- | --------- | ---------- | ------------------------------------------- | ------------------------------------------- | | [`REST API`](https://abas-erp.com/en/product/connectivity-iot) (REST-API) | REST | Token | keine | Produkt-/Materialstamm, Charge/Seriennummer | nur Community- oder Werbequelle, 24.10.2026 | **Hinweise** - `REST API`: Anmeldeverfahren nicht geprüft. **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition --- # Microsoft Dynamics 365 Business Central > Wie Business Central über seine API und eine eigene API-Seite Seriennummern an BatteryPass übergibt. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für Business Central und hat keine > Partnerschaft mit Microsoft; nichts hier ist eine Freigabe des Herstellers. Die Standard-API v2.0 liefert Artikel und Artikelposten, aber `itemLedgerEntries` hat keine Felder für Serien- oder Chargennummern. Seriennummern brauchen deshalb eine eigene AL-API-Seite, die Ihr Partner oder Ihre IT schreibt und die Artikelposten mit ihrer Seriennummer liefert. Das Preset `business-central-serials` passt zu ihren Spalten (und zu den Spaltennamen der Excel-Ausgabe der Benutzeroberfläche, deutsch und englisch); es setzt voraus, dass die Modellkennung in BatteryPass die Artikelnummer ist. Die Middleware meldet sich über Microsoft Entra ID als Dienst an (Client Credentials). Um neue Posten zu bemerken, kann sie Webhooks mit `POST /api/v2.0/subscriptions` abonnieren: Business Central prüft die Adresse mit einem Validierungs-Handshake, sendet schlanke Benachrichtigungen (Ressource und Art der Änderung) mit Ihrem `clientState` und lässt ein Abonnement nach drei Tagen auslaufen; erneuern Sie es rechtzeitig. Die Benachrichtigungen sind nicht signiert: prüfen Sie den `clientState` und lesen Sie die geänderte Ressource über die API, bevor Sie handeln. Danach folgen die Schritte von [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) und [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp). ## Schnittstellen und Prüfstand - **Hersteller:** Microsoft - **Cloud:** Business Central online (API v2.0) - **On-Premise:** Business Central on-premises (API v2.0) Die Standard-API liefert Artikelposten ohne Serien- und Chargennummern; dafür braucht es eine eigene AL-API-Seite. Webhook-Abonnements laufen nach drei Tagen ab und müssen erneuert werden. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ---------------------------- | ------------------------- | ---------------------------------------------- | -------------------------------------------------- | | [`API v2.0`](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/resources/dynamics_itemledgerentry) (Standard-API (Artikel, Artikelposten, Verkaufsbelege)) | OData V4 | OAuth 2.0 Client Credentials | keine | Produkt-/Materialstamm, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`Webhook subscriptions (/api/v2.0/subscriptions)`](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/dynamics-subscriptions) (Benachrichtigungen über Änderungen) | REST | OAuth 2.0 Client Credentials | Webhooks (nicht signiert) | Produkt-/Materialstamm, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | | `Custom AL API page` (Eigene API-Seite für Artikelposten mit Seriennummern) | OData V4 | OAuth 2.0 Client Credentials | keine | Charge/Seriennummer | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `API v2.0`: Anmeldung über Microsoft Entra ID (Service-to-Service); itemLedgerEntries hat keine Felder für Serien- oder Chargennummern. - `Webhook subscriptions (/api/v2.0/subscriptions)`: Beim Anlegen prüft Business Central die Adresse mit einem Validierungs-Handshake. Benachrichtigungen sind schlank (Ressource und Art der Änderung) und tragen den clientState des Abonnements, aber keine Signatur. - `Custom AL API page`: Eine Erweiterung, die der Partner oder die eigene IT schreibt; sie liefert die Spalten des Presets business-central-serials. **Importvorlagen (Presets): Business Central – Artikelposten mit Seriennummern** (`business-central-serials`): Die Artikelposten aus Microsoft Dynamics 365 Business Central mit Seriennummer, wie sie eine eigene AL-API-Seite liefert (die Standard-API itemLedgerEntries enthält keine Seriennummern). Spaltennamen der API-Seite und der Excel-Ausgabe der Benutzeroberfläche (deutsch und englisch). Setzt voraus, dass die Modellkennung die Artikelnummer ist. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------ | ----------------------------------------------------- | | `itemNo` (auch Item No., Artikelnr.) | Modellkennung / SKU `identification.model_identifier` | | `serialNo` (auch Serial No., Seriennr.) | Seriennummer `identification.serial_number` | | `postingDate` (auch Posting Date, Buchungsdatum) | Herstellungsdatum `identification.manufacturing_date` | **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # Microsoft Dynamics 365 Finance und Supply Chain Management > Wie Dynamics 365 Finance und Supply Chain Management über Datenentitäten und Business Events mit BatteryPass zusammenarbeitet. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für Dynamics 365 und hat keine > Partnerschaft mit Microsoft; nichts hier ist eine Freigabe des Herstellers. Die Datenentitäten unter `/data` (OData V4) stellen Produkte, Seriennummern, Fertigung, Lieferungen und Retouren bereit. Die Middleware meldet sich über Microsoft Entra ID mit einem Client-Secret oder besser einem Zertifikat an, als Anwendungsbenutzer mit Leserechten auf genau die Entitäten, die sie braucht. Business Events melden Änderungen, etwa einen fertig gemeldeten Fertigungsauftrag, an Azure Service Bus, Event Grid, Event Hubs, einen HTTPS-Endpunkt oder Power Automate. Laut Microsoft sind sie kein Weg für den Datenexport und kommen nicht zwingend in der richtigen Reihenfolge an. Behandeln Sie jedes Ereignis als Hinweis: lesen Sie die Daten danach über OData und verarbeiten Sie sie idempotent. Typisch passen [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) mit dem Preset `generic-csv`, [Rücksendungen und Reparaturen](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs) aus Serviceaufträgen und [Zustandsdaten](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms), wo ein Serviceprozess Messwerte erfasst. ## Schnittstellen und Prüfstand - **Hersteller:** Microsoft - **Cloud:** Finance and Supply Chain Management (formerly Finance and Operations) Business Events melden Änderungen, sind laut Microsoft aber kein Weg für den Datenexport und kommen nicht zwingend in der richtigen Reihenfolge an: nach dem Ereignis liest die Middleware die Daten über OData. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------- | -------------------------------------------------- | | [`OData data entities (/data)`](https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/odata) (Datenentitäten (Produkte, Serien, Fertigung, Lieferungen, Retouren)) | OData V4 | OAuth 2.0 Client Credentials | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang, Retoure | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`Business events`](https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/business-events/home-page) (Geschäftsereignisse) | REST | OAuth 2.0 Client Credentials | Geschäftsereignisse (nicht signiert) | Produkt-/Materialstamm, Fertigungsauftrag, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `OData data entities (/data)`: Anmeldung über Microsoft Entra ID mit Client-Secret oder Zertifikat. - `Business events`: Endpunkte sind unter anderem Azure Service Bus, Event Grid, Event Hubs, HTTPS und Power Automate; keine Reihenfolgegarantie. **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Zustandsdaten aus BMS und Service](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms.md): Ein Batteriemanagementsystem oder ein Servicewerkzeug meldet Messwerte wie Kapazitätsverlust oder Zyklen; BatteryPass übernimmt neuere Messungen und veröffentlicht nach der Richtlinie der Organisation. Scopes des Schlüssels: read, condition - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # Microsoft Dynamics NAV 2018 > Wie Dynamics NAV 2018 bis zum Umstieg Seriennummern liefern kann und warum neue Integrationen auf Business Central zielen. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für Dynamics NAV und hat keine > Partnerschaft mit Microsoft; nichts hier ist eine Freigabe des Herstellers. > [!WARNING] > Laut der Lifecycle-Seite von Microsoft endet der erweiterte Support für Dynamics NAV 2018 am 11. Januar 2028. Bauen Sie > neue Integrationen gegen die API von Business Central (siehe > [Business Central](https://passport.kabin.sh/docs/de/integrations/erp/dynamics-365-business-central)) und halten Sie, was Sie für NAV bauen, > klein. NAV 2018 veröffentlicht Seiten, Codeunits und Abfragen als SOAP- und OData-Webdienste. Die Middleware meldet sich mit NavUserPassword oder dem Webdienst-Zugriffsschlüssel des Benutzers an; nutzen Sie einen technischen Benutzer mit reinen Leserechten. NAV kennt keine Änderungsereignisse, die Middleware liest neue Seriennummern deshalb zeitgesteuert. Bis zum Umstieg genügt oft ein Dateiexport: die Middleware (oder ein geplanter Bericht) schreibt die Seriennummern mit den Spaltennamen des Presets `generic-csv`, und die Datei wird bereitgestellt wie unter [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) beschrieben. ## Schnittstellen und Prüfstand - **Hersteller:** Microsoft - **On-Premise:** Dynamics NAV 2018 Der erweiterte Support endet laut Microsoft am 11. Januar 2028. Neue Integrationen sollten auf die API von Business Central zielen; bis zum Umstieg genügt ein Dateiexport über die allgemeine Vorlage. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------------------- | ---------- | ------------------------------------------------------------------- | -------------------------------------------------- | | [`SOAP web services`](https://learn.microsoft.com/en-us/previous-versions/dynamicsnav-2018-developer/Web-Services) (Veröffentlichte Seiten und Codeunits) | SOAP | Basic Auth, API-Schlüssel | keine | Produkt-/Materialstamm, Charge/Seriennummer, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`OData web services`](https://learn.microsoft.com/en-us/lifecycle/products/dynamics-nav-2018) (Veröffentlichte Seiten und Abfragen) | OData V4 | Basic Auth, API-Schlüssel | keine | Produkt-/Materialstamm, Charge/Seriennummer, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `SOAP web services`: Anmeldung mit NavUserPassword oder dem Webdienst-Zugriffsschlüssel des Benutzers. **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition --- # Epicor Kinetic > Wie Epicor Kinetic über die REST-API v2 Seriennummern an BatteryPass liefern könnte. > [!WARNING] > Die Angaben auf dieser Seite stammen nur aus Community-Quellen und sind nicht gegen die Dokumentation von Epicor > geprüft. Klären Sie Schnittstellen und Anmeldung vor einem Projekt mit Epicor oder Ihrem Partner. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für Epicor Kinetic und hat keine > Partnerschaft mit Epicor; nichts hier ist eine Freigabe des Herstellers. Laut Community-Quellen stellt die REST-API v2 von Kinetic die Business Objects über REST bereit und verlangt zusätzlich zur Benutzeranmeldung einen API-Schlüssel im Header `x-api-key`. Senden Sie diesen Schlüssel nie als Query-Parameter: URLs landen in Logs. Nutzen Sie einen technischen Benutzer mit reinen Leserechten und legen Sie beide Geheimnisse in einen Tresor. Eine Middleware liest die Seriennummern fertiggemeldeter Aufträge und schreibt eine Datei mit dem Preset `generic-csv` für [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production). Änderungsereignisse sind nicht geprüft; fragen Sie bis dahin zeitgesteuert ab. ## Schnittstellen und Prüfstand - **Hersteller:** Epicor - **Cloud:** Epicor Kinetic (REST API v2) - **On-Premise:** Epicor Kinetic (REST API v2) Nur aus Community-Quellen: die REST-API v2 verlangt zusätzlich zur Benutzeranmeldung einen API-Schlüssel im Header x-api-key. Den Schlüssel nie als Query-Parameter senden. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | --------------------------------------------------------------------------------------------------------------------- | --------- | ------------------------- | ---------- | -------------------------------------------------------------------------------------- | ------------------------------------------- | | [`REST API v2`](https://www.epiusers.help/t/rest-interface-header-key-value-pairs/44309) (Business Objects über REST) | REST | API-Schlüssel, Basic Auth | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang | nur Community- oder Werbequelle, 24.10.2026 | **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # IFS Cloud > Wie IFS Cloud über seine OData-Projektionen Seriennummern an BatteryPass liefern könnte. > [!WARNING] > Die Angaben auf dieser Seite stammen nur aus Community-Quellen und sind nicht gegen die Dokumentation von IFS geprüft. > Klären Sie Schnittstellen, Anmeldung und Entitäten vor einem Projekt mit IFS oder Ihrem Partner. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für IFS Cloud und hat keine > Partnerschaft mit IFS; nichts hier ist eine Freigabe des Herstellers. Laut Community-Quellen sind die APIs von IFS Cloud OData-Projektionen der Anwendungen, und die Anmeldung läuft über OAuth 2.0. Eine Middleware würde die Seriennummern fertiggemeldeter Werkstattaufträge über eine solche Projektion lesen, mit einem Dienstbenutzer, der nur lesen darf, und eine Datei mit dem Preset `generic-csv` für [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) schreiben. Änderungsereignisse sind nicht geprüft; bis dahin fragt die Middleware zeitgesteuert ab. IFS wird oft auch für Serviceprozesse genutzt, deshalb können [Rücksendungen und Reparaturen](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs) und [Zustandsdaten](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms) ebenfalls passen. ## Schnittstellen und Prüfstand - **Hersteller:** IFS - **Cloud:** IFS Cloud - **On-Premise:** IFS Cloud (remote deployment) Nur aus Community-Quellen: die APIs sind OData-Projektionen, die Anmeldung läuft über OAuth 2.0. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | ------------------------------------------------------------------------------------------------------- | --------- | ---------------------------- | ---------- | -------------------------------------------------------------- | ------------------------------------------- | | [`Projections (OData)`](https://community.ifs.com/questions/37795) (OData-Projektionen der Anwendungen) | OData V4 | OAuth 2.0 Client Credentials | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag | nur Community- oder Werbequelle, 24.10.2026 | **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Zustandsdaten aus BMS und Service](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms.md): Ein Batteriemanagementsystem oder ein Servicewerkzeug meldet Messwerte wie Kapazitätsverlust oder Zyklen; BatteryPass übernimmt neuere Messungen und veröffentlicht nach der Richtlinie der Organisation. Scopes des Schlüssels: read, condition - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # Infor CloudSuite (M3, LN) > Wie Infor M3 und Infor LN über das ION API Gateway BatteryPass erreichen. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für Infor und hat keine > Partnerschaft mit Infor; nichts hier ist eine Freigabe des Herstellers. > [!WARNING] > Die LN-OData-Entitäten und die BOD-Nachrichten unten sind nicht gegen die Dokumentation von Infor geprüft (in der > Tabelle als „nicht geprüft“ markiert). Klären Sie Protokollversionen und Entitäten mit Infor oder Ihrem Partner, bevor > Sie darauf aufbauen. Jeder Aufruf läuft über das ION API Gateway mit OAuth 2.0 Client Credentials. Die Zugangsdaten stehen in einer `.ionapi`-Datei; behandeln Sie sie wie den Schlüssel für BatteryPass als Geheimnis: im Tresor, nie in einem Repository, und eine eigene Datei je Integration. In M3 ruft die Middleware MI-Programme über das Gateway auf, in LN liest sie Business Data Entities. Für [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) liest die Middleware die Seriennummern fertiggemeldeter Aufträge und schreibt eine Datei mit dem Preset `generic-csv`. Infor ION kann bei Änderungen außerdem Business Object Documents (BODs) veröffentlichen; nutzen Sie sie als Auslöser und lesen Sie die Einzelheiten über das Gateway. ## Schnittstellen und Prüfstand - **Hersteller:** Infor - **Cloud:** Infor M3 and Infor LN on Infor OS - **On-Premise:** Infor M3 and Infor LN with Infor OS on-premises Alle Aufrufe laufen über das ION API Gateway; die Zugangsdaten stehen in einer .ionapi-Datei, die wie ein Geheimnis zu behandeln ist. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ---------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------- | -------------------------------------------------- | | [`ION API Gateway`](https://docs.infor.com/inforos/2026.x/en-us/useradminlib_cloud/apigatewayag_cloud/wxo1761727799964.html) (Zugang zu den APIs von M3 und LN) | REST | OAuth 2.0 Client Credentials | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang, Retoure | geprüft in der Herstellerdokumentation, 24.10.2026 | | `M3 MI programs` (M3-Programme über das ION API Gateway) | REST | OAuth 2.0 Client Credentials | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | | `LN OData (BDE)` (LN-Business-Data-Entities) | OData V4 | OAuth 2.0 Client Credentials | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag | nicht geprüft | | `Business Object Documents (BODs)` (Nachrichten über Infor ION) | BOD (Infor ION) | OAuth 2.0 Client Credentials | Geschäftsereignisse (nicht signiert) | Produkt-/Materialstamm, Fertigungsauftrag, Lieferung/Warenausgang | nicht geprüft | **Hinweise** - `LN OData (BDE)`: Protokollversion und Entitäten nicht geprüft. **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Zustandsdaten aus BMS und Service](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms.md): Ein Batteriemanagementsystem oder ein Servicewerkzeug meldet Messwerte wie Kapazitätsverlust oder Zyklen; BatteryPass übernimmt neuere Messungen und veröffentlicht nach der Richtlinie der Organisation. Scopes des Schlüssels: read, condition - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # Odoo > Wie Odoo 19 über die JSON-2-API Los- und Seriennummern an BatteryPass übergibt. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für Odoo und hat keine Partnerschaft > mit Odoo S.A.; nichts hier ist eine Freigabe des Herstellers. Odoo 19 bietet die JSON-2-API: `POST /json/2//` mit einem API-Schlüssel als Bearer-Token. Seriennummern sind Datensätze von `stock.lot`, Fertigungsaufträge Datensätze von `mrp.production`. Das Preset `odoo-stock-lot` ordnet Los- oder Seriennummer und die interne Referenz des Produkts zu; Odoo führt am Los kein Herstelldatum, die Middleware ergänzt deshalb die Spalte „Manufacturing Date“, etwa aus dem Enddatum des Fertigungsauftrags. API-Schlüssel haben ein Ablaufdatum: nehmen Sie ihre Rotation in den Betriebsplan auf, wie beim Schlüssel für BatteryPass. XML-RPC und JSON-RPC sind abgekündigt und fallen mit Odoo 22 (Herbst 2028) beziehungsweise Odoo Online 21.1 weg; bauen Sie keine neuen Integrationen darauf. Automatisierungsregeln können einen Datensatz als ausgehenden Webhook an eine Adresse senden. Die Nutzlast ist nicht signiert; behandeln Sie sie deshalb als Hinweis: die Middleware liest den Datensatz über die JSON-2-API, bevor sie handelt. Danach geht es weiter wie unter [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production). ## Schnittstellen und Prüfstand - **Hersteller:** Odoo S.A. - **Cloud:** Odoo Online and Odoo.sh 19 - **On-Premise:** Odoo 19 Community and Enterprise Odoo 19 bietet die JSON-2-API mit API-Schlüsseln als Bearer-Token; XML-RPC und JSON-RPC sind abgekündigt und fallen mit Odoo 22 (Herbst 2028) beziehungsweise Odoo Online 21.1 weg. API-Schlüssel haben ein Ablaufdatum: die Rotation gehört in den Betriebsplan. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---------------------- | ------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------- | | [`JSON-2 API (/json/2//)`](https://www.odoo.com/documentation/19.0/developer/reference/external_api.html) (Alle Modelle, etwa product.product, stock.lot, mrp.production) | REST | API-Schlüssel | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang, Retoure | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`Webhooks (automation rules)`](https://www.odoo.com/documentation/19.0/applications/studio/automated_actions/webhooks.html) (Ausgehende Webhooks aus Automatisierungsregeln) | REST | API-Schlüssel | Webhooks (nicht signiert) | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`XML-RPC and JSON-RPC (deprecated)`](https://www.odoo.com/documentation/19.0/developer/reference/external_api.html) (Ältere externe API) | XML-RPC | API-Schlüssel, Sitzung | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang, Retoure | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `Webhooks (automation rules)`: Die Nutzlast ist nicht signiert; bei eingehenden Webhooks ist die geheime URL der Schutz. - `XML-RPC and JSON-RPC (deprecated)`: Für neue Integrationen nicht mehr verwenden. **Importvorlagen (Presets): Odoo – Los-/Seriennummern (stock.lot)** (`odoo-stock-lot`): Ein Export des Modells stock.lot aus Odoo mit der internen Referenz des Produkts. Odoo führt am Los kein Herstelldatum; die Middleware ergänzt die Spalte „Manufacturing Date“, zum Beispiel aus dem Enddatum des Fertigungsauftrags. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------------------------------- | ----------------------------------------------------- | | `Lot/Serial Number` (auch name, Los-/Seriennummer) | Seriennummer `identification.serial_number` | | `Product/Internal Reference` (auch product_id/default_code, Produkt/Interne Referenz) | Modellkennung / SKU `identification.model_identifier` | | `Manufacturing Date` (auch date_finished, Herstelldatum) | Herstellungsdatum `identification.manufacturing_date` | **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # Oracle Fusion Cloud Supply Chain Management > Wie Oracle Fusion Cloud SCM Seriennummern von Arbeitsaufträgen über seine REST-API und Oracle Integration an BatteryPass gibt. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für Oracle Fusion und hat keine > Partnerschaft mit Oracle; nichts hier ist eine Freigabe des Herstellers. Die Seriennummern eines Arbeitsauftrags sind in der REST-API eine Kindressource des Arbeitsauftrags: `workOrders/{WorkOrderId}/child/WorkOrderSerialNumber`. Ist ein Arbeitsauftrag abgeschlossen, liest die Middleware sie zusammen mit Artikel und Fertigmeldedatum und schreibt eine Datei mit dem Preset `generic-csv` für [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production). Ereignisse kommen über den ERP Cloud Adapter von Oracle Integration, der Geschäftsereignisse der Anwendung abonniert und einen Integrationsablauf startet. Nutzen Sie das Ereignis als Auslöser und lesen Sie die Daten danach über die REST-API. Die REST-API akzeptiert Basic Auth und OAuth 2.0; nehmen Sie OAuth 2.0 mit einem eigenen Integrationsbenutzer, der nur Arbeitsaufträge und Artikel lesen darf. ## Schnittstellen und Prüfstand - **Hersteller:** Oracle - **Cloud:** Oracle Fusion Cloud SCM (REST API 11.13.18.05) Seriennummern von Fertigungsaufträgen liegen als Kindressource am Arbeitsauftrag. Ereignisse kommen über den ERP Cloud Adapter von Oracle Integration. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------- | -------------------------------------------------- | | [`REST API (fscmRestApi)`](https://docs.oracle.com/en/cloud/saas/supply-chain-and-manufacturing/25d/fasrp/op-workorders-workorderid-child-workorderserialnumber-action-generateserialnumbers-post.html) (Artikel, Arbeitsaufträge und ihre Seriennummern, Lieferungen) | REST | Basic Auth, OAuth 2.0 Client Credentials | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`Oracle Integration ERP Cloud Adapter`](https://docs.oracle.com/en/cloud/paas/application-integration/erp-adapter/) (Geschäftsereignisse an Oracle Integration) | REST | OAuth 2.0 Client Credentials | Geschäftsereignisse (nicht signiert) | Produkt-/Materialstamm, Fertigungsauftrag, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `REST API (fscmRestApi)`: Pfad der Seriennummern eines Arbeitsauftrags workOrders/{WorkOrderId}/child/WorkOrderSerialNumber. **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # Oracle NetSuite > Wie Oracle NetSuite über SuiteTalk REST und SuiteQL Inventarnummern an BatteryPass übergibt. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für NetSuite und hat keine > Partnerschaft mit Oracle; nichts hier ist eine Freigabe des Herstellers. Serien- und Chargennummern stehen im Datensatz `inventorynumber`. Die Middleware liest sie mit SuiteQL über die REST-Webdienste oder exportiert eine gespeicherte Suche als CSV; das Preset `netsuite-inventory-numbers` ordnet Nummer und Artikel zu (es setzt voraus, dass die Modellkennung in BatteryPass der Artikelname ist). NetSuite führt an der Inventarnummer kein Herstelldatum; die Middleware ergänzt es, etwa aus dem Fertigungsauftrag. Für die Anmeldung sollten neue Integrationen OAuth 2.0 mit Client Credentials nutzen, ein JWT, das mit einem in NetSuite hinterlegten Zertifikat signiert ist. Partnerquellen nennen das Release 2027.1 als Ende für neue Integrationen mit Token-based Authentication (TBA); auf Oracles eigenen Seiten ließ sich dieses Datum bei unserer Prüfung nicht bestätigen. Planen Sie also in jedem Fall mit OAuth 2.0. Von sich aus sendet NetSuite keine Ereignisse. Ein User-Event-Skript (`afterSubmit` mit dem Modul `N/https`) kann nach dem Speichern eines Datensatzes Ihre Middleware aufrufen; Signatur, Wiederholungen und Fehlerbehandlung liegen dann bei diesem Skript. Einfacher ist eine regelmäßige SuiteQL-Abfrage nach dem Datum der letzten Änderung. ## Schnittstellen und Prüfstand - **Hersteller:** Oracle - **Cloud:** NetSuite (SuiteTalk REST web services) Für neue Integrationen OAuth 2.0 mit Client Credentials (ein mit Zertifikat signiertes JWT) statt Token-based Authentication: Partnerquellen nennen 2027.1 als Ende für neue TBA-Integrationen, auf Oracles Seiten war das nicht zu bestätigen. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------- | | [`SuiteTalk REST web services`](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_1540391670.html) (Datensätze (Artikel, Fertigungsaufträge, Lieferungen, Retouren)) | REST | OAuth 2.0 Client Credentials, Token | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang, Retoure | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`SuiteQL`](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157909186990.html) (Abfragen, etwa über den Datensatz inventorynumber) | REST | OAuth 2.0 Client Credentials, Token | keine | Charge/Seriennummer | geprüft in der Herstellerdokumentation, 24.10.2026 | | `SuiteScript afterSubmit with N/https` (Eigene Benachrichtigung nach dem Speichern eines Datensatzes) | REST | OAuth 2.0 Client Credentials | Webhooks (nicht signiert) | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `SuiteScript afterSubmit with N/https`: Ein User-Event-Skript ruft nach dem Speichern eine HTTPS-Adresse auf; Signatur und Wiederholungen muss das Skript selbst leisten. **Importvorlagen (Presets): NetSuite – Gespeicherte Suche über Inventarnummern** (`netsuite-inventory-numbers`): Eine gespeicherte Suche über den Datensatz Inventarnummer (inventorynumber) in Oracle NetSuite, als CSV exportiert oder per SuiteQL abgefragt. Setzt voraus, dass die Modellkennung der Artikelname ist. Das Herstelldatum ergänzt die Middleware, etwa aus dem Fertigungsauftrag. | Spalte im Export | Feld in BatteryPass | | -------------------------------------------------------------------- | ----------------------------------------------------- | | `Number` (auch Inventory Number, inventorynumber, Serial/Lot Number) | Seriennummer `identification.serial_number` | | `Item` (auch itemid, Item Name) | Modellkennung / SKU `identification.model_identifier` | | `Manufacturing Date` (auch Herstelldatum) | Herstellungsdatum `identification.manufacturing_date` | **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # proALPHA ERP > proALPHA mit einem Dateiexport an BatteryPass anbinden, bis eine Schnittstelle bestätigt ist. > [!WARNING] > Nicht geprüft: eine REST-Schnittstelle mit OpenAPI-Beschreibung wird nur von Dritten erwähnt. Bis der Hersteller sie > bestätigt, planen Sie mit einem Dateiexport. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für proALPHA und hat keine > Partnerschaft mit dem Hersteller; nichts hier ist eine Freigabe des Herstellers. Ein Dateiexport ist der sichere Weg: ein geplanter Bericht oder eine kleine Middleware schreibt die Seriennummern fertiggemeldeter Produktion mit den Spaltennamen des Presets `generic-csv` (GTIN, Seriennummer, Herstelldatum, Produktionsstätte, Modellversion, Batteriestatus). Die Middleware lädt die Datei mit einem Schlüssel hoch, der nur den Scope `import` hat; Mitglieder übernehmen und geben sie im Portal frei, wie unter [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) beschrieben. ## Schnittstellen und Prüfstand - **Hersteller:** proALPHA - **On-Premise:** proALPHA ERP Nicht geprüft: eine REST-Schnittstelle mit OpenAPI-Beschreibung wird nur von Dritten erwähnt. Bis zur Klärung mit dem Hersteller ist ein Dateiexport mit der allgemeinen Vorlage der sichere Weg. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | ------------------------------------------------------- | --------- | --------- | ---------- | ------------------------------------------- | ------------- | | `REST interface` (REST-Schnittstelle (nicht bestätigt)) | REST | Token | keine | Produkt-/Materialstamm, Charge/Seriennummer | nicht geprüft | **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read --- # Sage X3 > Was über die Anbindung von Sage X3 an BatteryPass bekannt ist und was noch zu klären ist. > [!WARNING] > Nicht geprüft: Sage nennt für X3 eine GraphQL-API, deren Referenz aber nur Partnern offensteht; Anmeldung und Objekte > konnten wir nicht bestätigen. Klären Sie die Schnittstelle vor einem Projekt mit Sage oder Ihrem Partner. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für Sage X3 und hat keine > Partnerschaft mit Sage; nichts hier ist eine Freigabe des Herstellers. Welche Schnittstelle Ihr X3 auch anbietet, auf der Seite von BatteryPass bleibt der Ablauf gleich: eine Middleware liest die Seriennummern fertiggemeldeter Produktion, schreibt eine Datei mit dem Preset `generic-csv` und stellt sie bereit wie unter [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) beschrieben; Mitglieder übernehmen und geben sie im Portal frei. Bis die Schnittstelle geklärt ist, ist ein geplanter Export einer Datei mit diesen Spalten ein sicherer Anfang. ## Schnittstellen und Prüfstand - **Hersteller:** Sage - **Cloud:** Sage X3 - **On-Premise:** Sage X3 Nicht geprüft: Sage nennt eine GraphQL-API; die Referenz ist nur für Partner zugänglich. Vor dem Projekt beim Hersteller klären. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | -------------------------------------------------------------------- | --------- | ---------------------------- | ---------- | ------------------------------------------- | ------------- | | [`GraphQL API`](https://developer.sage.com/x3/graphql) (GraphQL-API) | GraphQL | OAuth 2.0 Client Credentials | keine | Produkt-/Materialstamm, Charge/Seriennummer | nicht geprüft | **Hinweise** - `GraphQL API`: Anmeldeverfahren und Objekte nicht geprüft. **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition --- # SAP Business One > Wie SAP Business One über den Service Layer Seriennummern an BatteryPass liefert. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für SAP Business One und hat > keine Partnerschaft mit SAP; nichts hier ist eine Freigabe des Herstellers. Der Zugang ist der Service Layer: OData V3 unter `/b1s/v1` und OData V4 unter `/b1s/v2`. Er arbeitet mit Sitzungen: die Middleware meldet sich mit `POST /Login` an, hält das Sitzungs-Cookie und meldet sich neu an, wenn die Sitzung abläuft. Nutzen Sie einen technischen Business-One-Benutzer, der nur Artikel, Seriennummern und Fertigungsaufträge lesen darf. Webhooks sind im Service Layer nicht dokumentiert; die Middleware fragt deshalb regelmäßig ab, etwa alle paar Minuten nach Seriennummern, die seit dem letzten Lauf entstanden sind. Sie schreibt sie mit dem Preset `generic-csv` (GTIN, Seriennummer, Herstelldatum, Produktionsstätte) in eine Datei und stellt sie bereit wie unter [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) beschrieben. Der Service Layer selbst bleibt in Ihrem Netz; mit BatteryPass spricht nur die Middleware. ## Schnittstellen und Prüfstand - **Hersteller:** SAP - **On-Premise:** SAP Business One with Service Layer (SQL Server and SAP HANA) Der Service Layer arbeitet mit Sitzungen: die Middleware meldet sich mit POST /Login an und hält das Sitzungs-Cookie. Webhooks sind im Service Layer nicht dokumentiert; Änderungen findet die Middleware per Abfrage. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | --------- | ---------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------- | | [`Service Layer /b1s/v1`](https://learning.sap.com/courses/leveraging-the-sap-business-one-service-layer/logging-in-and-logging-out-using-service-layer-api) (Service Layer (OData V3)) | OData V3 | Sitzung | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang, Retoure | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`Service Layer /b1s/v2`](https://learning.sap.com/courses/leveraging-the-sap-business-one-service-layer/logging-in-and-logging-out-using-service-layer-api) (Service Layer (OData V4)) | OData V4 | Sitzung | keine | Produkt-/Materialstamm, Charge/Seriennummer, Fertigungsauftrag, Lieferung/Warenausgang, Retoure | geprüft in der Herstellerdokumentation, 24.10.2026 | **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # SAP S/4HANA Cloud Public Edition > Wie SAP S/4HANA Cloud Public Edition Seriennummern an BatteryPass übergibt und Pass-Links zurückbekommt. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für SAP S/4HANA Cloud und hat > keine Partnerschaft mit SAP; nichts hier ist eine Freigabe des Herstellers. In der Public Edition spricht jede Integration über ein eigenes Kommunikationsarrangement mit den freigegebenen APIs des SAP Business Accelerator Hub. Geben Sie der Middleware für BatteryPass genau die APIs, die sie braucht: die Seriennummern-API, Rückmeldungen zu Fertigungsaufträgen oder Warenbewegungen für [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) und die Produkt-API, wenn eine [Produktänderung](https://passport.kabin.sh/docs/de/integrations/workflows/product-change) die verantwortliche Person erreichen soll. Seriennummern liefert `CE_API_MATERIALSERIALNUMBER_0001`. Der SAP-Hinweis 3772443 beschreibt einen Fehler bei Serialisierungsstufe 1: das Anlegen antwortet mit `201`, legt aber nichts an. Das Lesen ist nicht betroffen; lassen Sie Seriennummern also in der Fertigung (oder mit IQ01) entstehen und lesen Sie sie nur aus der Middleware. Das Preset `sap-serial-numbers` ordnet Materialnummer, Seriennummer und Buchungsdatum des Wareneingangs zu; es setzt voraus, dass die Modellkennung des Batteriemodells in BatteryPass die SAP-Materialnummer ist. Änderungen erreichen die Middleware als CloudEvents über SAP Event Mesh, etwa `sap.s4.beh.product.v1.Product.Changed.v1`. Die Ereignisse sind schlank: sie tragen den Schlüssel, das Produkt liest die Middleware danach. Für den Rückweg (Digital Link und Passstatus am Seriennummernsatz) folgt die Middleware dem [Änderungsfeed](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp) von BatteryPass. ## Schnittstellen und Prüfstand - **Hersteller:** SAP - **Cloud:** Public Edition Jede Integration bekommt ein eigenes Kommunikationsarrangement mit genau den APIs, die sie braucht. Die Middleware läuft meist in der SAP Integration Suite oder einer anderen iPaaS. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ---------------------------------------------------------- | ------------------------------------ | ---------------------- | -------------------------------------------------- | | [`CE_PRODUCT_0002`](https://api.sap.com/api/CE_PRODUCT_0002/overview) (Produktstamm) | OData V4 | Basic Auth, X.509-Zertifikat, OAuth 2.0 Client Credentials | keine | Produkt-/Materialstamm | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`CE_API_MATERIALSERIALNUMBER_0001`](https://api.sap.com/api/CE_API_MATERIALSERIALNUMBER_0001/overview) (Serialnummern) | OData V4 | Basic Auth, X.509-Zertifikat, OAuth 2.0 Client Credentials | keine | Charge/Seriennummer | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`CE_PRODUCTIONORDER_0001`](https://api.sap.com/api/CE_PRODUCTIONORDER_0001/overview) (Fertigungsaufträge) | OData V4 | Basic Auth, X.509-Zertifikat, OAuth 2.0 Client Credentials | keine | Fertigungsauftrag | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`API_PROD_ORDER_CONFIRMATION_2_SRV`](https://api.sap.com/api/API_PROD_ORDER_CONFIRMATION_2_SRV/overview) (Rückmeldungen zu Fertigungsaufträgen) | OData V2 | Basic Auth, X.509-Zertifikat, OAuth 2.0 Client Credentials | keine | Fertigungsauftrag | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`API_OUTBOUND_DELIVERY_SRV_0002`](https://api.sap.com/api/API_OUTBOUND_DELIVERY_SRV_0002/overview) (Auslieferungen) | OData V2 | Basic Auth, X.509-Zertifikat, OAuth 2.0 Client Credentials | keine | Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`API_MATERIAL_DOCUMENT_SRV`](https://api.sap.com/api/API_MATERIAL_DOCUMENT_SRV/overview) (Warenbewegungen (Wareneingang, Warenausgang)) | OData V2 | Basic Auth, X.509-Zertifikat, OAuth 2.0 Client Credentials | keine | Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`API_CUSTOMER_RETURN_SRV`](https://api.sap.com/api/API_CUSTOMER_RETURN_SRV/overview) (Kundenretouren) | OData V2 | Basic Auth, X.509-Zertifikat, OAuth 2.0 Client Credentials | keine | Retoure | geprüft in der Herstellerdokumentation, 24.10.2026 | | `sap.s4.beh.product.v1.Product.Changed.v1` (Ereignis „Produkt geändert“ über SAP Event Mesh) | REST | OAuth 2.0 Client Credentials | Geschäftsereignisse (nicht signiert) | Produkt-/Materialstamm | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `CE_PRODUCT_0002`: Die ältere API API_PRODUCT_SRV (OData V2) ist nur noch für Produkttexte gedacht. - `CE_API_MATERIALSERIALNUMBER_0001`: SAP-Hinweis 3772443: Bei Serialisierungsstufe 1 antwortet das Anlegen mit 201, legt aber nichts an. Lesen ist nicht betroffen; Seriennummern entstehen dann über die Fertigung oder IQ01. - `sap.s4.beh.product.v1.Product.Changed.v1`: CloudEvents mit schlanker Nutzlast (Produktschlüssel); das Produkt liest die Middleware danach über die API. **Importvorlagen (Presets): SAP-Serialnummernliste** (`sap-serial-numbers`): Eine Liste der Serialnummern aus SAP S/4HANA, wie sie eine Middleware aus der Serialnummern-API oder dem Wareneingang aus der Fertigung zusammenstellt. Setzt voraus, dass die Modellkennung des Batteriemodells in BatteryPass die SAP-Materialnummer ist. Das Buchungsdatum des Wareneingangs steht für das Herstelldatum; das Werk bleibt ungemappt (die Produktionsstätte wählen Sie in den Importoptionen). | Spalte im Export | Feld in BatteryPass | | -------------------------------------------------- | ----------------------------------------------------- | | `Material` (auch Product, Materialnummer, Produkt) | Modellkennung / SKU `identification.model_identifier` | | `SerialNumber` (auch Serial Number, Serialnummer) | Seriennummer `identification.serial_number` | | `PostingDate` (auch Posting Date, Buchungsdatum) | Herstellungsdatum `identification.manufacturing_date` | **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Zustandsdaten aus BMS und Service](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms.md): Ein Batteriemanagementsystem oder ein Servicewerkzeug meldet Messwerte wie Kapazitätsverlust oder Zyklen; BatteryPass übernimmt neuere Messungen und veröffentlicht nach der Richtlinie der Organisation. Scopes des Schlüssels: read, condition - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # SAP S/4HANA (On-Premise und Private Edition) > Wie ein SAP S/4HANA im eigenen Rechenzentrum oder als Private Edition Seriennummern an BatteryPass übergibt. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für SAP S/4HANA und hat keine > Partnerschaft mit SAP; nichts hier ist eine Freigabe des Herstellers. Die `OP_`-APIs des SAP Business Accelerator Hub entsprechen den Cloud-APIs: `OP_API_MATERIALSERIALNUMBER_0001` für Seriennummern, `OP_API_BATCH_SRV_0001` für Chargen und `OP_API_PRODUCTION_ORDER_2_SRV_0001` für Fertigungsaufträge. Die Middleware erreicht das System meist über den SAP Cloud Connector oder ein Reverse Proxy im Werksnetz; das SAP-System selbst ist nie aus dem Internet erreichbar. Mit BatteryPass spricht nur die Middleware, mit ihrem eigenen Schlüssel. Für [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) liest die Middleware die Seriennummern einer Charge und schreibt eine Datei mit dem Preset `sap-serial-numbers` (Materialnummer, Seriennummer, Buchungsdatum des Wareneingangs). Es setzt voraus, dass die Modellkennung in BatteryPass die SAP-Materialnummer ist. Änderungen am Materialstamm kommen als `MATMAS`-IDocs über ALE-Änderungszeiger oder als RAP-Geschäftsereignisse über SAP Event Mesh. `SERDAT` ist die ALE-Nachricht zur Serialisierung und keine Seriennummern-IDoc: Seriennummern kommen über die OData-APIs. Pass-Links fließen zurück wie im [Ablauf für Pass-Links](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp). ## Schnittstellen und Prüfstand - **Hersteller:** SAP - **On-Premise:** On-premise and Private Edition with the OP\_ APIs of the SAP Business Accelerator Hub Die OP\_-APIs entsprechen den Cloud-APIs; die Middleware erreicht das System meist über den SAP Cloud Connector oder ein Reverse Proxy im Werksnetz, nie direkt aus dem Internet. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | -------------------------------------------------------------------------------------------------------------------------------- | --------- | ---------------------------------------------- | ------------------------------------ | ----------------------------------------- | -------------------------------------------------- | | [`OP_API_PRODUCT_SRV_0001`](https://api.sap.com/api/OP_API_PRODUCT_SRV_0001/overview) (Produktstamm) | OData V2 | Basic Auth, X.509-Zertifikat | keine | Produkt-/Materialstamm | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`OP_API_MATERIALSERIALNUMBER_0001`](https://api.sap.com/api/OP_API_MATERIALSERIALNUMBER_0001/overview) (Serialnummern) | OData V4 | Basic Auth, X.509-Zertifikat | keine | Charge/Seriennummer | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`OP_API_BATCH_SRV_0001`](https://api.sap.com/api/OP_API_BATCH_SRV_0001/overview) (Chargen) | OData V2 | Basic Auth, X.509-Zertifikat | keine | Charge/Seriennummer | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`OP_API_PRODUCTION_ORDER_2_SRV_0001`](https://api.sap.com/api/OP_API_PRODUCTION_ORDER_2_SRV_0001/overview) (Fertigungsaufträge) | OData V2 | Basic Auth, X.509-Zertifikat | keine | Fertigungsauftrag | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`OP_CUSTOMERRETURN_0001`](https://api.sap.com/api/OP_CUSTOMERRETURN_0001/overview) (Kundenretouren) | OData V4 | Basic Auth, X.509-Zertifikat | keine | Retoure | geprüft in der Herstellerdokumentation, 24.10.2026 | | `MATMAS` (IDoc für den Materialstamm) | IDoc | Basic Auth, X.509-Zertifikat | Geschäftsereignisse (nicht signiert) | Produkt-/Materialstamm | geprüft in der Herstellerdokumentation, 24.10.2026 | | `RAP business events` (Geschäftsereignisse des RAP-Modells) | REST | OAuth 2.0 Client Credentials, X.509-Zertifikat | Geschäftsereignisse (nicht signiert) | Produkt-/Materialstamm, Fertigungsauftrag | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `MATMAS`: SERDAT ist die ALE-Nachricht zur Serialisierung und keine Seriennummern-IDoc; Seriennummern kommen über die OData-APIs. Die ALE-Verteilung sendet MATMAS über Änderungszeiger, sobald sich ein Material ändert. - `RAP business events`: Über SAP Event Mesh oder Advanced Event Mesh; schlanke Ereignisse, die Daten liest die Middleware danach. **Importvorlagen (Presets): SAP-Serialnummernliste** (`sap-serial-numbers`): Eine Liste der Serialnummern aus SAP S/4HANA, wie sie eine Middleware aus der Serialnummern-API oder dem Wareneingang aus der Fertigung zusammenstellt. Setzt voraus, dass die Modellkennung des Batteriemodells in BatteryPass die SAP-Materialnummer ist. Das Buchungsdatum des Wareneingangs steht für das Herstelldatum; das Werk bleibt ungemappt (die Produktionsstätte wählen Sie in den Importoptionen). | Spalte im Export | Feld in BatteryPass | | -------------------------------------------------- | ----------------------------------------------------- | | `Material` (auch Product, Materialnummer, Produkt) | Modellkennung / SKU `identification.model_identifier` | | `SerialNumber` (auch Serial Number, Serialnummer) | Seriennummer `identification.serial_number` | | `PostingDate` (auch Posting Date, Buchungsdatum) | Herstellungsdatum `identification.manufacturing_date` | **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Zustandsdaten aus BMS und Service](https://passport.kabin.sh/docs/de/integrations/workflows/condition-from-bms.md): Ein Batteriemanagementsystem oder ein Servicewerkzeug meldet Messwerte wie Kapazitätsverlust oder Zyklen; BatteryPass übernimmt neuere Messungen und veröffentlicht nach der Richtlinie der Organisation. Scopes des Schlüssels: read, condition - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # weclapp > Wie weclapp über seine REST-API Artikel und Seriennummern an BatteryPass übergibt. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für weclapp und hat keine > Partnerschaft mit weclapp; nichts hier ist eine Freigabe des Herstellers. Die REST-API von weclapp erwartet den API-Token im Header `AuthenticationToken`. Der Token gehört einem Benutzer; legen Sie für die Integration deshalb einen technischen Benutzer mit knappen Rechten an und bewahren Sie seinen Token im Tresor neben dem Schlüssel für BatteryPass auf. Die Middleware liest Artikel und Seriennummern fertiggemeldeter Produktion, schreibt eine Datei mit dem Preset `generic-csv` und stellt sie bereit wie unter [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) beschrieben. Webhooks sind nicht geprüft; fragen Sie zeitgesteuert ab, solange Sie sie nicht bestätigt haben. Pass-Links können an den Artikel- oder Seriennummernsatz zurückgehen wie unter [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp). ## Schnittstellen und Prüfstand - **Hersteller:** weclapp - **Cloud:** weclapp (REST API v1) Der API-Token steht im Header AuthenticationToken; er gehört einem Benutzer, also einen technischen Benutzer mit knappen Rechten anlegen. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | -------------------------------------------------------------------------------- | --------- | ------------- | ---------- | ------------------------------------------------------------------- | -------------------------------------------------- | | [`REST API`](https://www.weclapp.com/api/) (Artikel, Seriennummern, Lieferungen) | REST | API-Schlüssel | keine | Produkt-/Materialstamm, Charge/Seriennummer, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `REST API`: Webhooks nicht geprüft. **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # Xentral > Wie Xentral über seine REST-API und Webhooks Produkte und Lieferungen an BatteryPass übergibt. > [!NOTE] > Diese Seite beschreibt ein Integrationsmuster. BatteryPass liefert keinen Konnektor für Xentral und hat keine > Partnerschaft mit Xentral; nichts hier ist eine Freigabe des Herstellers. Die REST-API von Xentral meldet sich mit persönlichen Zugriffstoken an. Legen Sie für die Integration einen technischen Benutzer an, damit der Token nicht aufhört zu funktionieren, wenn eine Kollegin geht, und bewahren Sie ihn im Tresor auf. Webhooks lassen sich über die API verwalten und melden der Middleware neue Produkte oder Lieferungen. Ob die Zustellungen signiert sind, ist nicht geprüft; die Middleware prüft deshalb Absender und Inhalt und liest den Datensatz über die API, bevor sie handelt. Seriennummern fertiggemeldeter Produktion gehen mit dem Preset `generic-csv` in eine Datei und werden bereitgestellt wie unter [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production) beschrieben. ## Schnittstellen und Prüfstand - **Hersteller:** Xentral ERP Software - **Cloud:** Xentral (REST API) Anmeldung mit persönlichen Zugriffstoken; einen technischen Benutzer für die Integration anlegen. Integrationsmuster, kein Konnektor: BatteryPass liefert keine ERP-Anbindung und ist bei keinem Hersteller zertifiziert. | Schnittstelle | Protokoll | Anmeldung | Ereignisse | Objekte | Prüfstand | | -------------------------------------------------------------------------------------------------- | --------- | --------- | ------------------------- | ------------------------------------------------------------------- | -------------------------------------------------- | | [`REST API`](https://developer.xentral.com/reference/authentication) (Artikel, Lager, Lieferungen) | REST | Token | keine | Produkt-/Materialstamm, Charge/Seriennummer, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | | [`Webhooks`](https://developer.xentral.com/reference/webhooks) (Webhooks verwalten und empfangen) | REST | Token | Webhooks (nicht signiert) | Produkt-/Materialstamm, Lieferung/Warenausgang | geprüft in der Herstellerdokumentation, 24.10.2026 | **Hinweise** - `Webhooks`: Signatur der Zustellungen nicht geprüft; die Middleware prüft Absender und Inhalt, bevor sie handelt. **Importvorlagen (Presets): Allgemeine CSV-Datei** (`generic-csv`): Für jedes ERP und für Dateien, die eine Middleware selbst schreibt: GTIN, Seriennummer, Herstelldatum, Produktionsstätte (Name oder GLN), Modellversion und Batteriestatus, mit deutschen und englischen Spaltennamen. | Spalte im Export | Feld in BatteryPass | | ------------------------------------------------------------- | ----------------------------------------------------- | | `GTIN` (auch EAN, GTIN-13, GTIN-14) | GTIN (GS1) `identification.gtin` | | `Serial number` (auch Seriennummer, serial_number) | Seriennummer `identification.serial_number` | | `Manufacturing date` (auch Herstelldatum, manufacturing_date) | Herstellungsdatum `identification.manufacturing_date` | | `Facility` (auch Produktionsstätte, Facility GLN, Werk-GLN) | Herstellungsort `identification.manufacturing_place` | | `Model version` (auch Modellversion, model_version) | Modellversion `model_version` | | `Battery status` (auch Batteriestatus, battery_status) | Batteriestatus `identification.battery_status` | ## Abläufe - [Einheiten aus der Fertigung](https://passport.kabin.sh/docs/de/integrations/workflows/units-from-production.md): Seriennummern aus Fertigungsrückmeldung oder Wareneingang werden als Import bereitgestellt; Mitglieder prüfen, übernehmen und geben frei, bevor ein Pass erscheint. Scopes des Schlüssels: import - [Pass-Links zurück ins ERP](https://passport.kabin.sh/docs/de/integrations/workflows/passport-links-to-erp.md): Die Middleware folgt dem Änderungsfeed und schreibt den GS1 Digital Link und den Passstatus an den Seriennummernsatz im ERP, damit Etiketten und Lieferpapiere ihn tragen. Scopes des Schlüssels: read - [Rücksendungen und Reparaturen als Lebenszyklus-Ereignisse](https://passport.kabin.sh/docs/de/integrations/workflows/returns-and-repairs.md): Abgeschlossene Reparaturen, Prüfungen und Komponententausche aus Service- oder Retourenaufträgen erscheinen als Ereignisse in der Historie der Einheit. Scopes des Schlüssels: read, condition - [Produktänderung führt zu neuer Modellversion](https://passport.kabin.sh/docs/de/integrations/workflows/product-change.md): Eine Änderung am Produktstamm im ERP löst eine Benachrichtigung aus; ein Mitglied legt in BatteryPass eine neue Modellversion an. Deklarierte Daten werden nie automatisch geschrieben. Scopes des Schlüssels: read --- # API-Referenz > Jede Operation des veröffentlichten Vertrags, mit Playground und Codebeispielen. Die Referenz wird beim Bau dieser Seiten aus dem veröffentlichten Vertrag der Integrations-API erzeugt (OpenAPI 3.0). Sie zeigt genau die Operationen, die ein API-Schlüssel oder ein öffentlicher Aufruf nutzen kann; die Operationen des Portals sind nicht Teil davon (siehe [Stabilität und Versionen](https://passport.kabin.sh/docs/de/versioning)). ## Grundlagen | | | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Basis-URL | `https:///api/v1` (siehe [Umgebungen](https://passport.kabin.sh/docs/de/environments)) | | Authentifizierung | `Authorization: Bearer bp_live_…` mit Scope `read`, `condition` oder `import` ([Authentifizierung](https://passport.kabin.sh/docs/de/authentication)) | | Format | JSON, Zeitpunkte in UTC, IDs als UUID | | Fehler | Problem Details nach RFC 9457 mit stabilem `code` ([Fehler](https://passport.kabin.sh/docs/de/errors)) | | Listen | Keyset-Paginierung mit `limit` und `cursor` ([Paginierung](https://passport.kabin.sh/docs/de/pagination)) | | Grenzen | `429 rate_limited` mit `Retry-After` ([Ratenlimits](https://passport.kabin.sh/docs/de/rate-limits)) | > [!TIP] > **Playground** > > Jede Operationsseite hat einen Playground („Try in Playground“). Er sendet die Anfrage aus Ihrem Browser an > die BatteryPass-Instanz, auf der diese Dokumentation läuft. Ihr Schlüssel bleibt nur in diesem Tab > (sessionStorage) und wird ohne Cookies gesendet; nutzen Sie dafür einen Schlüssel mit Scope `read`. ## Vertrag herunterladen - [`openapi.json`](https://passport.kabin.sh/docs/openapi.json) und [`openapi.yaml`](https://passport.kabin.sh/docs/openapi.yaml): der Stand, aus dem diese Dokumentation gebaut wurde. - `/api/v1/openapi.json` und `/api/v1/openapi.yaml` auf Ihrer BatteryPass-Adresse: der Stand, den Ihre Instanz gerade ausliefert. ## Alle Operationen ### Katalog [Units (individual batteries) of the active organisation](https://passport.kabin.sh/docs/de/api-reference/catalog/list-batteries.md): `GET /batteries` (read) [The units as a spreadsheet with the import's columns](https://passport.kabin.sh/docs/de/api-reference/catalog/export-units.md): `GET /batteries/export` (read) [A unit](https://passport.kabin.sh/docs/de/api-reference/catalog/get-battery.md): `GET /batteries/{batteryId}` (read) [List the battery models of the active organisation](https://passport.kabin.sh/docs/de/api-reference/catalog/list-battery-models.md): `GET /battery-models` (read) [A battery model](https://passport.kabin.sh/docs/de/api-reference/catalog/get-battery-model.md): `GET /battery-models/{modelId}` (read) [Versions of a battery model, newest effective date first](https://passport.kabin.sh/docs/de/api-reference/catalog/list-model-versions.md): `GET /battery-models/{modelId}/versions` (read) [A model version](https://passport.kabin.sh/docs/de/api-reference/catalog/get-model-version.md): `GET /model-versions/{versionId}` (read) ### Werte [Registry evaluation of the unit-level fields](https://passport.kabin.sh/docs/de/api-reference/values/get-battery-completeness.md): `GET /batteries/{batteryId}/completeness` (read) [Current unit-level values of a unit](https://passport.kabin.sh/docs/de/api-reference/values/list-battery-values.md): `GET /batteries/{batteryId}/values` (read) [All revisions of one unit-level value, newest first](https://passport.kabin.sh/docs/de/api-reference/values/list-battery-value-history.md): `GET /batteries/{batteryId}/values/{fieldKey}/history` (read) [Registry evaluation of the model-level fields](https://passport.kabin.sh/docs/de/api-reference/values/get-model-version-completeness.md): `GET /model-versions/{versionId}/completeness` (read) [Current model-level values of a version](https://passport.kabin.sh/docs/de/api-reference/values/list-model-version-values.md): `GET /model-versions/{versionId}/values` (read) [All revisions of one model-level value, newest first](https://passport.kabin.sh/docs/de/api-reference/values/list-model-version-value-history.md): `GET /model-versions/{versionId}/values/{fieldKey}/history` (read) ### Nachweise [The evidence library, newest first](https://passport.kabin.sh/docs/de/api-reference/evidence/list-evidence.md): `GET /evidence` (read) [One document with its revisions and where it is used](https://passport.kabin.sh/docs/de/api-reference/evidence/get-evidence.md): `GET /evidence/{evidenceId}` (read) [Download the file](https://passport.kabin.sh/docs/de/api-reference/evidence/download-evidence.md): `GET /evidence/{evidenceId}/download` (read) ### Pässe [The passport of a unit](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport.md): `GET /batteries/{batteryId}/passport` (read) [The QR code of the unit's GS1 Digital Link](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-qr.md): `GET /batteries/{batteryId}/passport/qr` (read) [The passport's registration with the EU registry](https://passport.kabin.sh/docs/de/api-reference/passports/get-registry-submission-status.md): `GET /batteries/{batteryId}/passport/registration` (read) [The published versions of a unit's passport, newest first](https://passport.kabin.sh/docs/de/api-reference/passports/list-passport-versions.md): `GET /batteries/{batteryId}/passport/versions` (read) [One published version with its signed snapshot and validation report](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-version.md): `GET /batteries/{batteryId}/passport/versions/{versionNo}` (read) [The BatteryPass v2.0 export of a version (all tiers)](https://passport.kabin.sh/docs/de/api-reference/passports/export-passport-json.md): `GET /batteries/{batteryId}/passport/versions/{versionNo}/batterypass-v2` (read) [What changed from an earlier version to this one](https://passport.kabin.sh/docs/de/api-reference/passports/get-passport-version-diff.md): `GET /batteries/{batteryId}/passport/versions/{versionNo}/diff` (read) [A PDF of a version with every tier, restricted rows tagged, the signature and the QR code](https://passport.kabin.sh/docs/de/api-reference/passports/export-passport-pdf.md): `GET /batteries/{batteryId}/passport/versions/{versionNo}/pdf` (read) ### Lebenszyklus [The condition data of a unit (the registry's dynamic fields)](https://passport.kabin.sh/docs/de/api-reference/lifecycle/get-condition.md): `GET /batteries/{batteryId}/condition` (read) [Report measured condition data (merge)](https://passport.kabin.sh/docs/de/api-reference/lifecycle/update-condition.md): `PATCH /batteries/{batteryId}/condition` (condition) [The lifecycle of a unit, newest first](https://passport.kabin.sh/docs/de/api-reference/lifecycle/list-lifecycle-events.md): `GET /batteries/{batteryId}/lifecycle-events` (read) [Record an event in the life of a unit](https://passport.kabin.sh/docs/de/api-reference/lifecycle/create-lifecycle-event.md): `POST /batteries/{batteryId}/lifecycle-events` (condition) ### Feldregister [Version, content hash and status of the field registry in use](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-info.md): `GET /registry` (read) [The editor schema of a battery category](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-schema.md): `GET /registry/schema` (read) [The registry in use and how many of the organisation's passports need an update](https://passport.kabin.sh/docs/de/api-reference/registry/get-registry-status.md): `GET /registry/status` (read) ### Standorte [Manufacturing facilities of the active organisation](https://passport.kabin.sh/docs/de/api-reference/facilities/list-facilities.md): `GET /facilities` (read) ### Öffentlich [The public passport of a unit, looked up by GTIN and serial number](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-by-product-id.md): `GET /public/dpps/by-product-id/{productId}` (public) [The public passport (current version)](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport.md): `GET /public/passports/{passportId}` (public) [A public document of a public passport](https://passport.kabin.sh/docs/de/api-reference/public/download-public-passport-document.md): `GET /public/passports/{passportId}/documents/{documentId}` (public) [Check the signatures of a version on the server](https://passport.kabin.sh/docs/de/api-reference/public/verify-passport.md): `GET /public/passports/{passportId}/verify` (public) [The public passport as it was at a point in time](https://passport.kabin.sh/docs/de/api-reference/public/get-public-passport-version.md): `GET /public/passports/{passportId}/version` (public) ### Änderungen [The organisation's changes, oldest first, for polling](https://passport.kabin.sh/docs/de/api-reference/changes/list-changes.md): `GET /changes` (read) ### Importe [Imports of the active organisation, newest first](https://passport.kabin.sh/docs/de/api-reference/imports/list-imports.md): `GET /imports` (import) [Upload a spreadsheet of units](https://passport.kabin.sh/docs/de/api-reference/imports/create-import.md): `POST /imports` (import) [One import with its mapping and summary](https://passport.kabin.sh/docs/de/api-reference/imports/get-import.md): `GET /imports/{importId}` (import) [Map the columns of an import](https://passport.kabin.sh/docs/de/api-reference/imports/update-import-mapping.md): `PUT /imports/{importId}/mapping` (import) [Start validating every row of an import](https://passport.kabin.sh/docs/de/api-reference/imports/validate-import.md): `POST /imports/{importId}/validate` (import) --- # The units as a spreadsheet with the import's columns > `GET /batteries/export` · Scope: `read` · operationId `exportUnits` Any member. The units matching the filters (at most 10 000) with the columns of an import: model, version, the unit's fields and every unit-level field with a value, so the file can be edited and imported again (`docs/architecture.md` §7). XLSX by default. ## Query-Parameter - **battery_model_id** `string (uuid)` - **model_version_id** `string (uuid)` - **passport_status** `string` (einer von: `draft`, `qr_locked`, `published`, `update_needed`, `suspended`, `withdrawn`, `deleted_recycled`): Status of a unit's passport (`docs/domain-model.md` §7). - **serial_prefix** `string` - **import_id** `string (uuid)` - **format** `string` (einer von: `xlsx`, `csv`): The file format of a spreadsheet download. ## Antworten ### 200 (`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, `text/csv`) The spreadsheet. `any` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/export" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # A battery model > `GET /battery-models/{modelId}` · Scope: `read` · operationId `getBatteryModel` Any member. ## Pfad-Parameter - **modelId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The battery model. - **archived_at** `string | null` (erforderlich) - **category** `string` (erforderlich, einer von: `ev`, `lmt`, `industrial`, `stationary`, `industrial_without_bms`): Battery category per Regulation (EU) 2023/1542; drives registry requirements. - **created_at** `string (date-time)` (erforderlich) - **gtin** `string | null` (erforderlich): GTIN-14 (normalised, check digit valid). - **id** `string (uuid)` (erforderlich) - **name** `string` (erforderlich) - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **sku** `string | null` (erforderlich) - **updated_at** `string (date-time)` (erforderlich) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/battery-models/{modelId}" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # A unit > `GET /batteries/{batteryId}` · Scope: `read` · operationId `getBattery` Any member. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The unit. - **battery_model_id** `string (uuid)` (erforderlich) - **battery_status** `string` (erforderlich, einer von: `original`, `repurposed`, `reused`, `remanufactured`, `waste`) - **created_at** `string (date-time)` (erforderlich) - **facility_id** `string (uuid)` - **id** `string (uuid)` (erforderlich) - **identifiers_locked_at** `string (date-time)`: Set when the identifiers were locked (pre-print) or first published; serial and model are fixed afterwards. - **import_id** `string (uuid)`: The import that created the unit. - **manufacturing_date** `string (date)` - **model_version_id** `string (uuid)` (erforderlich) - **passport_status** `string` (erforderlich, einer von: `draft`, `qr_locked`, `published`, `update_needed`, `suspended`, `withdrawn`, `deleted_recycled`): Status of a unit's passport (`docs/domain-model.md` §7). - **predecessor_battery_id** `string (uuid)`: Second life: the unit this battery was repurposed, reused or remanufactured from. - **putting_into_service** `string (date)` - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **serial_number** `string` (erforderlich) - **successor_battery_ids** `string (uuid)[]`: Second life: the units made from this battery. - **updated_at** `string (date-time)` (erforderlich) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # A model version > `GET /model-versions/{versionId}` · Scope: `read` · operationId `getModelVersion` Any member. ## Pfad-Parameter - **versionId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The version. - **approved_at** `string (date-time)` - **approved_by_name** `string` - **battery_model_id** `string (uuid)` (erforderlich) - **created_at** `string (date-time)` (erforderlich) - **effective_from** `string (date)` (erforderlich) - **id** `string (uuid)` (erforderlich) - **registry_version** `string`: Registry version of the last evaluation after a change. - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **status** `string` (erforderlich, einer von: `draft`, `approved`, `superseded`) - **unit_count** `integer` (erforderlich) - **updated_at** `string (date-time)` (erforderlich) - **version_label** `string` (erforderlich) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/model-versions/{versionId}" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Units (individual batteries) of the active organisation > `GET /batteries` · Scope: `read` · operationId `listBatteries` Any member. Ordered by serial number, then id; filter by model, version, passport status, serial number prefix or the import that created the units. ## Query-Parameter - **limit** `integer` (Standard: 50): Maximum number of items to return. - **cursor** `string`: Opaque cursor from a previous page's `next_cursor`. - **battery_model_id** `string (uuid)` - **model_version_id** `string (uuid)` - **passport_status** `string` (einer von: `draft`, `qr_locked`, `published`, `update_needed`, `suspended`, `withdrawn`, `deleted_recycled`): Status of a unit's passport (`docs/domain-model.md` §7). - **serial_prefix** `string` - **import_id** `string (uuid)` ## Antworten ### 200 (`application/json`) One page of units. - **items** `Battery[]` (erforderlich) - **battery_model_id** `string (uuid)` (erforderlich) - **battery_status** `string` (erforderlich, einer von: `original`, `repurposed`, `reused`, `remanufactured`, `waste`) - **created_at** `string (date-time)` (erforderlich) - **facility_id** `string (uuid)` - **id** `string (uuid)` (erforderlich) - **identifiers_locked_at** `string (date-time)`: Set when the identifiers were locked (pre-print) or first published; serial and model are fixed afterwards. - **import_id** `string (uuid)`: The import that created the unit. - **manufacturing_date** `string (date)` - **model_version_id** `string (uuid)` (erforderlich) - **passport_status** `string` (erforderlich, einer von: `draft`, `qr_locked`, `published`, `update_needed`, `suspended`, `withdrawn`, `deleted_recycled`): Status of a unit's passport (`docs/domain-model.md` §7). - **predecessor_battery_id** `string (uuid)`: Second life: the unit this battery was repurposed, reused or remanufactured from. - **putting_into_service** `string (date)` - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **serial_number** `string` (erforderlich) - **successor_battery_ids** `string (uuid)[]`: Second life: the units made from this battery. - **updated_at** `string (date-time)` (erforderlich) - **next_cursor** `string | null` (erforderlich): Cursor for the next page, or null when this is the last page. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # List the battery models of the active organisation > `GET /battery-models` · Scope: `read` · operationId `listBatteryModels` Any member. Ordered by name, then id. Archived models only with `include_archived=true`. ## Query-Parameter - **limit** `integer` (Standard: 50): Maximum number of items to return. - **cursor** `string`: Opaque cursor from a previous page's `next_cursor`. - **include_archived** `boolean` (Standard: false) ## Antworten ### 200 (`application/json`) One page of battery models. - **items** `BatteryModel[]` (erforderlich) - **archived_at** `string | null` (erforderlich) - **category** `string` (erforderlich, einer von: `ev`, `lmt`, `industrial`, `stationary`, `industrial_without_bms`): Battery category per Regulation (EU) 2023/1542; drives registry requirements. - **created_at** `string (date-time)` (erforderlich) - **gtin** `string | null` (erforderlich): GTIN-14 (normalised, check digit valid). - **id** `string (uuid)` (erforderlich) - **name** `string` (erforderlich) - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **sku** `string | null` (erforderlich) - **updated_at** `string (date-time)` (erforderlich) - **next_cursor** `string | null` (erforderlich): Cursor for the next page, or null when this is the last page. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/battery-models" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Versions of a battery model, newest effective date first > `GET /battery-models/{modelId}/versions` · Scope: `read` · operationId `listModelVersions` Any member. At most 200. ## Pfad-Parameter - **modelId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The versions. - **items** `ModelVersion[]` (erforderlich) - **approved_at** `string (date-time)` - **approved_by_name** `string` - **battery_model_id** `string (uuid)` (erforderlich) - **created_at** `string (date-time)` (erforderlich) - **effective_from** `string (date)` (erforderlich) - **id** `string (uuid)` (erforderlich) - **registry_version** `string`: Registry version of the last evaluation after a change. - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **status** `string` (erforderlich, einer von: `draft`, `approved`, `superseded`) - **unit_count** `integer` (erforderlich) - **updated_at** `string (date-time)` (erforderlich) - **version_label** `string` (erforderlich) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/battery-models/{modelId}/versions" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Registry evaluation of the unit-level fields > `GET /batteries/{batteryId}/completeness` · Scope: `read` · operationId `getBatteryCompleteness` Any member. Evaluated against the category, the unit's putting-into-service date (else its manufacturing date, else the version's effective date) and the organisation's facts; includes the summary of its model version. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The evaluation. - **category** `string` (erforderlich, einer von: `ev`, `lmt`, `industrial`, `stationary`, `industrial_without_bms`): Battery category per Regulation (EU) 2023/1542; drives registry requirements. - **fields** `FieldCompleteness[]` (erforderlich): Visible fields in registry order (hidden fields are left out). - **applies_from** `string (date)` - **display** `string` (erforderlich, einer von: `required_missing`, `required_pending_review`, `ok`, `not_applicable`, `later_required`, `optional_missing`): Combined status shown to users (docs/domain-model.md §5). - **field_key** `string` (erforderlich) - **not_applicable_reason** `string` - **reason** `string` (erforderlich, einer von: `rule`, `condition_met`, `condition_open`, `condition_not_met`, `applies_from`, `trigger`, `marked_not_applicable`): Why the field has its requirement: `rule` (registry rule for the category), `condition_met`, `condition_open` (cannot be decided yet; treated as required), `condition_not_met`, `applies_from` (obligation starts after the reference date), `trigger` (a legal act is still pending), `marked_not_applicable` (a user recorded a reason). - **requirement** `string` (erforderlich, einer von: `required`, `conditional`, `optional`, `not_applicable`, `later_required`) - **section** `string` (erforderlich) - **trigger** `LabelText`: Text in German and English (ADR-010); clients pick the locale. - **value_status** `string` (erforderlich, einer von: `missing`, `captured`, `auto_checked`, `approved`, `rejected`) - **model_version** `CompletenessSummary` - **blocking** `integer` (erforderlich): Fields that count against publishing (required_missing + required_pending_review). - **counts** `StatusCounts` (erforderlich) - **total** `integer` (erforderlich) - **reference_date** `string (date)`: The placed-on-market date obligations were evaluated for. - **registry** `RegistryRef` (erforderlich) - **hash** `string` (erforderlich) - **status** `string` (erforderlich, einer von: `seed`, `verified`): `seed` until verified against primary sources (docs/registry/verification-log.md). - **version** `string` (erforderlich) - **summary** `CompletenessSummary` (erforderlich) - **blocking** `integer` (erforderlich): Fields that count against publishing (required_missing + required_pending_review). - **counts** `StatusCounts` (erforderlich) - **total** `integer` (erforderlich) - **target** `TargetRef` (erforderlich) - **id** `string (uuid)` (erforderlich) - **type** `string` (erforderlich, einer von: `model_version`, `battery`) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/completeness" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Registry evaluation of the model-level fields > `GET /model-versions/{versionId}/completeness` · Scope: `read` · operationId `getModelVersionCompleteness` Any member. Evaluated against the category, the version's effective date and the organisation's facts. ## Pfad-Parameter - **versionId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The evaluation. - **category** `string` (erforderlich, einer von: `ev`, `lmt`, `industrial`, `stationary`, `industrial_without_bms`): Battery category per Regulation (EU) 2023/1542; drives registry requirements. - **fields** `FieldCompleteness[]` (erforderlich): Visible fields in registry order (hidden fields are left out). - **applies_from** `string (date)` - **display** `string` (erforderlich, einer von: `required_missing`, `required_pending_review`, `ok`, `not_applicable`, `later_required`, `optional_missing`): Combined status shown to users (docs/domain-model.md §5). - **field_key** `string` (erforderlich) - **not_applicable_reason** `string` - **reason** `string` (erforderlich, einer von: `rule`, `condition_met`, `condition_open`, `condition_not_met`, `applies_from`, `trigger`, `marked_not_applicable`): Why the field has its requirement: `rule` (registry rule for the category), `condition_met`, `condition_open` (cannot be decided yet; treated as required), `condition_not_met`, `applies_from` (obligation starts after the reference date), `trigger` (a legal act is still pending), `marked_not_applicable` (a user recorded a reason). - **requirement** `string` (erforderlich, einer von: `required`, `conditional`, `optional`, `not_applicable`, `later_required`) - **section** `string` (erforderlich) - **trigger** `LabelText`: Text in German and English (ADR-010); clients pick the locale. - **value_status** `string` (erforderlich, einer von: `missing`, `captured`, `auto_checked`, `approved`, `rejected`) - **model_version** `CompletenessSummary` - **blocking** `integer` (erforderlich): Fields that count against publishing (required_missing + required_pending_review). - **counts** `StatusCounts` (erforderlich) - **total** `integer` (erforderlich) - **reference_date** `string (date)`: The placed-on-market date obligations were evaluated for. - **registry** `RegistryRef` (erforderlich) - **hash** `string` (erforderlich) - **status** `string` (erforderlich, einer von: `seed`, `verified`): `seed` until verified against primary sources (docs/registry/verification-log.md). - **version** `string` (erforderlich) - **summary** `CompletenessSummary` (erforderlich) - **blocking** `integer` (erforderlich): Fields that count against publishing (required_missing + required_pending_review). - **counts** `StatusCounts` (erforderlich) - **total** `integer` (erforderlich) - **target** `TargetRef` (erforderlich) - **id** `string (uuid)` (erforderlich) - **type** `string` (erforderlich, einer von: `model_version`, `battery`) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/model-versions/{versionId}/completeness" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # All revisions of one unit-level value, newest first > `GET /batteries/{batteryId}/values/{fieldKey}/history` · Scope: `read` · operationId `listBatteryValueHistory` Any member. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) - **fieldKey** `string` (erforderlich): Registry field key, e.g. `performance.nominal_voltage`. ## Antworten ### 200 (`application/json`) The revisions. - **items** `FieldValue[]` (erforderlich) - **checks** `ValueCheck[]` (erforderlich) - **evidence** `EvidenceRef[]`: Documents this revision rests on (set on approved revisions and supplier submissions). - **field_key** `string` (erforderlich) - **measured_at** `string (date-time)`: When a measured value (condition data, ADR-016) was measured. - **recorded_at** `string (date-time)` (erforderlich) - **recorded_by** `UserRef` - **registry_version** `string` (erforderlich) - **responsible** `UserRef` - **review_note** `string` - **review_status** `string` (erforderlich, einer von: `captured`, `auto_checked`, `approved`, `rejected`) - **reviewed_at** `string (date-time)` - **reviewed_by** `UserRef` - **revision** `integer` (erforderlich) - **source** `string` (erforderlich, einer von: `manual`, `import`, `supplier`, `api`, `system`) - **superseded_at** `string (date-time)` - **unit** `string` - **value** `any | null` (erforderlich): The value as JSON; null when the field was cleared. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/values/{fieldKey}/history" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Current unit-level values of a unit > `GET /batteries/{batteryId}/values` · Scope: `read` · operationId `listBatteryValues` Any member. One entry per field that has (or had) a value; `value` is null when it was cleared. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The current values. - **items** `FieldValue[]` (erforderlich) - **checks** `ValueCheck[]` (erforderlich) - **evidence** `EvidenceRef[]`: Documents this revision rests on (set on approved revisions and supplier submissions). - **field_key** `string` (erforderlich) - **measured_at** `string (date-time)`: When a measured value (condition data, ADR-016) was measured. - **recorded_at** `string (date-time)` (erforderlich) - **recorded_by** `UserRef` - **registry_version** `string` (erforderlich) - **responsible** `UserRef` - **review_note** `string` - **review_status** `string` (erforderlich, einer von: `captured`, `auto_checked`, `approved`, `rejected`) - **reviewed_at** `string (date-time)` - **reviewed_by** `UserRef` - **revision** `integer` (erforderlich) - **source** `string` (erforderlich, einer von: `manual`, `import`, `supplier`, `api`, `system`) - **superseded_at** `string (date-time)` - **unit** `string` - **value** `any | null` (erforderlich): The value as JSON; null when the field was cleared. - **links** `EvidenceLink[]` (erforderlich): Active evidence links of the target, per field. - **declared_value** `any | null`: The value the document states for the field, if recorded. - **evidence** `EvidenceRef` (erforderlich) - **field_key** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **linked_at** `string (date-time)` (erforderlich) - **linked_by** `UserRef` - **registry** `RegistryRef` (erforderlich) - **hash** `string` (erforderlich) - **status** `string` (erforderlich, einer von: `seed`, `verified`): `seed` until verified against primary sources (docs/registry/verification-log.md). - **version** `string` (erforderlich) - **target** `TargetRef` (erforderlich) - **id** `string (uuid)` (erforderlich) - **type** `string` (erforderlich, einer von: `model_version`, `battery`) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/values" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # All revisions of one model-level value, newest first > `GET /model-versions/{versionId}/values/{fieldKey}/history` · Scope: `read` · operationId `listModelVersionValueHistory` Any member. ## Pfad-Parameter - **versionId** `string (uuid)` (erforderlich) - **fieldKey** `string` (erforderlich): Registry field key, e.g. `performance.nominal_voltage`. ## Antworten ### 200 (`application/json`) The revisions. - **items** `FieldValue[]` (erforderlich) - **checks** `ValueCheck[]` (erforderlich) - **evidence** `EvidenceRef[]`: Documents this revision rests on (set on approved revisions and supplier submissions). - **field_key** `string` (erforderlich) - **measured_at** `string (date-time)`: When a measured value (condition data, ADR-016) was measured. - **recorded_at** `string (date-time)` (erforderlich) - **recorded_by** `UserRef` - **registry_version** `string` (erforderlich) - **responsible** `UserRef` - **review_note** `string` - **review_status** `string` (erforderlich, einer von: `captured`, `auto_checked`, `approved`, `rejected`) - **reviewed_at** `string (date-time)` - **reviewed_by** `UserRef` - **revision** `integer` (erforderlich) - **source** `string` (erforderlich, einer von: `manual`, `import`, `supplier`, `api`, `system`) - **superseded_at** `string (date-time)` - **unit** `string` - **value** `any | null` (erforderlich): The value as JSON; null when the field was cleared. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/model-versions/{versionId}/values/{fieldKey}/history" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Current model-level values of a version > `GET /model-versions/{versionId}/values` · Scope: `read` · operationId `listModelVersionValues` Any member. One entry per field that has (or had) a value; `value` is null when it was cleared. ## Pfad-Parameter - **versionId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The current values. - **items** `FieldValue[]` (erforderlich) - **checks** `ValueCheck[]` (erforderlich) - **evidence** `EvidenceRef[]`: Documents this revision rests on (set on approved revisions and supplier submissions). - **field_key** `string` (erforderlich) - **measured_at** `string (date-time)`: When a measured value (condition data, ADR-016) was measured. - **recorded_at** `string (date-time)` (erforderlich) - **recorded_by** `UserRef` - **registry_version** `string` (erforderlich) - **responsible** `UserRef` - **review_note** `string` - **review_status** `string` (erforderlich, einer von: `captured`, `auto_checked`, `approved`, `rejected`) - **reviewed_at** `string (date-time)` - **reviewed_by** `UserRef` - **revision** `integer` (erforderlich) - **source** `string` (erforderlich, einer von: `manual`, `import`, `supplier`, `api`, `system`) - **superseded_at** `string (date-time)` - **unit** `string` - **value** `any | null` (erforderlich): The value as JSON; null when the field was cleared. - **links** `EvidenceLink[]` (erforderlich): Active evidence links of the target, per field. - **declared_value** `any | null`: The value the document states for the field, if recorded. - **evidence** `EvidenceRef` (erforderlich) - **field_key** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **linked_at** `string (date-time)` (erforderlich) - **linked_by** `UserRef` - **registry** `RegistryRef` (erforderlich) - **hash** `string` (erforderlich) - **status** `string` (erforderlich, einer von: `seed`, `verified`): `seed` until verified against primary sources (docs/registry/verification-log.md). - **version** `string` (erforderlich) - **target** `TargetRef` (erforderlich) - **id** `string (uuid)` (erforderlich) - **type** `string` (erforderlich, einer von: `model_version`, `battery`) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/model-versions/{versionId}/values" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Download the file > `GET /evidence/{evidenceId}/download` · Scope: `read` · operationId `downloadEvidence` Documents of the tiers `public` and `legitimate_interest` can be downloaded by any member; `authority` and `internal` documents by editors and above. Always sent as an attachment. ## Pfad-Parameter - **evidenceId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`*/*`) The file. `any` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/evidence/{evidenceId}/download" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # One document with its revisions and where it is used > `GET /evidence/{evidenceId}` · Scope: `read` · operationId `getEvidence` Any member. ## Pfad-Parameter - **evidenceId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The document. - **access_tier** `string` (erforderlich, einer von: `public`, `legitimate_interest`, `authority`, `internal`): Access tier of a field on the passport (Art. 77(2), Annex XIII). - **created_at** `string (date-time)` (erforderlich) - **filename** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **kind** `string` (erforderlich, einer von: `declaration_of_conformity`, `test_report`, `dismantling_manual`, `safety_instructions`, `datasheet`, `diagnostic_report`, `study`, `image`, `other`) - **mime** `string` (erforderlich) - **revision** `integer` (erforderlich) - **revisions** `EvidenceRef[]`: All revisions of the document, newest first (getEvidence only). - **access_tier** `string` (erforderlich, einer von: `public`, `legitimate_interest`, `authority`, `internal`): Access tier of a field on the passport (Art. 77(2), Annex XIII). - **filename** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **kind** `string` (erforderlich, einer von: `declaration_of_conformity`, `test_report`, `dismantling_manual`, `safety_instructions`, `datasheet`, `diagnostic_report`, `study`, `image`, `other`) - **revision** `integer` (erforderlich) - **superseded** `boolean` (erforderlich) - **title** `string` (erforderlich) - **sha256** `string` (erforderlich) - **size_bytes** `integer (int64)` (erforderlich) - **source** `string` (erforderlich, einer von: `manual`, `supplier`) - **superseded** `EvidenceSupersession` - **at** `string (date-time)` (erforderlich) - **by** `UserRef` - **reason** `string` (erforderlich) - **replaced_by** `string (uuid)` - **supersedes** `string (uuid)`: The previous revision of this document. - **supplier_request_id** `string (uuid)` - **title** `string` (erforderlich) - **uploaded_by** `UserRef` - **display_name** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **usages** `EvidenceUsage[]`: Where the document is linked or used as a value (getEvidence only). - **declared_value** `any | null`: The value the document states (links only). - **field_key** `string` (erforderlich) - **target** `TargetRef` (erforderlich) - **target_label** `string` (erforderlich): Model name and version label, or the unit's serial number. - **via** `string` (erforderlich, einer von: `link`, `value`): `link`: an evidence link; `value`: the current value of a `file` field is this document. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/evidence/{evidenceId}" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # The evidence library, newest first > `GET /evidence` · Scope: `read` · operationId `listEvidence` Any member. Superseded revisions are left out unless `include_superseded=true`. ## Query-Parameter - **limit** `integer` (Standard: 50): Maximum number of items to return. - **cursor** `string`: Opaque cursor from a previous page's `next_cursor`. - **kind** `string` (einer von: `declaration_of_conformity`, `test_report`, `dismantling_manual`, `safety_instructions`, `datasheet`, `diagnostic_report`, `study`, `image`, `other`) - **include_superseded** `boolean` (Standard: false) ## Antworten ### 200 (`application/json`) A page of documents. - **items** `Evidence[]` (erforderlich) - **access_tier** `string` (erforderlich, einer von: `public`, `legitimate_interest`, `authority`, `internal`): Access tier of a field on the passport (Art. 77(2), Annex XIII). - **created_at** `string (date-time)` (erforderlich) - **filename** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **kind** `string` (erforderlich, einer von: `declaration_of_conformity`, `test_report`, `dismantling_manual`, `safety_instructions`, `datasheet`, `diagnostic_report`, `study`, `image`, `other`) - **mime** `string` (erforderlich) - **revision** `integer` (erforderlich) - **revisions** `EvidenceRef[]`: All revisions of the document, newest first (getEvidence only). - **sha256** `string` (erforderlich) - **size_bytes** `integer (int64)` (erforderlich) - **source** `string` (erforderlich, einer von: `manual`, `supplier`) - **superseded** `EvidenceSupersession` - **supersedes** `string (uuid)`: The previous revision of this document. - **supplier_request_id** `string (uuid)` - **title** `string` (erforderlich) - **uploaded_by** `UserRef` - **usages** `EvidenceUsage[]`: Where the document is linked or used as a value (getEvidence only). - **next_cursor** `string | null` (erforderlich): Cursor for the next page, or null when this is the last page. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/evidence" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # The BatteryPass v2.0 export of a version (all tiers) > `GET /batteries/{batteryId}/passport/versions/{versionNo}/batterypass-v2` · Scope: `read` · operationId `exportPassportJson` Any member. The stored export, including restricted data; sent as an attachment. Its schema report is in `getPassportVersion` (advisory, ADR-012). ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) - **versionNo** `integer` (erforderlich) ## Antworten ### 200 (`application/json`) The export. `JSONDocument` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/passport/versions/{versionNo}/batterypass-v2" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # A PDF of a version with every tier, restricted rows tagged, the signature and the QR code > `GET /batteries/{batteryId}/passport/versions/{versionNo}/pdf` · Scope: `read` · operationId `exportPassportPdf` Any member. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) - **versionNo** `integer` (erforderlich) ## Query-Parameter - **lang** `string` (einer von: `de`, `en`): Preferred language for emails and the portal. ## Antworten ### 200 (`application/pdf`) The PDF. `any` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/passport/versions/{versionNo}/pdf" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # The QR code of the unit's GS1 Digital Link > `GET /batteries/{batteryId}/passport/qr` · Scope: `read` · operationId `getPassportQr` Any member. Rendered on demand from `gs1_digital_link` (error correction M, quiet zone of four modules). Only once the identifiers are locked (`409 conflict` before). ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Query-Parameter - **format** `string` (Standard: "png", einer von: `png`, `svg`) - **size** `integer` (Standard: 512): Width and height of the PNG in pixels (ignored for SVG, which scales). ## Antworten ### 200 (`image/png`, `image/svg+xml`) The image. `any` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 409 (`application/problem+json`) The request conflicts with the current state (`conflict`), e.g. a stale `If-Match` version. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/passport/qr" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # What changed from an earlier version to this one > `GET /batteries/{batteryId}/passport/versions/{versionNo}/diff` · Scope: `read` · operationId `getPassportVersionDiff` Any member. Compares the signed snapshots field by field; `against` defaults to the previous version. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) - **versionNo** `integer` (erforderlich) ## Query-Parameter - **against** `integer` ## Antworten ### 200 (`application/json`) The changes. - **fields** `PassportFieldChange[]` (erforderlich) - **after** `PassportDiffValue` - **before** `PassportDiffValue` - **change** `string` (erforderlich, einer von: `added`, `removed`, `changed`, `provenance`): `provenance`: same value, a newer revision (re-approved, re-measured). - **key** `string` (erforderlich) - **level** `string` (erforderlich, einer von: `model`, `unit`) - **from_version_no** `integer`: Absent when there is nothing to compare with (the first publication). - **metadata** `PassportMetaChange[]` (erforderlich) - **after** `string` - **before** `string` - **key** `string` (erforderlich, einer von: `model_version`, `registry_version`, `category`, `facility`, `operator`, `predecessor`) - **to** `string` (erforderlich, einer von: `version`, `preview`) - **to_version_no** `integer`: Absent in a publish preview. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/passport/versions/{versionNo}/diff" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # One published version with its signed snapshot and validation report > `GET /batteries/{batteryId}/passport/versions/{versionNo}` · Scope: `read` · operationId `getPassportVersion` Any member. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) - **versionNo** `integer` (erforderlich) ## Antworten ### 200 (`application/json`) The version. - **batterypass_v2_issue_count** `integer` (erforderlich) - **batterypass_v2_valid** `boolean` (erforderlich): Whether the BatteryPass v2.0 export passed its schema (advisory, ADR-012). - **canonical_hash** `string` (erforderlich): `sha256:` of the JCS-canonical snapshot (ADR-004). - **key_id** `string` (erforderlich) - **public_hash** `string` (erforderlich): `sha256:` of the JCS-canonical public document (ADR-013). - **published_at** `string (date-time)` (erforderlich) - **published_by** `string (uuid)` - **published_by_name** `string` - **reason** `string` (erforderlich) - **registry_version** `string` (erforderlich) - **valid_until** `string (date-time)`: When a later version replaced this one. - **version_no** `integer` (erforderlich) - **batterypass_v2** `SchemaReport` (erforderlich): Result of validating a BatteryPass v2.0 export against its schema (advisory, ADR-012). - **issues** `SchemaIssue[]` (erforderlich) - **schema** `string` (erforderlich): The schema file, e.g. `Stationary_Industrial_Above_2kWh_Feb27_2.0.1.json`. - **valid** `boolean` (erforderlich) - **public_document** `PassportDocument` (erforderlich): A passport version projected to an access tier. The `public` projection is stored and signed at publication (ADR-013); verify it by canonicalising this object with RFC 8785 (JCS), hashing it with SHA-256 and checking the Ed25519 signature. - **category** `PassportCategory` (erforderlich) - **economic_operator** `PublicProfile`: The economic operator as shown on public passports. Every property is optional; publishing (M4) requires a complete profile. - **gs1_digital_link** `string` (erforderlich) - **gtin** `string` (erforderlich) - **model_name** `string` (erforderlich): The name of the battery model (the product name), for headings. - **passport_id** `string`: Absent only in a preview of a passport that has no identifier yet. - **predecessor_passport_id** `string`: Second life: the passport identifier of the battery this one was repurposed, reused or remanufactured from. - **restricted_fields_present** `string[]` (erforderlich): Keys of fields with a value above this tier. Keys only, never values. - **sections** `PassportSection[]` (erforderlich) - **serial_number** `string` (erforderlich) - **tier** `string` (erforderlich, einer von: `public`, `legitimate_interest`, `authority`): The access tier a passport document is projected to; each includes the tiers before it. - **version** `PassportDocumentVersion` (erforderlich) - **public_signature** `string` (erforderlich): Ed25519 signature of the canonical public document bytes (base64url, no padding). - **registry_hash** `string` (erforderlich) - **signature** `string` (erforderlich): Ed25519 signature of the canonical snapshot bytes (base64url, no padding). - **snapshot** `JSONDocument` (erforderlich): A JSON document whose shape is described elsewhere (snapshot, BatteryPass v2.0 export). ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/passport/versions/{versionNo}" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # The passport of a unit > `GET /batteries/{batteryId}/passport` · Scope: `read` · operationId `getPassport` Any member. A unit whose identifiers are not locked and that was never published has a passport in status `draft` without `passport_id`. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The passport. - **battery_id** `string (uuid)` (erforderlich) - **current_version** `PassportVersionSummary` - **batterypass_v2_issue_count** `integer` (erforderlich) - **batterypass_v2_valid** `boolean` (erforderlich): Whether the BatteryPass v2.0 export passed its schema (advisory, ADR-012). - **canonical_hash** `string` (erforderlich): `sha256:` of the JCS-canonical snapshot (ADR-004). - **key_id** `string` (erforderlich) - **public_hash** `string` (erforderlich): `sha256:` of the JCS-canonical public document (ADR-013). - **published_at** `string (date-time)` (erforderlich) - **published_by** `string (uuid)` - **published_by_name** `string` - **reason** `string` (erforderlich) - **registry_version** `string` (erforderlich) - **valid_until** `string (date-time)`: When a later version replaced this one. - **version_no** `integer` (erforderlich) - **first_published_at** `string (date-time)` - **gs1_digital_link** `string`: `{RESOLVER_BASE_URL}/01/{gtin}/21/{serial}`; fixed once the identifiers are locked, a preview before. - **gtin** `string`: The model's GTIN-14. - **identifiers_locked_at** `string (date-time)` - **passport_id** `string`: Permanent passport identifier `urn:dpp:{uuid}`, assigned when the identifiers are locked or at the first publication. - **public_url** `string`: The public passport page, once published. - **row_version** `integer (int64)`: Optimistic-concurrency version; send it back as `If-Match` when updating. - **serial_number** `string` (erforderlich) - **status** `string` (erforderlich, einer von: `draft`, `qr_locked`, `published`, `update_needed`, `suspended`, `withdrawn`, `deleted_recycled`): Status of a unit's passport (`docs/domain-model.md` §7). - **withdrawn_at** `string (date-time)` - **withdrawn_reason** `string` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/passport" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # The passport's registration with the EU registry > `GET /batteries/{batteryId}/passport/registration` · Scope: `read` · operationId `getRegistrySubmissionStatus` Any member. The EU digital product passport registry has no interface for economic operators yet (`docs/regulatory-reference.md`), so the registration is `not_available` and nothing is submitted. A passport is never shown as registered without the registry's confirmation. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The registration status. - **attempts** `integer` - **available** `boolean` (erforderlich): Whether the platform can submit to the registry at all. - **detail** `string`: Why the registration is not available or failed (English). - **external_id** `string` - **last_attempt_at** `string (date-time)` - **passport_version** `integer`: The version the latest submission concerns. - **registry** `string` (erforderlich, einer von: `eu_dpp`) - **state** `string` (erforderlich, einer von: `not_available`, `not_submitted`, `pending`, `submitted`, `confirmed`, `failed`): `not_available`: the registry has no interface yet, nothing is submitted. The other states belong to a registry adapter that does not exist yet: `not_submitted` (available, nothing sent for this passport), `pending` (queued), `submitted` (sent, no answer), `confirmed` (the registry confirmed receipt), `failed`. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/passport/registration" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # The published versions of a unit's passport, newest first > `GET /batteries/{batteryId}/passport/versions` · Scope: `read` · operationId `listPassportVersions` Any member. With `at`, only versions published at or before that instant: the first item is the version that was valid then (history by date for the portal, with every tier). ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Query-Parameter - **limit** `integer` (Standard: 50): Maximum number of items to return. - **cursor** `string`: Opaque cursor from a previous page's `next_cursor`. - **at** `string (date-time)` ## Antworten ### 200 (`application/json`) A page of versions. - **items** `PassportVersionSummary[]` (erforderlich) - **batterypass_v2_issue_count** `integer` (erforderlich) - **batterypass_v2_valid** `boolean` (erforderlich): Whether the BatteryPass v2.0 export passed its schema (advisory, ADR-012). - **canonical_hash** `string` (erforderlich): `sha256:` of the JCS-canonical snapshot (ADR-004). - **key_id** `string` (erforderlich) - **public_hash** `string` (erforderlich): `sha256:` of the JCS-canonical public document (ADR-013). - **published_at** `string (date-time)` (erforderlich) - **published_by** `string (uuid)` - **published_by_name** `string` - **reason** `string` (erforderlich) - **registry_version** `string` (erforderlich) - **valid_until** `string (date-time)`: When a later version replaced this one. - **version_no** `integer` (erforderlich) - **next_cursor** `string | null` (erforderlich): Cursor for the next page, or null when this is the last page. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/passport/versions" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Record an event in the life of a unit > `POST /batteries/{batteryId}/lifecycle-events` · Scope: `condition` · operationId `createLifecycleEvent` Editor and above, or an API key with the `condition` scope (source `api`, ADR-017). `occurred_on` lies between the manufacturing date and today. `evidence_ids` name documents of the evidence library (a diagnostic report, a repair protocol). Events are records: they do not change the unit's data; condition values are reported with `updateCondition`, second life with `createSecondLifeBattery`. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Request-Body (`application/json`) - **description** `string` - **evidence_ids** `string (uuid)[]` - **occurred_on** `string (date)` (erforderlich) - **type** `string` (erforderlich, einer von: `placed_on_market`, `put_into_service`, `inspection`, `repair`, `component_replaced`, `condition_update`, `waste`, `recycled`, `custom`): The event types members and service partners report. ## Antworten ### 201 (`application/json`) The event. - **api_key** `ApiKeyRef`: An organisation API key (ADR-017) that reported an event (source `api`) or staged an import (ADR-026). - **id** `string (uuid)` (erforderlich) - **name** `string` (erforderlich) - **battery_id** `string (uuid)` (erforderlich) - **created_at** `string (date-time)` (erforderlich) - **description** `string` - **evidence** `EvidenceRef[]` (erforderlich) - **access_tier** `string` (erforderlich, einer von: `public`, `legitimate_interest`, `authority`, `internal`): Access tier of a field on the passport (Art. 77(2), Annex XIII). - **filename** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **kind** `string` (erforderlich, einer von: `declaration_of_conformity`, `test_report`, `dismantling_manual`, `safety_instructions`, `datasheet`, `diagnostic_report`, `study`, `image`, `other`) - **revision** `integer` (erforderlich) - **superseded** `boolean` (erforderlich) - **title** `string` (erforderlich) - **field_keys** `string[]` (erforderlich): The condition fields this event updated. - **id** `string (uuid)` (erforderlich) - **occurred_on** `string (date)` (erforderlich) - **partner** `ServicePartnerRef` - **email** `string (email)` (erforderlich) - **name** `string` - **request_id** `string (uuid)` (erforderlich) - **related_battery_id** `string (uuid)`: Second life: the successor (on the predecessor's event) or the predecessor (on the successor's). - **related_serial_number** `string` - **reported_by** `UserRef` - **display_name** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **source** `string` (erforderlich, einer von: `manual`, `supplier`, `api`, `system`): `manual` (a member), `supplier` (a service-partner link), `api` (an API key, M7), `system`. - **type** `string` (erforderlich, einer von: `placed_on_market`, `put_into_service`, `inspection`, `repair`, `component_replaced`, `condition_update`, `repurposed`, `reused`, `remanufactured`, `waste`, `recycled`, `custom`): What happened to a unit. `condition_update` is recorded by condition updates, `repurposed`, `reused` and `remanufactured` by `createSecondLifeBattery`; the others are reported. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 409 (`application/problem+json`) The request conflicts with the current state (`conflict`), e.g. a stale `If-Match` version. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X POST "https://passport.kabin.sh/api/v1/batteries/{batteryId}/lifecycle-events" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ -H "Content-Type: application/json" \ -d @body.json ``` --- # The condition data of a unit (the registry's dynamic fields) > `GET /batteries/{batteryId}/condition` · Scope: `read` · operationId `getCondition` Any member. The dynamic fields visible for the unit and their current values with `measured_at`. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The condition data. - **battery_id** `string (uuid)` (erforderlich) - **field_keys** `string[]` (erforderlich): The dynamic fields visible for the unit, in registry order. - **republish** `string` (erforderlich, einer von: `immediate`, `daily`): When accepted condition data reaches a published passport (ADR-016): `immediate` or `daily` (02:00 UTC). - **values** `FieldValue[]` (erforderlich): Current values of these fields. - **checks** `ValueCheck[]` (erforderlich) - **evidence** `EvidenceRef[]`: Documents this revision rests on (set on approved revisions and supplier submissions). - **field_key** `string` (erforderlich) - **measured_at** `string (date-time)`: When a measured value (condition data, ADR-016) was measured. - **recorded_at** `string (date-time)` (erforderlich) - **recorded_by** `UserRef` - **registry_version** `string` (erforderlich) - **responsible** `UserRef` - **review_note** `string` - **review_status** `string` (erforderlich, einer von: `captured`, `auto_checked`, `approved`, `rejected`) - **reviewed_at** `string (date-time)` - **reviewed_by** `UserRef` - **revision** `integer` (erforderlich) - **source** `string` (erforderlich, einer von: `manual`, `import`, `supplier`, `api`, `system`) - **superseded_at** `string (date-time)` - **unit** `string` - **value** `any | null` (erforderlich): The value as JSON; null when the field was cleared. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/condition" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # The lifecycle of a unit, newest first > `GET /batteries/{batteryId}/lifecycle-events` · Scope: `read` · operationId `listLifecycleEvents` Any member. Events recorded by members, service-partner reports, condition updates and second life. ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Query-Parameter - **limit** `integer` (Standard: 50): Maximum number of items to return. - **cursor** `string`: Opaque cursor from a previous page's `next_cursor`. ## Antworten ### 200 (`application/json`) A page of events. - **items** `LifecycleEvent[]` (erforderlich) - **api_key** `ApiKeyRef`: An organisation API key (ADR-017) that reported an event (source `api`) or staged an import (ADR-026). - **battery_id** `string (uuid)` (erforderlich) - **created_at** `string (date-time)` (erforderlich) - **description** `string` - **evidence** `EvidenceRef[]` (erforderlich) - **field_keys** `string[]` (erforderlich): The condition fields this event updated. - **id** `string (uuid)` (erforderlich) - **occurred_on** `string (date)` (erforderlich) - **partner** `ServicePartnerRef` - **related_battery_id** `string (uuid)`: Second life: the successor (on the predecessor's event) or the predecessor (on the successor's). - **related_serial_number** `string` - **reported_by** `UserRef` - **source** `string` (erforderlich, einer von: `manual`, `supplier`, `api`, `system`): `manual` (a member), `supplier` (a service-partner link), `api` (an API key, M7), `system`. - **type** `string` (erforderlich, einer von: `placed_on_market`, `put_into_service`, `inspection`, `repair`, `component_replaced`, `condition_update`, `repurposed`, `reused`, `remanufactured`, `waste`, `recycled`, `custom`): What happened to a unit. `condition_update` is recorded by condition updates, `repurposed`, `reused` and `remanufactured` by `createSecondLifeBattery`; the others are reported. - **next_cursor** `string | null` (erforderlich): Cursor for the next page, or null when this is the last page. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/batteries/{batteryId}/lifecycle-events" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Report measured condition data (merge) > `PATCH /batteries/{batteryId}/condition` · Scope: `condition` · operationId `updateCondition` Editor and above, or an API key with the `condition` scope (source `api`, ADR-016, ADR-017). Merge semantics: only the named fields change, `null` clears a field. Each value has a `measured_at` (its own or the request's), which must not lie in the future or before the manufacturing date. Fields that are not dynamic fields of the unit are refused (`unknown_field`); so is the whole update when a value is invalid or the merged condition data fails a cross-field check. A value older than the stored one, or equal to it, is skipped and listed. Accepted values are stored as approved measurements and recorded as a `condition_update` lifecycle event; a published passport is re-published as the organisation's `condition_republish` setting says (immediately in this request, or by the daily job). ## Pfad-Parameter - **batteryId** `string (uuid)` (erforderlich) ## Request-Body (`application/json`) - **measured_at** `string (date-time)`: The measurement time of values without their own. - **note** `string`: Becomes the description of the lifecycle event. - **values** `ConditionValueInput[]` (erforderlich) - **field_key** `string` (erforderlich) - **measured_at** `string (date-time)`: When the value was measured; defaults to the update's `measured_at`. - **value** `any | null` (erforderlich): The value as JSON; null clears the field. ## Antworten ### 200 (`application/json`) What was accepted, what was skipped, and the publication. - **accepted** `FieldValue[]` (erforderlich) - **checks** `ValueCheck[]` (erforderlich) - **evidence** `EvidenceRef[]`: Documents this revision rests on (set on approved revisions and supplier submissions). - **field_key** `string` (erforderlich) - **measured_at** `string (date-time)`: When a measured value (condition data, ADR-016) was measured. - **recorded_at** `string (date-time)` (erforderlich) - **recorded_by** `UserRef` - **registry_version** `string` (erforderlich) - **responsible** `UserRef` - **review_note** `string` - **review_status** `string` (erforderlich, einer von: `captured`, `auto_checked`, `approved`, `rejected`) - **reviewed_at** `string (date-time)` - **reviewed_by** `UserRef` - **revision** `integer` (erforderlich) - **source** `string` (erforderlich, einer von: `manual`, `import`, `supplier`, `api`, `system`) - **superseded_at** `string (date-time)` - **unit** `string` - **value** `any | null` (erforderlich): The value as JSON; null when the field was cleared. - **event** `LifecycleEvent` - **api_key** `ApiKeyRef`: An organisation API key (ADR-017) that reported an event (source `api`) or staged an import (ADR-026). - **battery_id** `string (uuid)` (erforderlich) - **created_at** `string (date-time)` (erforderlich) - **description** `string` - **evidence** `EvidenceRef[]` (erforderlich) - **field_keys** `string[]` (erforderlich): The condition fields this event updated. - **id** `string (uuid)` (erforderlich) - **occurred_on** `string (date)` (erforderlich) - **partner** `ServicePartnerRef` - **related_battery_id** `string (uuid)`: Second life: the successor (on the predecessor's event) or the predecessor (on the successor's). - **related_serial_number** `string` - **reported_by** `UserRef` - **source** `string` (erforderlich, einer von: `manual`, `supplier`, `api`, `system`): `manual` (a member), `supplier` (a service-partner link), `api` (an API key, M7), `system`. - **type** `string` (erforderlich, einer von: `placed_on_market`, `put_into_service`, `inspection`, `repair`, `component_replaced`, `condition_update`, `repurposed`, `reused`, `remanufactured`, `waste`, `recycled`, `custom`): What happened to a unit. `condition_update` is recorded by condition updates, `repurposed`, `reused` and `remanufactured` by `createSecondLifeBattery`; the others are reported. - **publication** `ConditionPublication` (erforderlich) - **blockers** `PublishBlocker[]` - **outcome** `string` (erforderlich, einer von: `published`, `unchanged`, `blocked`, `scheduled`, `not_published`): `published` / `unchanged`: published immediately (or nothing changed); `blocked`: the gate refused, the passport is `update_needed`; `scheduled`: the daily job publishes it; `not_published`: the unit has no published passport. - **version** `PassportVersionSummary` - **skipped** `ConditionSkip[]` (erforderlich) - **field_key** `string` (erforderlich) - **reason** `string` (erforderlich, einer von: `older_measurement`, `unchanged`): `older_measurement`: the stored value was measured later; `unchanged`: same value and time. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 409 (`application/problem+json`) The request conflicts with the current state (`conflict`), e.g. a stale `If-Match` version. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X PATCH "https://passport.kabin.sh/api/v1/batteries/{batteryId}/condition" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ -H "Content-Type: application/json" \ -d @body.json ``` --- # Version, content hash and status of the field registry in use > `GET /registry` · Scope: `read` · operationId `getRegistryInfo` Any signed-in user. ## Antworten ### 200 (`application/json`) The registry identity. - **hash** `string` (erforderlich): SHA-256 of the canonical registry content. - **status** `string` (erforderlich, einer von: `seed`, `verified`): `seed` until verified against primary sources (docs/registry/verification-log.md). - **version** `string` (erforderlich) ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/registry" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # The editor schema of a battery category > `GET /registry/schema` · Scope: `read` · operationId `getRegistrySchema` Any signed-in user. Sections and fields with bilingual labels, help, tier, the requirement rule for the category (before dates, conditions and values are applied; per-target status comes from the completeness operations), validation schema and export paths. ## Query-Parameter - **category** `string` (erforderlich, einer von: `ev`, `lmt`, `industrial`, `stationary`, `industrial_without_bms`): Battery category per Regulation (EU) 2023/1542; drives registry requirements. ## Antworten ### 200 (`application/json`) The schema. - **categories** `RegistryCategory[]` (erforderlich) - **key** `string` (erforderlich, einer von: `ev`, `lmt`, `industrial`, `stationary`, `industrial_without_bms`): Battery category per Regulation (EU) 2023/1542; drives registry requirements. - **label** `LabelText` (erforderlich): Text in German and English (ADR-010); clients pick the locale. - **category** `string` (erforderlich, einer von: `ev`, `lmt`, `industrial`, `stationary`, `industrial_without_bms`): Battery category per Regulation (EU) 2023/1542; drives registry requirements. - **checks** `RegistryCheck[]` (erforderlich) - **fields** `string[]` (erforderlich) - **key** `string` (erforderlich) - **level** `string` (erforderlich, einer von: `model`, `unit`) - **message** `LabelText` (erforderlich): Text in German and English (ADR-010); clients pick the locale. - **severity** `string` (erforderlich, einer von: `fail`, `warn`) - **hash** `string` (erforderlich) - **org_facts** `RegistryOrgFact[]` (erforderlich) - **key** `string` (erforderlich) - **label** `LabelText` (erforderlich): Text in German and English (ADR-010); clients pick the locale. - **type** `string` (erforderlich, einer von: `boolean`) - **sections** `RegistrySection[]` (erforderlich) - **din_spec** `string` - **fields** `RegistryField[]` (erforderlich) - **key** `string` (erforderlich) - **label** `LabelText` (erforderlich): Text in German and English (ADR-010); clients pick the locale. - **order** `integer` (erforderlich) - **status** `string` (erforderlich, einer von: `seed`, `verified`): `seed` until verified against primary sources (docs/registry/verification-log.md). - **version** `string` (erforderlich) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/registry/schema?category=…" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # The registry in use and how many of the organisation's passports need an update > `GET /registry/status` · Scope: `read` · operationId `getRegistryStatus` Any member. After a new registry version is deployed, a job re-evaluates the organisation's published passports; passports with a newly missing required value become `update_needed` (they stay online) and their versions get an `update_passports` task. ## Antworten ### 200 (`application/json`) The status. - **reevaluation** `Job`: `created_by` names the member who started the job, `created_by_api_key` the API key that started an import validation (ADR-026); a job without either was started by the system (the registry re-evaluation). - **created_at** `string (date-time)` (erforderlich) - **created_by** `UserRef` - **created_by_api_key** `ApiKeyRef`: An organisation API key (ADR-017) that reported an event (source `api`) or staged an import (ADR-026). - **error_code** `string` (einer von: `forbidden`, `not_found`, `conflict`, `invalid`, `internal_error`): Why a job failed (`forbidden`: the member who started it lost the role or left). - **file** `JobFile` - **finished_at** `string (date-time)` - **id** `string (uuid)` (erforderlich) - **import_id** `string (uuid)` - **kind** `string` (erforderlich, einer von: `import_validate`, `import_commit`, `import_approve`, `bulk_publish`, `qr_export`, `registry_reevaluate`, `org_export`) - **progress** `JobProgress` (erforderlich) - **result** `JobResult`: The summary of a finished job; which members are set depends on the kind. - **started_at** `string (date-time)` - **status** `string` (erforderlich, einer von: `queued`, `running`, `succeeded`, `failed`) - **registry_hash** `string` (erforderlich) - **registry_status** `string` (erforderlich, einer von: `seed`, `verified`): `seed` until verified against primary sources (docs/registry/verification-log.md). - **registry_version** `string` (erforderlich) - **update_needed** `integer` (erforderlich): The organisation's passports in status `update_needed`. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/registry/status" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Manufacturing facilities of the active organisation > `GET /facilities` · Scope: `read` · operationId `listFacilities` Any member. Ordered by name, then id. ## Query-Parameter - **limit** `integer` (Standard: 50): Maximum number of items to return. - **cursor** `string`: Opaque cursor from a previous page's `next_cursor`. ## Antworten ### 200 (`application/json`) One page of facilities. - **items** `Facility[]` (erforderlich) - **address** `Address` (erforderlich): A postal address (ADR-022). `street` is the first address line, street and house number as written in the country; `extra_line` the second (building, floor, c/o). `region` is the administrative area where the country's format has one (`getAddressFormats`; required for IT and ES). Postal codes are checked against the country's pattern. `place_id` and `confirmed_source` say how the address was entered; they are never part of signed or public passports. - **created_at** `string (date-time)` (erforderlich) - **gln** `string`: GS1 Global Location Number, 13 digits with a valid check digit. - **id** `string (uuid)` (erforderlich) - **name** `string` (erforderlich) - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **updated_at** `string (date-time)` (erforderlich) - **next_cursor** `string | null` (erforderlich): Cursor for the next page, or null when this is the last page. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/facilities" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # A public document of a public passport > `GET /public/passports/{passportId}/documents/{documentId}` · öffentlich, ohne Schlüssel · operationId `downloadPublicPassportDocument` No authentication. Only documents of the `public` tier that a public field of a published version names (for example the EU declaration of conformity); everything else is `404`. ## Pfad-Parameter - **passportId** `string (uuid)` (erforderlich): The UUID of the passport identifier `urn:dpp:{uuid}`. - **documentId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`*/*`) The file. `any` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 429 (`application/problem+json`) Too many attempts (`rate_limited`); retry after the number of seconds in the `Retry-After` header. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/public/passports/{passportId}/documents/{documentId}" ``` --- # The public passport of a unit, looked up by GTIN and serial number > `GET /public/dpps/by-product-id/{productId}` · öffentlich, ohne Schlüssel · operationId `getPublicPassportByProductId` No authentication. Same response as `getPublicPassport`. ## Pfad-Parameter - **productId** `string` (erforderlich): `{gtin}:{serial}` (EN 18222 style), e.g. `04012345000023:SV2027-000184`; the GTIN may have 8, 12, 13 or 14 digits. ## Antworten ### 200 (`application/json`) The passport. - **document** `PassportDocument`: A passport version projected to an access tier. The `public` projection is stored and signed at publication (ADR-013); verify it by canonicalising this object with RFC 8785 (JCS), hashing it with SHA-256 and checking the Ed25519 signature. - **category** `PassportCategory` (erforderlich) - **economic_operator** `PublicProfile`: The economic operator as shown on public passports. Every property is optional; publishing (M4) requires a complete profile. - **gs1_digital_link** `string` (erforderlich) - **gtin** `string` (erforderlich) - **model_name** `string` (erforderlich): The name of the battery model (the product name), for headings. - **passport_id** `string`: Absent only in a preview of a passport that has no identifier yet. - **predecessor_passport_id** `string`: Second life: the passport identifier of the battery this one was repurposed, reused or remanufactured from. - **restricted_fields_present** `string[]` (erforderlich): Keys of fields with a value above this tier. Keys only, never values. - **sections** `PassportSection[]` (erforderlich) - **serial_number** `string` (erforderlich) - **tier** `string` (erforderlich, einer von: `public`, `legitimate_interest`, `authority`): The access tier a passport document is projected to; each includes the tiers before it. - **version** `PassportDocumentVersion` (erforderlich) - **links** `PublicPassportLinks` (erforderlich) - **gs1_digital_link** `string` - **json** `string` (erforderlich) - **keys** `string` (erforderlich) - **page** `string` (erforderlich) - **predecessor** `string`: Second life: the public JSON of the passport of the battery this one was made from. - **restricted** `string`: The page where authorised parties sign in to read restricted tiers; only while the portal is enabled (ADR-018). - **successors** `string[]`: Second life: the public JSON of the passports of batteries made from this one. - **passport_id** `string` (erforderlich) - **status** `string` (erforderlich, einer von: `draft`, `qr_locked`, `published`, `update_needed`, `suspended`, `withdrawn`, `deleted_recycled`): Status of a unit's passport (`docs/domain-model.md` §7). - **valid_until** `string (date-time)`: Set when this version was replaced by a later one (history by date). - **verification** `PassportSignature` - **algorithm** `string` (erforderlich, einer von: `Ed25519`) - **canonicalization** `string` (erforderlich, einer von: `RFC8785`) - **hash** `string` (erforderlich): `sha256:` of the canonical `document`. - **key_id** `string` (erforderlich) - **keys_url** `string` (erforderlich) - **signature** `string` (erforderlich): Ed25519 signature of the canonical `document` bytes (base64url, no padding). - **signed_at** `string (date-time)` (erforderlich) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 429 (`application/problem+json`) Too many attempts (`rate_limited`); retry after the number of seconds in the `Retry-After` header. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/public/dpps/by-product-id/{productId}" ``` --- # The public passport as it was at a point in time > `GET /public/passports/{passportId}/version` · öffentlich, ohne Schlüssel · operationId `getPublicPassportVersion` No authentication. The version with `published_at ≤ at < valid_until`; `404` before the first publication. Versions are immutable, so the response is cached for a day. ## Pfad-Parameter - **passportId** `string (uuid)` (erforderlich): The UUID of the passport identifier `urn:dpp:{uuid}`. ## Query-Parameter - **at** `string (date-time)` (erforderlich) ## Antworten ### 200 (`application/json`) The version. - **document** `PassportDocument`: A passport version projected to an access tier. The `public` projection is stored and signed at publication (ADR-013); verify it by canonicalising this object with RFC 8785 (JCS), hashing it with SHA-256 and checking the Ed25519 signature. - **category** `PassportCategory` (erforderlich) - **economic_operator** `PublicProfile`: The economic operator as shown on public passports. Every property is optional; publishing (M4) requires a complete profile. - **gs1_digital_link** `string` (erforderlich) - **gtin** `string` (erforderlich) - **model_name** `string` (erforderlich): The name of the battery model (the product name), for headings. - **passport_id** `string`: Absent only in a preview of a passport that has no identifier yet. - **predecessor_passport_id** `string`: Second life: the passport identifier of the battery this one was repurposed, reused or remanufactured from. - **restricted_fields_present** `string[]` (erforderlich): Keys of fields with a value above this tier. Keys only, never values. - **sections** `PassportSection[]` (erforderlich) - **serial_number** `string` (erforderlich) - **tier** `string` (erforderlich, einer von: `public`, `legitimate_interest`, `authority`): The access tier a passport document is projected to; each includes the tiers before it. - **version** `PassportDocumentVersion` (erforderlich) - **links** `PublicPassportLinks` (erforderlich) - **gs1_digital_link** `string` - **json** `string` (erforderlich) - **keys** `string` (erforderlich) - **page** `string` (erforderlich) - **predecessor** `string`: Second life: the public JSON of the passport of the battery this one was made from. - **restricted** `string`: The page where authorised parties sign in to read restricted tiers; only while the portal is enabled (ADR-018). - **successors** `string[]`: Second life: the public JSON of the passports of batteries made from this one. - **passport_id** `string` (erforderlich) - **status** `string` (erforderlich, einer von: `draft`, `qr_locked`, `published`, `update_needed`, `suspended`, `withdrawn`, `deleted_recycled`): Status of a unit's passport (`docs/domain-model.md` §7). - **valid_until** `string (date-time)`: Set when this version was replaced by a later one (history by date). - **verification** `PassportSignature` - **algorithm** `string` (erforderlich, einer von: `Ed25519`) - **canonicalization** `string` (erforderlich, einer von: `RFC8785`) - **hash** `string` (erforderlich): `sha256:` of the canonical `document`. - **key_id** `string` (erforderlich) - **keys_url** `string` (erforderlich) - **signature** `string` (erforderlich): Ed25519 signature of the canonical `document` bytes (base64url, no padding). - **signed_at** `string (date-time)` (erforderlich) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 429 (`application/problem+json`) Too many attempts (`rate_limited`); retry after the number of seconds in the `Retry-After` header. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/public/passports/{passportId}/version?at=…" ``` --- # The public passport (current version) > `GET /public/passports/{passportId}` · öffentlich, ohne Schlüssel · operationId `getPublicPassport` No authentication. Only the `public` tier, as stored when the version was published (ADR-013). Passports that are not public (`qr_locked`, `withdrawn`, `suspended`, `deleted_recycled`) answer with their status and no content. Cached for 5 minutes; rate-limited per client address. ## Pfad-Parameter - **passportId** `string (uuid)` (erforderlich): The UUID of the passport identifier `urn:dpp:{uuid}`. ## Antworten ### 200 (`application/json`) The passport. - **document** `PassportDocument`: A passport version projected to an access tier. The `public` projection is stored and signed at publication (ADR-013); verify it by canonicalising this object with RFC 8785 (JCS), hashing it with SHA-256 and checking the Ed25519 signature. - **category** `PassportCategory` (erforderlich) - **economic_operator** `PublicProfile`: The economic operator as shown on public passports. Every property is optional; publishing (M4) requires a complete profile. - **gs1_digital_link** `string` (erforderlich) - **gtin** `string` (erforderlich) - **model_name** `string` (erforderlich): The name of the battery model (the product name), for headings. - **passport_id** `string`: Absent only in a preview of a passport that has no identifier yet. - **predecessor_passport_id** `string`: Second life: the passport identifier of the battery this one was repurposed, reused or remanufactured from. - **restricted_fields_present** `string[]` (erforderlich): Keys of fields with a value above this tier. Keys only, never values. - **sections** `PassportSection[]` (erforderlich) - **serial_number** `string` (erforderlich) - **tier** `string` (erforderlich, einer von: `public`, `legitimate_interest`, `authority`): The access tier a passport document is projected to; each includes the tiers before it. - **version** `PassportDocumentVersion` (erforderlich) - **links** `PublicPassportLinks` (erforderlich) - **gs1_digital_link** `string` - **json** `string` (erforderlich) - **keys** `string` (erforderlich) - **page** `string` (erforderlich) - **predecessor** `string`: Second life: the public JSON of the passport of the battery this one was made from. - **restricted** `string`: The page where authorised parties sign in to read restricted tiers; only while the portal is enabled (ADR-018). - **successors** `string[]`: Second life: the public JSON of the passports of batteries made from this one. - **passport_id** `string` (erforderlich) - **status** `string` (erforderlich, einer von: `draft`, `qr_locked`, `published`, `update_needed`, `suspended`, `withdrawn`, `deleted_recycled`): Status of a unit's passport (`docs/domain-model.md` §7). - **valid_until** `string (date-time)`: Set when this version was replaced by a later one (history by date). - **verification** `PassportSignature` - **algorithm** `string` (erforderlich, einer von: `Ed25519`) - **canonicalization** `string` (erforderlich, einer von: `RFC8785`) - **hash** `string` (erforderlich): `sha256:` of the canonical `document`. - **key_id** `string` (erforderlich) - **keys_url** `string` (erforderlich) - **signature** `string` (erforderlich): Ed25519 signature of the canonical `document` bytes (base64url, no padding). - **signed_at** `string (date-time)` (erforderlich) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 429 (`application/problem+json`) Too many attempts (`rate_limited`); retry after the number of seconds in the `Retry-After` header. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/public/passports/{passportId}" ``` --- # Check the signatures of a version on the server > `GET /public/passports/{passportId}/verify` · öffentlich, ohne Schlüssel · operationId `verifyPassport` No authentication. A convenience: the same check works offline with the public JSON and `/.well-known/batterypass/keys.json` (ADR-004, ADR-013). Without `version_no`, the current version. ## Pfad-Parameter - **passportId** `string (uuid)` (erforderlich): The UUID of the passport identifier `urn:dpp:{uuid}`. ## Query-Parameter - **version_no** `integer` ## Antworten ### 200 (`application/json`) The result. - **checked_at** `string (date-time)` (erforderlich) - **key_id** `string` (erforderlich) - **public_document_valid** `boolean` (erforderlich) - **signed_at** `string (date-time)` (erforderlich) - **snapshot_valid** `boolean` (erforderlich) - **valid** `boolean` (erforderlich): Both signatures verify and both hashes match the stored documents. - **version_no** `integer` (erforderlich) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 429 (`application/problem+json`) Too many attempts (`rate_limited`); retry after the number of seconds in the `Retry-After` header. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/public/passports/{passportId}/verify" ``` --- # The organisation's changes, oldest first, for polling > `GET /changes` · Scope: `read` · operationId `listChanges` Any member, or an API key with the `read` scope (ADR-026). The organisation's changes in the order they became visible, oldest first, as `{id, occurred_at, entity_type, entity_id, action}`: no values, no actors, no personal data; read the entity for its current state. Keep `next_cursor` of the last page and ask again later: a change appears only once every transaction that started before it has finished, so a cursor never skips a change that commits late. `next_cursor` is set whenever the page has items, also on the last page; an empty page keeps the cursor you sent (`next_cursor` null). Without `cursor` the feed starts at the beginning of the organisation's history. ## Query-Parameter - **limit** `integer` (Standard: 50): Maximum number of items to return. - **cursor** `string`: Opaque cursor from a previous page's `next_cursor`. - **types** `string[]`: Only these actions (all when omitted). ## Antworten ### 200 (`application/json`) One page of changes. - **items** `Change[]` (erforderlich) - **action** `string` (erforderlich, einer von: `battery.created`, `passport.published`, `passport.republished`, `passport.withdrawn`, `model_version.approved`, `import.committed`, `import.approved`, `lifecycle_event.recorded`, `condition.updated`): What changed (ADR-026): `battery.created` (a unit was created), `passport.published` (a unit's first passport version), `passport.republished` (a later version), `passport.withdrawn`, `model_version.approved`, `import.committed` (its units exist), `import.approved` (its values were approved), `lifecycle_event.recorded` (an event other than a condition update), `condition.updated` (measured condition data was accepted). - **entity_id** `string (uuid)` (erforderlich) - **entity_type** `string` (erforderlich, einer von: `battery`, `model_version`, `import`): The resource to read after a change: `battery` (`getBattery`, `getPassport`, `listLifecycleEvents`, `getCondition`), `model_version` (`getModelVersion`) or `import` (`getImport`). Passport changes name the unit. - **id** `string (uuid)` (erforderlich) - **occurred_at** `string (date-time)` (erforderlich) - **next_cursor** `string | null` (erforderlich): Cursor for the next page, or null when this is the last page. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/changes" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Upload a spreadsheet of units > `POST /imports` · Scope: `import` · operationId `createImport` Editor and above, or an API key with the `import` scope (ADR-026). A CSV (UTF-8, UTF-16 or Windows-1252; `;`, `,`, tab or `|` separated) or XLSX file of at most 10 MB, 10 000 rows and 200 columns; the first non-empty row holds the headers. The answer is the import with a mapping proposed from the headers (registry CSV headers, aliases, labels; units such as `(kWh)` are converted), or taken from the import preset named in `preset` (the ERP registry): a preset maps the columns it knows by their headers and leaves the others unmapped. A key stages the import only: members of the organisation commit it and approve its values in the portal, so declared data still enters through a member's decision. ## Request-Body (`multipart/form-data`) - **file** `any` (erforderlich) - **preset** `string`: An import preset of the ERP registry (`getErpRegistry`, ADR-026): which column header of an ERP export maps to which registry field. - **sheet** `string`: The XLSX worksheet to read (the first one when omitted). ## Antworten ### 201 (`application/json`) The import. - **approved_at** `string (date-time)` - **approved_by** `string (uuid)` - **committed_at** `string (date-time)` - **created_at** `string (date-time)` (erforderlich) - **created_by** `UserRef` - **display_name** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **created_by_api_key** `ApiKeyRef`: An organisation API key (ADR-017) that reported an event (source `api`) or staged an import (ADR-026). - **id** `string (uuid)` (erforderlich) - **name** `string` (erforderlich) - **filename** `string` (erforderlich) - **format** `string` (erforderlich, einer von: `csv`, `xlsx`) - **headers** `string[]` (erforderlich) - **id** `string (uuid)` (erforderlich) - **mapping** `ColumnMapping[]` (erforderlich) - **auto** `boolean`: Proposed by the server from the header. - **column** `integer` (erforderlich): The 0-based column of the file. - **header** `string`: The column's header (set by the server). - **target** `string`: A target of `listImportTargets`: a field key, `key[n].property` (list item n, 1-based), `key.property` (object property) or `model_version`. Empty or missing: the column is ignored. - **unit** `string`: The unit the column's numbers are written in when it differs from the field's (e.g. `kWh` for a field in Wh). - **options** `ImportOptions` (erforderlich) - **battery_model_id** `string (uuid)`: The model of rows without a model column. - **facility_id** `string (uuid)`: The place of manufacture of rows without one. - **preset** `string`: An import preset of the ERP registry (`getErpRegistry`, ADR-026): which column header of an ERP export maps to which registry field. - **row_count** `integer` (erforderlich): Data rows (empty rows are left out). - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **sample_rows** `string[][]` (erforderlich): The first data rows of the file as read (at most 5), to show example values while mapping. - **sheet** `string`: The XLSX worksheet that was read. - **size_bytes** `integer (int64)` (erforderlich) - **status** `string` (erforderlich, einer von: `uploaded`, `validating`, `validated`, `committing`, `committed`, `failed`): `uploaded` (mapping can change) → `validating` → `validated` → `committing` → `committed`; `failed` when the file could not be validated (validate again). A changed mapping sends a validated import back to `uploaded`. - **summary** `ImportSummary` - **approved** `integer` (erforderlich): Imported values approved by import approvals. - **created** `integer` (erforderlich): Units the commit created. - **duplicates** `integer` (erforderlich): Rows whose serial number repeats an earlier row or an existing unit; not imported. - **errors** `integer` (erforderlich): Rows with errors; not imported. - **not_approved** `integer` (erforderlich): Imported values the last approval left for individual review. - **rows** `integer` (erforderlich) - **valid** `integer` (erforderlich): Rows without messages. - **warnings** `integer` (erforderlich): Rows with warnings only; they are imported. - **updated_at** `string (date-time)` (erforderlich) - **validated_at** `string (date-time)` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 413 (`application/problem+json`) The request body exceeds the size limit (`payload_too_large`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X POST "https://passport.kabin.sh/api/v1/imports" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # One import with its mapping and summary > `GET /imports/{importId}` · Scope: `import` · operationId `getImport` Any member, or an API key with the `import` scope (ADR-026). Poll it after `validateImport`: the status becomes `validated` (or `failed`) and `summary` counts valid, warning, error and duplicate rows. ## Pfad-Parameter - **importId** `string (uuid)` (erforderlich) ## Antworten ### 200 (`application/json`) The import. - **approved_at** `string (date-time)` - **approved_by** `string (uuid)` - **committed_at** `string (date-time)` - **created_at** `string (date-time)` (erforderlich) - **created_by** `UserRef` - **display_name** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **created_by_api_key** `ApiKeyRef`: An organisation API key (ADR-017) that reported an event (source `api`) or staged an import (ADR-026). - **id** `string (uuid)` (erforderlich) - **name** `string` (erforderlich) - **filename** `string` (erforderlich) - **format** `string` (erforderlich, einer von: `csv`, `xlsx`) - **headers** `string[]` (erforderlich) - **id** `string (uuid)` (erforderlich) - **mapping** `ColumnMapping[]` (erforderlich) - **auto** `boolean`: Proposed by the server from the header. - **column** `integer` (erforderlich): The 0-based column of the file. - **header** `string`: The column's header (set by the server). - **target** `string`: A target of `listImportTargets`: a field key, `key[n].property` (list item n, 1-based), `key.property` (object property) or `model_version`. Empty or missing: the column is ignored. - **unit** `string`: The unit the column's numbers are written in when it differs from the field's (e.g. `kWh` for a field in Wh). - **options** `ImportOptions` (erforderlich) - **battery_model_id** `string (uuid)`: The model of rows without a model column. - **facility_id** `string (uuid)`: The place of manufacture of rows without one. - **preset** `string`: An import preset of the ERP registry (`getErpRegistry`, ADR-026): which column header of an ERP export maps to which registry field. - **row_count** `integer` (erforderlich): Data rows (empty rows are left out). - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **sample_rows** `string[][]` (erforderlich): The first data rows of the file as read (at most 5), to show example values while mapping. - **sheet** `string`: The XLSX worksheet that was read. - **size_bytes** `integer (int64)` (erforderlich) - **status** `string` (erforderlich, einer von: `uploaded`, `validating`, `validated`, `committing`, `committed`, `failed`): `uploaded` (mapping can change) → `validating` → `validated` → `committing` → `committed`; `failed` when the file could not be validated (validate again). A changed mapping sends a validated import back to `uploaded`. - **summary** `ImportSummary` - **approved** `integer` (erforderlich): Imported values approved by import approvals. - **created** `integer` (erforderlich): Units the commit created. - **duplicates** `integer` (erforderlich): Rows whose serial number repeats an earlier row or an existing unit; not imported. - **errors** `integer` (erforderlich): Rows with errors; not imported. - **not_approved** `integer` (erforderlich): Imported values the last approval left for individual review. - **rows** `integer` (erforderlich) - **valid** `integer` (erforderlich): Rows without messages. - **warnings** `integer` (erforderlich): Rows with warnings only; they are imported. - **updated_at** `string (date-time)` (erforderlich) - **validated_at** `string (date-time)` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/imports/{importId}" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Imports of the active organisation, newest first > `GET /imports` · Scope: `import` · operationId `listImports` Any member, or an API key with the `import` scope (ADR-026). An import staged with a key names the key in `created_by_api_key` instead of `created_by`. ## Query-Parameter - **limit** `integer` (Standard: 50): Maximum number of items to return. - **cursor** `string`: Opaque cursor from a previous page's `next_cursor`. ## Antworten ### 200 (`application/json`) One page of imports. - **items** `Import[]` (erforderlich) - **approved_at** `string (date-time)` - **approved_by** `string (uuid)` - **committed_at** `string (date-time)` - **created_at** `string (date-time)` (erforderlich) - **created_by** `UserRef` - **created_by_api_key** `ApiKeyRef`: An organisation API key (ADR-017) that reported an event (source `api`) or staged an import (ADR-026). - **filename** `string` (erforderlich) - **format** `string` (erforderlich, einer von: `csv`, `xlsx`) - **headers** `string[]` (erforderlich) - **id** `string (uuid)` (erforderlich) - **mapping** `ColumnMapping[]` (erforderlich) - **options** `ImportOptions` (erforderlich) - **preset** `string`: An import preset of the ERP registry (`getErpRegistry`, ADR-026): which column header of an ERP export maps to which registry field. - **row_count** `integer` (erforderlich): Data rows (empty rows are left out). - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **sample_rows** `string[][]` (erforderlich): The first data rows of the file as read (at most 5), to show example values while mapping. - **sheet** `string`: The XLSX worksheet that was read. - **size_bytes** `integer (int64)` (erforderlich) - **status** `string` (erforderlich, einer von: `uploaded`, `validating`, `validated`, `committing`, `committed`, `failed`): `uploaded` (mapping can change) → `validating` → `validated` → `committing` → `committed`; `failed` when the file could not be validated (validate again). A changed mapping sends a validated import back to `uploaded`. - **summary** `ImportSummary` - **updated_at** `string (date-time)` (erforderlich) - **validated_at** `string (date-time)` - **next_cursor** `string | null` (erforderlich): Cursor for the next page, or null when this is the last page. ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X GET "https://passport.kabin.sh/api/v1/imports" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Map the columns of an import > `PUT /imports/{importId}/mapping` · Scope: `import` · operationId `updateImportMapping` Editor and above, or an API key with the `import` scope (ADR-026), while the import is not being processed or committed. Each target at most once; `unit` is the unit a column's numbers are written in when it differs from the field's. With `preset` (an import preset of the ERP registry) the mapping is taken from the preset by the file's headers and `mapping` must be empty. A validated import must be validated again. ## Pfad-Parameter - **importId** `string (uuid)` (erforderlich) ## Header - **If-Match** `string` (erforderlich): The `row_version` the client last read, as a quoted string, e.g. `"3"`. ## Request-Body (`application/json`) - **mapping** `ColumnMapping[]` - **auto** `boolean`: Proposed by the server from the header. - **column** `integer` (erforderlich): The 0-based column of the file. - **header** `string`: The column's header (set by the server). - **target** `string`: A target of `listImportTargets`: a field key, `key[n].property` (list item n, 1-based), `key.property` (object property) or `model_version`. Empty or missing: the column is ignored. - **unit** `string`: The unit the column's numbers are written in when it differs from the field's (e.g. `kWh` for a field in Wh). - **options** `ImportOptions` - **battery_model_id** `string (uuid)`: The model of rows without a model column. - **facility_id** `string (uuid)`: The place of manufacture of rows without one. - **preset** `string`: An import preset of the ERP registry (`getErpRegistry`, ADR-026): which column header of an ERP export maps to which registry field. ## Antworten ### 200 (`application/json`) The import. - **approved_at** `string (date-time)` - **approved_by** `string (uuid)` - **committed_at** `string (date-time)` - **created_at** `string (date-time)` (erforderlich) - **created_by** `UserRef` - **display_name** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **created_by_api_key** `ApiKeyRef`: An organisation API key (ADR-017) that reported an event (source `api`) or staged an import (ADR-026). - **id** `string (uuid)` (erforderlich) - **name** `string` (erforderlich) - **filename** `string` (erforderlich) - **format** `string` (erforderlich, einer von: `csv`, `xlsx`) - **headers** `string[]` (erforderlich) - **id** `string (uuid)` (erforderlich) - **mapping** `ColumnMapping[]` (erforderlich) - **auto** `boolean`: Proposed by the server from the header. - **column** `integer` (erforderlich): The 0-based column of the file. - **header** `string`: The column's header (set by the server). - **target** `string`: A target of `listImportTargets`: a field key, `key[n].property` (list item n, 1-based), `key.property` (object property) or `model_version`. Empty or missing: the column is ignored. - **unit** `string`: The unit the column's numbers are written in when it differs from the field's (e.g. `kWh` for a field in Wh). - **options** `ImportOptions` (erforderlich) - **battery_model_id** `string (uuid)`: The model of rows without a model column. - **facility_id** `string (uuid)`: The place of manufacture of rows without one. - **preset** `string`: An import preset of the ERP registry (`getErpRegistry`, ADR-026): which column header of an ERP export maps to which registry field. - **row_count** `integer` (erforderlich): Data rows (empty rows are left out). - **row_version** `integer (int64)` (erforderlich): Optimistic-concurrency version; send it back as `If-Match` when updating. - **sample_rows** `string[][]` (erforderlich): The first data rows of the file as read (at most 5), to show example values while mapping. - **sheet** `string`: The XLSX worksheet that was read. - **size_bytes** `integer (int64)` (erforderlich) - **status** `string` (erforderlich, einer von: `uploaded`, `validating`, `validated`, `committing`, `committed`, `failed`): `uploaded` (mapping can change) → `validating` → `validated` → `committing` → `committed`; `failed` when the file could not be validated (validate again). A changed mapping sends a validated import back to `uploaded`. - **summary** `ImportSummary` - **approved** `integer` (erforderlich): Imported values approved by import approvals. - **created** `integer` (erforderlich): Units the commit created. - **duplicates** `integer` (erforderlich): Rows whose serial number repeats an earlier row or an existing unit; not imported. - **errors** `integer` (erforderlich): Rows with errors; not imported. - **not_approved** `integer` (erforderlich): Imported values the last approval left for individual review. - **rows** `integer` (erforderlich) - **valid** `integer` (erforderlich): Rows without messages. - **warnings** `integer` (erforderlich): Rows with warnings only; they are imported. - **updated_at** `string (date-time)` (erforderlich) - **validated_at** `string (date-time)` ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 409 (`application/problem+json`) The request conflicts with the current state (`conflict`), e.g. a stale `If-Match` version. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X PUT "https://passport.kabin.sh/api/v1/imports/{importId}/mapping" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" \ -H "Content-Type: application/json" \ -d @body.json ``` --- # Start validating every row of an import > `POST /imports/{importId}/validate` · Scope: `import` · operationId `validateImport` Editor and above, or an API key with the `import` scope (ADR-026). A job reads every row with the mapping, validates it with the editor's validators and finds duplicate serial numbers in the file and in the database (after removing surrounding spaces and invisible characters). Requires a serial number column and a model column or `options.battery_model_id`. Follow the import with `getImport` until its status is `validated` or `failed`. ## Pfad-Parameter - **importId** `string (uuid)` (erforderlich) ## Antworten ### 202 (`application/json`) The validation job. - **created_at** `string (date-time)` (erforderlich) - **created_by** `UserRef` - **display_name** `string` (erforderlich) - **id** `string (uuid)` (erforderlich) - **created_by_api_key** `ApiKeyRef`: An organisation API key (ADR-017) that reported an event (source `api`) or staged an import (ADR-026). - **id** `string (uuid)` (erforderlich) - **name** `string` (erforderlich) - **error_code** `string` (einer von: `forbidden`, `not_found`, `conflict`, `invalid`, `internal_error`): Why a job failed (`forbidden`: the member who started it lost the role or left). - **file** `JobFile` - **content_type** `string` (erforderlich) - **expires_at** `string (date-time)` (erforderlich) - **filename** `string` (erforderlich) - **size** `integer (int64)` (erforderlich) - **finished_at** `string (date-time)` - **id** `string (uuid)` (erforderlich) - **import_id** `string (uuid)` - **kind** `string` (erforderlich, einer von: `import_validate`, `import_commit`, `import_approve`, `bulk_publish`, `qr_export`, `registry_reevaluate`, `org_export`) - **progress** `JobProgress` (erforderlich) - **done** `integer` (erforderlich) - **total** `integer` (erforderlich) - **result** `JobResult`: The summary of a finished job; which members are set depends on the kind. - **approval** `ImportApprovalSummary` - **detail** `string`: Why a failed job failed (English). - **export** `ExportSummary`: What a full organisation export contains. - **import_summary** `ImportSummary` - **outcomes** `JobOutcomeCounts` - **reevaluation** `ReevaluationSummary` - **units** `integer` - **started_at** `string (date-time)` - **status** `string` (erforderlich, einer von: `queued`, `running`, `succeeded`, `failed`) ### 400 (`application/problem+json`) The request is malformed or failed validation (`validation_error`), or a token is invalid or expired (`invalid_token`). ### 401 (`application/problem+json`) The API key is missing, unknown or revoked (`unauthenticated`). ### 403 (`application/problem+json`) The API key may not perform this action (`forbidden`): it lacks the operation's scope, or the action needs more than a viewer's rights. ### 404 (`application/problem+json`) The resource does not exist in the organisation of the API key, or the public passport is unknown (`not_found`). ### 409 (`application/problem+json`) The request conflicts with the current state (`conflict`), e.g. a stale `If-Match` version. ### default (`application/problem+json`) Any other problem, e.g. `internal_error`. ## Beispiel ```bash title="cURL" curl -X POST "https://passport.kabin.sh/api/v1/imports/{importId}/validate" \ -H "Authorization: Bearer $BATTERYPASS_API_KEY" ``` --- # Änderungen > Was sich an der Integrations-API und an dieser Dokumentation geändert hat. Neue Einträge stehen oben. Den Verlauf gibt es auch als [RSS-Feed](https://passport.kabin.sh/docs/de/changelog/rss.xml). ## 0.10.0 _24.10.2026 · API · Vertrag · Integrationen_ Integrationen können jetzt Einheiten einbringen und Änderungen verfolgen, ohne alles abzusuchen. - Ein dritter Schlüssel-Scope, `import`: Ein Schlüssel damit lädt Tabellenimporte von Einheiten hoch, ordnet sie zu und lässt sie prüfen ([`createImport`](https://passport.kabin.sh/docs/de/api-reference/imports/create-import), [`getImport`](https://passport.kabin.sh/docs/de/api-reference/imports/get-import), [`listImports`](https://passport.kabin.sh/docs/de/api-reference/imports/list-imports), [`updateImportMapping`](https://passport.kabin.sh/docs/de/api-reference/imports/update-import-mapping), [`validateImport`](https://passport.kabin.sh/docs/de/api-reference/imports/validate-import)). Das Übernehmen des Imports und das Freigeben seiner Werte bleiben bei Mitgliedern im Portal. Ein mit Schlüssel bereitgestellter Import nennt ihn in `created_by_api_key`; `created_by` fehlt dann. - Importvorlagen: `preset` bei `createImport` und `updateImportMapping` ordnet die Spalten eines bekannten ERP-Exports über ihre Spaltennamen zu. - Der Änderungsfeed [`listChanges`](https://passport.kabin.sh/docs/de/api-reference/changes/list-changes) (Scope `read`): die Änderungen der Organisation, älteste zuerst, ohne Werte, Handelnde oder personenbezogene Daten, sicher mit gespeichertem Cursor abzufragen. - Neue [Integrationsanleitungen](https://passport.kabin.sh/docs/de/integrations): das Sicherheitsmodell, fünf Abläufe, iPaaS, Standards und eine Seite je ERP-System aus dem ERP-Register, mit Angabe, wie und wann jede Schnittstelle geprüft wurde. ## Entwicklerdokumentation _24.10.2026 · Dokumentation_ Die Entwicklerdokumentation zieht unter `/docs/` um und ersetzt die bisherige Seite `/api-docs` (die auf die API-Referenz weiterleitet). - Erste Schritte, Konzepte, Anleitungen und Rezepte auf Deutsch und Englisch. - Die API-Referenz wird aus dem veröffentlichten Vertrag erzeugt, mit Playground, Codebeispielen in cURL, JavaScript, Python und Go und dem Vertrag zum Herunterladen ([JSON](https://passport.kabin.sh/docs/openapi.json), [YAML](https://passport.kabin.sh/docs/openapi.yaml)). - Jede Seite gibt es als Markdown (die Adresse der Seite plus `.md`), dazu [`llms.txt`](https://passport.kabin.sh/docs/llms.txt) und [`llms-full.txt`](https://passport.kabin.sh/docs/llms-full.txt) für Sprachmodelle. ## 0.9.0 _24.10.2026 · API · Vertrag_ Adressen folgen dem Format ihres europäischen Landes. Alle Änderungen sind kompatibel. - `Address` erhält `extra_line` (die zweite Adresszeile: Gebäude, Etage, c/o) und `region` (die Verwaltungseinheit, wo das Adressformat des Landes eine hat). Beide sind optional und erscheinen in den Standorten von [`listFacilities`](https://passport.kabin.sh/docs/de/api-reference/facilities/list-facilities) und im öffentlichen Profil des Wirtschaftsakteurs in Pässen. - `listFacilities` liefert außerdem `place_id` und `confirmed_source`: wie ein Mitglied die Adresse eingegeben hat (von Hand, aus der Adresssuche oder durch die Adressprüfung bestätigt). Beide stehen nie in signierten Snapshots oder öffentlichen Pässen. - Der neue Problem-Code `address_lookup_unavailable` (`503`) gehört zur Adresssuche des Portals, die keine API-Schlüssel annimmt. ## 0.8.0 _24.10.2026 · API · Vertrag_ Veröffentlicht wird nur noch der Teil des Vertrags, den Integrationen nutzen können: die 32 Operationen, die einen API-Schlüssel annehmen, und die 5 öffentlichen Pass-Abfragen. Anmeldung, Sitzungen, Schlüsselverwaltung und alle anderen Operationen des Portals stehen nicht mehr in `/api/v1/openapi.json` und `.yaml`; sie können sich ohne Ankündigung ändern. - API-Schlüssel der Organisation (`Authorization: Bearer bp_live_…`) mit den Scopes `read` und `condition`. Inhaberinnen und Inhaber legen Schlüssel im Portal an; ein Schlüssel hat die Rechte eines Betrachters. - Mit `condition` melden Batteriemanagementsysteme und Servicepartner Zustandsdaten (`updateCondition`) und Lebenszyklus-Ereignisse (`createLifecycleEvent`). - Grenzen: 600 Anfragen pro Minute für alle Schlüssel einer Organisation zusammen, 30 Versuche je 15 Minuten für unbekannte Schlüssel, 300 öffentliche Abfragen pro Minute und Adresse; `429` mit `Retry-After`.