API-Dokumentation

REST-API v1

Modelle, Pässe, Lebenszyklus-Ereignisse, Telemetrie und Nachweise maschinell pflegen. JSON über HTTPS, Schlüssel mit Berechtigungen, Test-Modus ohne Speichern.

Basis-URL
https://app.batteriepasswerk.com/api/v1
Version
1.1.0
Version

Es gibt derzeit nur Version 1. Ältere Versionen werden hier auswählbar, sobald es sie gibt.

Überblick

Die API bildet dieselben Objekte ab wie das Dashboard: ein Modell ist die Stammdaten-Ebene eines Produkts, ein Pass gehört zu genau einer physischen Batterie. Ereignisse und Messwerte hängen am Pass. Alles läuft über eine Basis-URL, alle Antworten sind JSON, auch Fehler.

Ein Schlüssel, eine Firma

Der Schlüssel bestimmt die Firma. Fremde Kennungen antworten 404, nicht 403 - es gibt keinen Weg, aus einem Schlüssel auf fremde Daten zu schließen.

Kein DELETE

Batteriepässe sind aufbewahrungspflichtig. Version 1 kennt GET, POST und PATCH. Das Lebensende wird als Ereignis gemeldet.

Server-Hoheit

Pflichtfeldquote, Batteriestatus, Kurzcode und Registerzustand berechnet der Server. Diese Felder sind lesbar, aber nicht schreibbar.

Gleiche Regeln wie im Dashboard

Kontingente, Pflichtfelder und Rollen gelten identisch. Es gibt keinen Weg, über die API etwas anzulegen, das im Dashboard verboten wäre.

Schritt 1: Schlüssel anlegen

API-Schlüssel entstehen ausschließlich im Dashboard und ausschließlich durch ein Konto mit der Rolle Admin. Der Inhaber hat immer Admin.

  1. Integrationen öffnen

    Im Dashboard links in der Navigation unter Verwaltung auf Integrationen. Nur Admins sehen diesen Punkt.

  2. API-Schlüssel erstellen

    Im Bereich API-Zugang auf die Schaltfläche API-Schlüssel erstellen. Bezeichnung vergeben, die das System benennt, etwa SAP-Nord-Prod.

  3. Modus und Berechtigungen wählen

    Test für die Entwicklung, Live für den Produktivbetrieb. Dazu eine Vorlage oder einzelne Berechtigungen, und optional ein Ablaufdatum von 30, 90 oder 365 Tagen.

  4. Klartext einmal sichern

    Der Schlüssel wird genau einmal angezeigt. Gespeichert wird nur ein SHA-256-Hash, deshalb kann ihn niemand nachträglich auslesen, auch wir nicht.

Den Schlüssel wie ein Passwort behandeln: nicht in Repositories, nicht im Frontend, nicht in Logs. Geht er verloren, im Dashboard rotieren - der alte wird sofort ungültig, der neue erscheint einmalig.

Schritt 2: Erster Aufruf

GET /me ist der Verbindungstest. Die Antwort nennt Firma, Tarif, Modus, die wirksamen Berechtigungen, das Rate-Limit und das freie Kontingent im laufenden Vertragsjahr. Wenn dieser Aufruf funktioniert, stimmen Schlüssel, Header und Netzwerkweg.

export BPW_API_KEY="bpw_test_…"

curl -s https://app.batteriepasswerk.com/api/v1/me \
  -H "Authorization: Bearer $BPW_API_KEY"

Schritt 3: In der Konsole ausprobieren

Im Dashboard unter Integrationen führt ein Link zur API-Konsole. Dort wählen Sie einen Endpunkt aus der OpenAPI-Beschreibung, füllen Parameter aus, senden mit Ihrer Anmeldung statt mit einem Schlüssel und lesen die Antwort als klappbaren JSON-Baum. Jeder Aufruf lässt sich als cURL kopieren und direkt ins Terminal übernehmen. Schreibende Aufrufe laufen dort standardmäßig als Dry-Run.

Authentifizierung

Jede Anfrage trägt den Header Authorization mit dem Schema Bearer. Es gibt zwei Schlüsselarten, erkennbar am Präfix.

SchlüsselFormZweck
Livebpw_live_ + 48 Hex-ZeichenProduktivbetrieb, schreibt wirklich
Testbpw_test_ + 48 Hex-ZeichenEntwicklung und Abnahme, speichert nie

Zusätzlich akzeptiert die API das Sitzungs-Token eines angemeldeten Dashboard-Nutzers. Die Berechtigungen ergeben sich dann aus der Rolle. Das ist der Weg der API-Konsole und nicht für Integrationen gedacht. Fehlt der Header oder ist der Schlüssel ungültig, antwortet die API 401 mit WWW-Authenticate. Nach 30 Fehlversuchen je IP und Minute folgt 429.

Live, Test und Dry-Run

Es gibt bewusst keine getrennte Sandbox-Datenbank. Ein Test-Schlüssel arbeitet auf Ihren echten Stammdaten und prüft Berechtigungen, Pflichtfelder, Seriennummern und Kontingente wie im Ernstfall, speichert aber nichts.

Live-SchlüsselTest-Schlüssel
Lesenechte Datenechte Daten
Schreibengespeichert, 201vollständig geprüft, 200 mit dry_run: true
Kontingentwird verbrauchtwird nur geprüft
Protokolljaja, als Dry-Run markiert
Rate-Limit600 / Minute60 / Minute

Auch ein Live-Schlüssel kann eine einzelne Anfrage als Probe senden: Header X-BPW-Dry-Run: true. Die Antwort-Header X-BPW-Mode und X-BPW-Dry-Run nennen immer den tatsächlich gültigen Zustand.

Berechtigungen

Jeder Schlüssel trägt genau die Berechtigungen, die das anfragende System braucht. Fehlt eine, antwortet die Route 403 mit dem Code insufficient_scope.

BerechtigungErlaubt
models:readModelle lesen, Entwurf prüfen
models:writeModelle anlegen und ändern
passes:readPässe und Ereignisse lesen, inklusive public_url, gs1_link und short_url für eigene QR-Codes
passes:writePässe anlegen und ändern, Ereignisse melden
telemetry:readTelemetrie-Zeitreihe je Pass lesen (seit 1.1.0)
telemetry:writeMesswerte melden
certificates:readNachweise lesen und herunterladen
suppliers:readLieferanten, Datenanfragen und gelieferte Werte lesen (seit 1.1.0)
suppliers:writeLieferanten einladen (seit 1.1.0)
audit:readAudit-Trail lesen (seit 1.1.0)

GET /me, GET /field-catalog und GET /openapi.json brauchen keine Berechtigung. Vorlagen im Dashboard: Nur lesen (alle read-Berechtigungen), ERP-Synchronisation, BMS-Telemetrie (lesen und melden), Alle, Eigene; das Dashboard erklärt zu jeder Berechtigung die freigeschalteten Endpunkte. Höchstens zehn aktive Schlüssel je Firma; jede Aktion an einem Schlüssel steht mit Person und Zeitpunkt im Audit-Trail.

Tarife

Die Prüfung läuft bei jedem einzelnen Aufruf, nicht nur beim Anlegen des Schlüssels. Ein Tarifwechsel wirkt sofort: Nach einem Wechsel von Enterprise auf Pro behält ein Live-Schlüssel nur noch telemetry:read und telemetry:write.

TarifTest-SchlüsselLive-Schlüssel
Pilot, Starterja, voller Funktionsumfangnein
Projanur telemetry:read und telemetry:write
Enterprisejaalle Berechtigungen
Archiv, Lifetimeja, nur lesendnein

In Aufbewahrungs-Tarifen antworten alle schreibenden Routen 403 mit tenant_read_only. Telemetrie setzt zusätzlich den Tarif Pro oder höher voraus, unabhängig vom Schlüsselmodus.

Anfragen und Antworten

Einzelobjekte kommen direkt als JSON-Objekt, Listen im Envelope mit data, has_more und next_cursor.

Zeit
Zeitstempel sind ISO 8601 in UTC mit Millisekunden, Kalenderdaten YYYY-MM-DD. Eingaben werden normalisiert; ein Datum wie 2026-02-30 wird abgelehnt.
null gegen fehlend
Bei POST und PATCH bleibt ein nicht geliefertes Feld unverändert, null leert das Feld. Leere Zeichenketten werden zu null.
Unbekannte Felder
Werden abgelehnt, nicht ignoriert. Ein Tippfehler im ERP fällt sofort auf, statt still Daten zu verlieren.
Kennungen
UUID in Kleinschreibung. Der übliche Weg vom ERP-Datensatz zum Objekt führt über GET /passes?serial=… oder GET /models?code=…
Zeichensatz und Caching
UTF-8, Content-Type application/json. Alle Antworten tragen Cache-Control: no-store.

Antwort-Header

HeaderBedeutung
X-Request-IdKennung des Aufrufs, steht auch in jeder Fehlerantwort und im API-Protokoll des Dashboards.
X-RateLimit-Limit / -Remaining / -ResetKontingent des laufenden Fensters, Reset als Unix-Sekunden.
X-BPW-Modelive oder test, je nach verwendetem Schlüssel.
X-BPW-Dry-Runtrue, wenn dieser Aufruf nichts gespeichert hat.
Idempotent-Replayedtrue, wenn eine gespeicherte Antwort wiederholt wurde.
Retry-AfterNur bei 429: Wartezeit in Sekunden.

Fehler

Jeder Fehler nutzt denselben Envelope. type gruppiert grob nach HTTP-Status, code ist stabil und für Programmlogik gedacht, message ist englisch und kann sich ändern. param nennt das erste betroffene Feld, im Stapel etwa items[3].serial, details listet alle. Wiederholen Sie 429 und 5xx mit Backoff, 4xx nie automatisch.

400 Bad Request
HTTP/1.1 400 Bad Request
X-Request-Id: req_7f3c9a21e4b84c60

{
  "error": {
    "type": "invalid_request",
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "param": "energy_kwh",
    "details": [
      { "param": "energy_kwh", "code": "invalid_type", "message": "energy_kwh must be a number." },
      { "param": "gtin", "code": "invalid_gtin", "message": "gtin must be a valid GS1 GTIN." }
    ],
    "request_id": "req_7f3c9a21e4b84c60"
  }
}
HTTPcodeWann
400unknown_fieldFeld steht nicht im Schema. details nennt jedes unbekannte Feld.
400validation_failedTypfehler in Modell-Feldern, details listet alle betroffenen Felder.
400missing_fieldPflichtfeld fehlt, etwa code, model_id oder serial.
400invalid_enumWert steht nicht in der erlaubten Liste.
400invalid_serialSeriennummer verletzt den GS1-AI-21-Zeichensatz oder ist zu lang.
400invalid_gtinGTIN hat keine gültige Prüfziffer.
400invalid_date / invalid_timestampKein gültiges Kalenderdatum bzw. kein ISO-8601-Zeitstempel.
400duplicate_serial_in_batchZwei Einträge im selben Stapel tragen dieselbe Seriennummer.
400out_of_range / no_measurementTelemetriewert ausserhalb des Bereichs oder gar kein Messwert geliefert.
400empty_patchPATCH ohne ein einziges änderbares Feld.
401missing_authorizationKein Authorization-Header gesendet.
401invalid_api_keySchlüssel unbekannt oder falsch formatiert.
401api_key_revoked / api_key_expiredSchlüssel wurde widerrufen oder ist abgelaufen.
403plan_requiredDer Tarif erlaubt diesen Modus oder diese Berechtigung nicht.
403insufficient_scopeDem Schlüssel fehlt die für die Route nötige Berechtigung.
403tenant_read_onlyAufbewahrungs-Tarif, schreibende Aufrufe sind gesperrt.
404model_not_found / pass_not_found / certificate_not_foundObjekt existiert nicht in Ihrer Firma. Fremde Kennungen antworten ebenfalls 404.
404route_not_foundPfad gibt es nicht. Tippfehler oder fehlendes /v1.
405method_not_allowedMethode für diesen Pfad nicht erlaubt, Allow-Header nennt die erlaubten.
409duplicate_code / duplicate_serialModell-Code oder Seriennummer existiert bereits in Ihrer Firma.
409limit_reachedKontingent des Tarifs erreicht. GET /me zeigt den Stand.
422idempotency_key_reusedGleicher Idempotency-Key, aber anderer Inhalt.
429rate_limit_exceededFenster erschöpft. Retry-After nennt die Wartezeit in Sekunden.
500internal_errorUnerwarteter Fehler. Bitte die request_id melden.

Pagination

Listen liefern höchstens 200 Einträge je Seite, Standard 50. Sortiert wird nach Erstellzeitpunkt absteigend, bei Telemetrie nach Messzeitpunkt. Der Cursor ist opak und arbeitet auf Zeitstempel und Kennung, nicht auf Offsets: Datensätze, die während des Durchlaufs entstehen, verschieben nichts und werden nicht übersprungen. Für den inkrementellen Abgleich merken Sie sich den größten updated_at der letzten Seite und senden ihn beim nächsten Lauf als updated_since.

pagination.py
import requests

BASE = "https://app.batteriepasswerk.com/api/v1"
H = {"Authorization": f"Bearer {KEY}"}

def iterate(path, **params):
    cursor = None
    while True:
        r = requests.get(f"{BASE}{path}", headers=H,
                         params={**params, "limit": 200, "cursor": cursor})
        r.raise_for_status()
        page = r.json()
        yield from page["data"]
        if not page["has_more"]:
            return
        cursor = page["next_cursor"]

# Inkrementell: nur was sich seit dem letzten Lauf geändert hat
for p in iterate("/passes", updated_since="2026-09-01T00:00:00Z"):
    print(p["serial"], p["lifecycle_status"])

Rate-Limits

Feste Fenster von 60 Sekunden. Jede Antwort nennt das verbleibende Kontingent, bei Überschreitung folgt 429 mit Retry-After. Ein Stapel mit 500 Pässen zählt als eine Anfrage, deshalb ist Massen-Serialisierung selten das Limit.

AufruferLimit
Live-Schlüssel600 Anfragen je Minute
Test-Schlüssel60 je Minute
Sitzung in der Konsole120 je Minute
Fehlgeschlagene Anmeldungen30 je Minute und IP
GET /openapi.json60 je Minute und IP

Idempotenz

Jeder POST nimmt den Header Idempotency-Key mit bis zu 255 Zeichen, etwa die Belegnummer aus dem ERP.

  • Erste Ausführung: normale Verarbeitung, die Antwort wird 24 Stunden unter Firma, Schlüssel und Key gespeichert.
  • Wiederholung mit identischer Anfrage: die gespeicherte Antwort, zusätzlich der Header Idempotent-Replayed: true, ohne erneute Ausführung.
  • Wiederholung mit anderem Inhalt: 422 mit dem Code idempotency_key_reused.
  • Gespeichert werden nur 2xx und 4xx. Nach 429 oder 5xx darf mit demselben Key erneut versucht werden.
# Erster Versuch läuft in einen Timeout - Ergebnis unbekannt
curl -X POST https://app.batteriepasswerk.com/api/v1/passes \
  -H "Authorization: Bearer $BPW_API_KEY" \
  -H "Idempotency-Key: los-2026-09-0042" \
  -H "Content-Type: application/json" \
  -d '{ "items": [ … ] }'

# Gefahrlose Wiederholung mit demselben Schlüssel
# → 201 mit derselben Antwort, zusätzlich: Idempotent-Replayed: true

Konto und Katalog

Achtzehn Endpunkte. Jeder nennt die nötige Berechtigung, die Parameter und ein vollständiges Beispiel. Alle Pfade sind relativ zur Basis-URL.

GET/meBerechtigung: keine

Verbindungstest und Selbstauskunft

Nennt Firma, Tarif, Modus, wirksame Berechtigungen, Rate-Limit und das Kontingent im laufenden Vertragsjahr. Erster Aufruf jeder Integration.

Antwort
{
  "api_version": "v1",
  "mode": "test",
  "dry_run": true,
  "tenant": { "id": "8f21…c4", "name": "Muster GmbH", "plan": "enterprise" },
  "principal": {
    "kind": "api_key",
    "key": { "id": "0d4e…91", "label": "SAP-Nord-Prod", "prefix": "bpw_test_a1b2c3d4", "expires_at": null }
  },
  "scopes": ["models:read", "models:write", "passes:read", "passes:write", "telemetry:write", "certificates:read"],
  "read_only": false,
  "rate_limit": { "limit": 60, "window_seconds": 60 },
  "usage": {
    "contract_year_start": "2026-03-01T00:00:00.000Z",
    "models": { "used": 3, "max": null, "remaining": null },
    "passes": { "used": 18240, "quota": 25000, "quota_source": "plan",
                "remaining": 6760, "enforcement": "overage",
                "overage_units": 0, "overage_price_eur": 0.05, "blocked": false }
  },
  "server_time": "2026-09-09T08:14:02.118Z"
}
GET/field-catalogBerechtigung: keine

Pflichtfeld-Regeln und EU-Datenpunkte

Jede Pflichtfeld-Regel mit den Nummern der 71 offiziellen EU-Datenpunkte, der Rechtsgrundlage, der Anwendbarkeit und den API-Feldern, die sie erfüllen. Damit bildet ein ERP die Datenpunkte auf eigene Felder ab.

Antwort
{
  "rules_version": 9,
  "categories": ["lmt", "bess", "ind", "ev", "device", "sli"],
  "sections": ["identity", "conformity", "carbon", "materials", "circularity", "performance", "dynamic"],
  "data": [
    {
      "key": "gtin",
      "section": "identity",
      "resource": "model",
      "fields": ["gtin"],
      "fill_rule": "all",
      "eu_datapoints": [1],
      "legal_ref": "BR Art. 77(3)",
      "applicability": "always",
      "blocker": true,
      "second_life_exempt": false,
      "condition_note": { "de": null, "en": null }
    }
  ]
}
GET/openapi.jsonBerechtigung: keine

Maschinenlesbarer Vertrag

OpenAPI 3.1, öffentlich abrufbar ohne Schlüssel. Grundlage für Client-Generatoren und für die API-Konsole im Dashboard.

Antwort
{
  "openapi": "3.1.0",
  "info": { "title": "Batteriepasswerk REST API", "version": "1.0.0" },
  "servers": [{ "url": "https://app.batteriepasswerk.com/api/v1" }],
  "paths": { "/me": { "get": { "operationId": "getMe" } } }
}

Modelle

GET/modelsBerechtigung: models:read

Modelle auflisten

Neueste zuerst, Cursor-Pagination. Für den inkrementellen Abgleich updated_since setzen.

Parameter

NameOrtTypBeschreibung
limitqueryinteger 1-200Seitengröße, Standard 50.
cursorquerystringOpaker Cursor aus next_cursor der vorigen Seite.
codequerystringExakter Modell-Code, höchstens ein Treffer.
categoryquerylmt | bess | ind | ev | device | sliBatteriekategorie nach Verordnung.
statusqueryready | review | pending | critInterner Bearbeitungsstatus.
updated_sincequeryISO 8601Nur Modelle, die seit diesem Zeitpunkt geändert wurden.
curl -s "https://app.batteriepasswerk.com/api/v1/models?category=bess&limit=2" \
  -H "Authorization: Bearer $BPW_API_KEY"
GET/models/{id}Berechtigung: models:read

Ein Modell lesen

Alle Fachfelder plus die server-berechnete Pflichtfeldquote. Unbekannte oder fremde Kennungen antworten 404.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
curl -s https://app.batteriepasswerk.com/api/v1/models/f76b5107-d6c3-4527-8d04-0c89209bede1 \
  -H "Authorization: Bearer $BPW_API_KEY"
POST/modelsBerechtigung: models:write

Modell anlegen

Body enthält die schreibbaren Modell-Felder (siehe Feldreferenz). Unbekannte Felder werden abgelehnt. Antwort 201 mit dem Datensatz und dem aktuellen Kontingent; mit Test-Schlüssel 200 mit dry_run und id: null.

Parameter

NameOrtTypBeschreibung
code *bodystringModellbezeichnung, eindeutig je Firma.
categorybodylmt | bess | ind | ev | device | sliBestimmt, welche Pflichtfelder anwendbar sind.
gtinbodystringGS1-GTIN mit gültiger Prüfziffer, steuert die öffentliche Adresse.
second_lifebodybooleanSecond-Life-Ausnahme nach Art. 7(5) und 8(4).
applicability_flagsbodyobject of booleanErklärte Nicht-Anwendbarkeit je Regel des Feldkatalogs.
curl -s -X POST https://app.batteriepasswerk.com/api/v1/models \
  -H "Authorization: Bearer $BPW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-model-4711" \
  -d '{
        "code": "BESS-10",
        "category": "bess",
        "name": "Home Storage 10",
        "chemistry": "LFP",
        "energy_kwh": 10.2,
        "nominal_voltage_v": 51.2,
        "gtin": "04012345678901",
        "ce_marked": true,
        "separate_collection": true
      }'
PATCH/models/{id}Berechtigung: models:write

Modell ändern

Nur gelieferte Felder ändern sich, null leert ein Feld, nicht geliefert bleibt unverändert. Die Pflichtfeldquote wird neu berechnet.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
curl -s -X PATCH https://app.batteriepasswerk.com/api/v1/models/f76b5107-… \
  -H "Authorization: Bearer $BPW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "co2_kg_per_kwh": 61.4, "co2_study_url": "https://example.org/lca.pdf" }'
POST/models/validateBerechtigung: models:read

Entwurf prüfen, nichts speichern

Zustandslos: liefert die Pflichtfeldquote, die ein Modell mit diesen Werten hätte, und listet jedes fehlende Feld. Ideal als Vorprüfung im ERP, bevor überhaupt geschrieben wird.

curl -s -X POST https://app.batteriepasswerk.com/api/v1/models/validate \
  -H "Authorization: Bearer $BPW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "BESS-10", "category": "bess", "energy_kwh": 10.2 }'

Pässe

GET/passesBerechtigung: passes:read

Pässe auflisten

Neueste zuerst. Mit serial finden Sie genau einen Pass, das ist der übliche Weg vom ERP-Datensatz zur Pass-Kennung.

Parameter

NameOrtTypBeschreibung
limitqueryinteger 1-200Seitengröße, Standard 50.
cursorquerystringOpaker Cursor aus next_cursor der vorigen Seite.
model_idqueryuuidNur Pässe dieses Modells.
serialquerystringExakte Seriennummer.
batchquerystringCharge oder Los.
statusqueryready | review | pending | critInterner Bearbeitungsstatus.
lifecycle_statusqueryoriginal | repurposed | re-used | remanufactured | wasteGesetzlicher Batteriestatus, EU-Datenpunkt 67.
created_since / updated_sincequeryISO 8601Zeitfilter, wirken als grösser-gleich.
curl -s "https://app.batteriepasswerk.com/api/v1/passes?serial=SN-000123" \
  -H "Authorization: Bearer $BPW_API_KEY"
GET/passes/{id}Berechtigung: passes:read

Einen Pass lesen

Ein Pass inklusive abgeleitetem Batteriestatus, letztem Telemetrie-Wert, Registerzustand und den öffentlichen Adressen für QR-Druck und Kurzlink.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
curl -s https://app.batteriepasswerk.com/api/v1/passes/ffdad688-745f-475e-aaaa-e23112c0f041 \
  -H "Authorization: Bearer $BPW_API_KEY"
POST/passesBerechtigung: passes:write

Pässe anlegen, einzeln oder als Stapel

Ein Objekt legt einen Pass an, items mit bis zu 500 Einträgen einen Stapel. Ein Stapel zählt als eine Anfrage gegen das Rate-Limit. Doppelte Seriennummern innerhalb eines Stapels sind immer ein Fehler.

Parameter

NameOrtTypBeschreibung
model_id *bodyuuidMuss ein Modell Ihrer Firma sein.
serial *bodystring, max. 20GS1-AI-21-Zeichensatz, eindeutig je Firma.
statusbodyready | review | pending | critStandard pending.
batchbodystring, max. 100Charge oder Los.
production_datebodyYYYY-MM-DDProduktionsdatum, EU-Datenpunkt 9.
on_conflictbodyerror | skipNur im Stapel: skip überspringt vorhandene Seriennummern statt abzubrechen.
curl -s -X POST https://app.batteriepasswerk.com/api/v1/passes \
  -H "Authorization: Bearer $BPW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: los-2026-09-0042" \
  -d '{
        "items": [
          { "model_id": "f76b5107-…", "serial": "SN-000123", "batch": "B04", "production_date": "2026-09-01" },
          { "model_id": "f76b5107-…", "serial": "SN-000124", "batch": "B04", "production_date": "2026-09-01" }
        ],
        "on_conflict": "skip"
      }'
PATCH/passes/{id}Berechtigung: passes:write

Pass ändern

Änderbar sind status, batch und production_date. Der gesetzliche Batteriestatus lässt sich bewusst nicht setzen, er entsteht aus Ereignissen.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
curl -s -X PATCH https://app.batteriepasswerk.com/api/v1/passes/ffdad688-… \
  -H "Authorization: Bearer $BPW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "ready" }'

Lebenszyklus-Ereignisse

GET/passes/{id}/eventsBerechtigung: passes:read

Ereigniskette lesen

Alle Lebenszyklus-Ereignisse eines Passes, neueste zuerst. source ist null bei Eingaben im Dashboard und api bei maschinellen Meldungen.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
limitqueryinteger 1-200Seitengröße, Standard 50.
cursorquerystringOpaker Cursor aus next_cursor der vorigen Seite.
Antwort
{
  "data": [
    { "id": "9c2e…41a7", "pass_id": "ffdad688-…", "event_type": "market",
      "event_date": "2026-09-05", "note": null, "source": "api",
      "created_at": "2026-09-05T09:12:44.201Z" }
  ],
  "has_more": false,
  "next_cursor": null
}
POST/passes/{id}/eventsBerechtigung: passes:write

Ereignis melden

Meldet ein Lebenszyklus-Ereignis. Der gesetzliche Batteriestatus wird daraus abgeleitet und in der Antwort zurückgegeben. recycled und eol beenden den Pass.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
event_type *bodymarket | repaired | reused | secondlife | remanufactured | recycled | eolArt des Ereignisses.
event_datebodyYYYY-MM-DDStandard heute, kalendarisch geprüft.
notebodystring, max. 500Freitext, etwa Auftrags- oder Werkstattnummer.
curl -s -X POST https://app.batteriepasswerk.com/api/v1/passes/ffdad688-…/events \
  -H "Authorization: Bearer $BPW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "event_type": "secondlife", "event_date": "2031-04-18" }'

Telemetrie

GET/passes/{id}/telemetryBerechtigung: telemetry:read

Messreihe lesen

Messwerte eines Passes, neueste Messung zuerst, sortiert nach recorded_at.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
limitqueryinteger 1-200Seitengröße, Standard 50.
cursorquerystringOpaker Cursor aus next_cursor der vorigen Seite.
Antwort
{
  "data": [
    { "id": "7a41…c2", "pass_id": "ffdad688-…", "recorded_at": "2026-09-08T22:00:00.000Z",
      "soh_pct": 98.4, "soc_pct": 62, "cycle_count": 142, "negative_event": null,
      "created_at": "2026-09-08T22:00:03.774Z" }
  ],
  "has_more": true,
  "next_cursor": "eyJjIjoiMjAyNi0wOS0wOFQyMjowMDowMFoiLCJpIjoiN2E0MSJ9"
}
POST/passes/{id}/telemetryBerechtigung: telemetry:write

Messung melden

Mindestens ein Messwert oder negative_event je Aufruf. Setzt den Tarif Pro oder höher voraus, unabhängig vom Schlüsselmodus. Werte ausserhalb des Bereichs antworten 400.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
recorded_atbodyISO 8601Messzeitpunkt aus dem Gerät, Standard Empfangszeit.
soh_pct, soc_pct, cycle_count, …bodynumberMesswerte, siehe Telemetrie-Feldreferenz.
negative_eventbodydeep_discharge | overheat | accidentNegatives Ereignis, EU-Datenpunkt 69.
curl -s -X POST https://app.batteriepasswerk.com/api/v1/passes/ffdad688-…/telemetry \
  -H "Authorization: Bearer $BPW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "recorded_at": "2026-09-08T22:00:00Z",
        "soh_pct": 98.4,
        "soc_pct": 62,
        "cycle_count": 142,
        "temp_min_c": 11.2,
        "temp_max_c": 28.7
      }'

Nachweise

GET/certificatesBerechtigung: certificates:read

Nachweise auflisten

Metadaten der hinterlegten Nachweise. validity leitet sich aus valid_until ab, expiring bedeutet Ablauf innerhalb von 30 Tagen.

Parameter

NameOrtTypBeschreibung
limitqueryinteger 1-200Seitengröße, Standard 50.
cursorquerystringOpaker Cursor aus next_cursor der vorigen Seite.
model_idqueryuuidNur Nachweise zu diesem Modell.
typequeryreach | material | duediligence | co2 | conformity | testreport | disassemblyArt des Nachweises.
statusqueryreview | acceptedFreigabestand.
Antwort
{
  "data": [
    { "id": "b2c8…19", "name": "UN 38.3 Testzusammenfassung", "type": "testreport",
      "issuer": "TÜV", "model_id": "f76b5107-…", "valid_until": "2027-05-30",
      "validity": "valid", "status": "accepted", "file_name": "un383.pdf",
      "file_size": 481221, "has_file": true }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/certificates/{id}Berechtigung: certificates:read

Nachweis mit Download-Link

Wie die Liste, zusätzlich ein signierter Download-Link mit 300 Sekunden Gültigkeit. Den Link nicht zwischenspeichern, sondern bei Bedarf neu holen.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
Antwort
{
  "id": "b2c8…19",
  "name": "UN 38.3 Testzusammenfassung",
  "type": "testreport",
  "validity": "valid",
  "download_url": "https://…/storage/v1/object/sign/certs/…?token=…",
  "download_expires_at": "2026-09-09T08:36:02.000Z"
}

Lieferanten und Datenanfragen

GET/suppliersBerechtigung: suppliers:read

Lieferanten auflisten

Alle Lieferanten, die Ihre Firma mindestens einmal eingeladen hat, ein Eintrag je E-Mail-Adresse. Neueste zuerst.

Parameter

NameOrtTypBeschreibung
limitqueryinteger 1-200Seitengröße, Standard 50.
cursorquerystringOpaker Cursor aus next_cursor der vorigen Seite.
Antwort
{
  "data": [
    {
      "id": "3f0c…9a",
      "name": "Zellwerk GmbH",
      "email": "einkauf@zellwerk.example",
      "created_at": "2026-09-09T08:12:40.211Z",
      "updated_at": "2026-09-09T08:12:40.211Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/supplier-requestsBerechtigung: suppliers:read

Datenanfragen auflisten

Jede Datenanfrage an einen Lieferanten mit Status (invited, progress, delivered, expired), den angefragten Feldern, Frist und Erinnerungen. Neueste zuerst.

Parameter

NameOrtTypBeschreibung
limitqueryinteger 1-200Seitengröße, Standard 50.
cursorquerystringOpaker Cursor aus next_cursor der vorigen Seite.
supplier_idqueryuuidNur Anfragen an diesen Lieferanten.
model_idqueryuuidNur Anfragen zu diesem Modell.
statusqueryinvited | progress | delivered | expiredStatus der Anfrage.
Antwort
{
  "data": [
    {
      "id": "b7d2…41",
      "supplier_id": "3f0c…9a",
      "supplier_name": "Zellwerk GmbH",
      "supplier_email": "einkauf@zellwerk.example",
      "model_id": "0d1f…c8",
      "model_code": "BESS-10",
      "supplier_type": "cell",
      "status": "delivered",
      "fields": [{ "key": "cobalt_pct", "label": "Kobalt-Anteil", "unit": "%" }],
      "due_date": "2026-10-15",
      "invited_at": "2026-09-09T08:12:41.030Z",
      "expires_at": "2026-11-08T08:12:41.030Z",
      "delivered_at": "2026-09-12T14:03:07.512Z",
      "last_reminder_at": null,
      "reminder_count": 0,
      "created_at": "2026-09-09T08:12:41.030Z",
      "updated_at": "2026-09-12T14:03:07.512Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
POST/supplier-requestsBerechtigung: suppliers:write

Lieferanten einladen

Legt den Lieferanten (per E-Mail eindeutig) und eine Datenanfrage zu einem Ihrer Modelle an und verschickt den passwortlosen Self-Service-Link (60 Tage gültig). Der Link steht nur in dieser Antwort. Tarif Starter oder höher, höchstens 60 Einladungen je Stunde und Firma. Dry-Run prüft alles, legt nichts an und verschickt nichts.

Parameter

NameOrtTypBeschreibung
supplier_name *bodystringFirmenname des Lieferanten, bis 200 Zeichen.
email *bodye-mailEmpfänger der Einladung, identifiziert den Lieferanten in Ihrer Firma.
model_id *bodyuuidEines Ihrer Modelle.
fields *bodyarrayAngefragte Felder { key, label, unit?, label_en?, label_zh? }, 1 bis 30. key: a-z, 0-9, Unterstrich.
supplier_typebodycell | material | bmsArt des Lieferanten, optional.
due_datebodyYYYY-MM-DDFrist für die Lieferung der Daten, optional.
curl -s -X POST https://app.batteriepasswerk.com/api/v1/supplier-requests \
  -H "Authorization: Bearer $BPW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-po-4711-cells" \
  -d '{
        "supplier_name": "Zellwerk GmbH",
        "email": "einkauf@zellwerk.example",
        "model_id": "0d1f…c8",
        "supplier_type": "cell",
        "due_date": "2026-10-15",
        "fields": [
          { "key": "cobalt_pct", "label": "Kobalt-Anteil", "unit": "%", "label_en": "Cobalt share" }
        ]
      }'
GET/supplier-requests/{id}Berechtigung: suppliers:read

Datenanfrage mit gelieferten Werten lesen

Eine Anfrage inklusive submissions: die Werte, die der Lieferant je Feldschlüssel geliefert hat, bei Datei-Nachweisen mit Dateiname.

Parameter

NameOrtTypBeschreibung
id *pathuuidKennung des Objekts in Ihrer Firma.
Antwort
{
  "id": "b7d2…41",
  "supplier_name": "Zellwerk GmbH",
  "model_code": "BESS-10",
  "status": "delivered",
  "fields": [{ "key": "cobalt_pct", "label": "Kobalt-Anteil", "unit": "%" }],
  "submissions": [
    {
      "field_key": "cobalt_pct",
      "value": "6.2",
      "file_name": null,
      "file_size": null,
      "submitted_at": "2026-09-12T14:03:07.512Z"
    }
  ]
}

Audit-Trail

GET/auditBerechtigung: audit:read

Audit-Trail lesen

Jede Schreibaktion Ihrer Firma, wie sie die Datenbank festhält: wer (Nutzer, API-Schlüssel oder System), was (action wie pass.create, Objekt, Referenz) und wann. seq und row_hash gehören zur Hash-Kette je Firma und machen Manipulationen nachweisbar. Neueste zuerst.

Parameter

NameOrtTypBeschreibung
limitqueryinteger 1-200Seitengröße, Standard 50.
cursorquerystringOpaker Cursor aus next_cursor der vorigen Seite.
entity_typequerymodel | pass | cert | supplier | member | api_keyObjektart.
actionquerystringGenaue Aktion, z. B. pass.create, model.update, supplier.invite.
entity_idqueryuuidEinträge zu diesem Objekt.
actor_kindqueryuser | api_key | systemWer gehandelt hat.
actor_key_idqueryuuidEinträge über diesen API-Schlüssel.
sincequeryISO 8601Ab diesem Zeitpunkt.
untilqueryISO 8601Bis zu diesem Zeitpunkt.
curl -s "https://app.batteriepasswerk.com/api/v1/audit?entity_type=pass&since=2026-09-01T00:00:00Z&limit=100" \
  -H "Authorization: Bearer $BPW_API_KEY"

Modell-Felder

Alle schreibbaren Felder eines Modells in snake_case, identisch mit der Benennung im Dashboard. Die Spalte EU nennt die Nummer des offiziellen EU-Datenpunkts, soweit das Feld einem zugeordnet ist. Zeichenketten sind auf 4000 Zeichen begrenzt. Nur lesbar sind id, readiness, readiness_detail, created_at, updated_at und public_url.

Identität und Verwaltung

FeldTypEU
codestring, Pflicht7
namestring-
categorylmt | bess | ind | ev | device | sli6
statusready | review | pending | crit-
gtinstring1
economic_operator_idstring2
manufacturing_sitestring8
weight_kgnumber10
unitsinteger-
warranty_monthsinteger35
second_lifeboolean-
applicability_flagsobject of boolean-

Konformität

FeldTypEU
ce_markedboolean40
separate_collectionboolean40
substance_symbolsstring [ ] 41
conformity_responsiblestring2
conformity_declaration_idstring42
due_diligence_urlstring19

CO2-Fussabdruck

FeldTypEU
co2_kg_per_kwhnumber17
carbon_perf_classstring18
co2_phasesobject17
co2_limit_okboolean17
co2_study_urlstring17
lca_method, lca_source_de, lca_source_en, co2_auditorstring-

Materialien

FeldTypEU
chemistrystring12
hazard_de, hazard_en, hazard_detailstring13
substance_impact_de, substance_impact_enstring13
critical_materialsarray15
cathode, anode_de, anode_en, electrolytestring45
active_materials, material_originstring-

Kreislauf

FeldTypEU
rec_cobalt_pct, rec_lithium_pct, rec_nickel_pct, rec_lead_pctnumber20-23
renewable_pctnumber24
recyclate_pctinteger-
eol_info_de, eol_info_enstring43
waste_prevention_url, separate_collection_url, collection_info_urlstring43
recycling_efficiency_pctnumber-
spare_part_numbersstring46
spare_source_postal, spare_source_email, spare_source_webstring47
disassembly_doc_urlstring48
disassembly_cert_iduuid48
safety_measures_urlstring49

Leistung und Haltbarkeit

FeldTypEU
energy_kwhnumber11
nominal_voltage_vnumber27
voltage_min_v, voltage_max_vnumber26, 28
rated_capacity_ahnumber25
power_wnumber29
max_power_wnumber30
rated_cyclesinteger31, 59
cycle_life_teststring32
capacity_threshold_pctnumber33
temp_min_c, temp_max_cnumber34
temp_storage_min_c, temp_storage_max_cnumber34
round_trip_pctnumber36
rte_50_pctnumber37
internal_resistance_cell_mohm, internal_resistance_pack_mohmnumber38
c_ratenumber39
capacity_fade_pct, power_fade_pct, rte_fade_pctnumber52, 54, 58
expected_lifetime_yearsnumber60
hazard_classstring-
extinguishing_de, extinguishing_enstring14

Pass-Felder

Schreibbar beim Anlegen sind model_id, serial, status, batch und production_date; danach per PATCH noch status, batch und production_date. Alles Übrige gehört dem Server.

FeldBedeutung
lifecycle_statusGesetzlicher Batteriestatus, aus der Ereigniskette abgeleitet (EU 67).
retired_atGesetzt, sobald recycled oder eol gemeldet wurde.
state_of_health_pctLetzter gemeldeter Telemetrie-Wert.
short_code, short_urlKurzlink der öffentlichen Pass-Seite.
public_url, gs1_linkGS1 Digital Link, sobald das Modell eine GTIN hat, sonst die Kennungs-Adresse.
registry_status, registry_uri, registry_registered_at, registry_errorZustand der Registrierung im EU-Register.
supersedes_pass_id, superseded_by_pass_idVerkettung bei Umnutzung und Wiederaufarbeitung.

Ereignistypen und abgeleiteter Status

event_typelifecycle_status danachBedeutung
market-In Verkehr gebracht. Ändert den Status nicht, ist aber der gesetzliche Startpunkt.
repaired-Repariert, Status bleibt unverändert.
reusedre-usedWiederverwendung im ursprünglichen Zweck.
secondliferepurposedUmnutzung, etwa Fahrzeugbatterie wird stationärer Speicher.
remanufacturedremanufacturedWiederaufgearbeitet.
recycledwasteRecycelt. Beendet den Pass, retired_at wird gesetzt.
eolwasteLebensende ohne Recycling-Nachweis.

Telemetrie-Felder

Mindestens ein Messwert oder negative_event je Aufruf. Werte außerhalb des Bereichs antworten 400 mit out_of_range und dem betroffenen Feld in param.

FeldBereichEU
recorded_atISO 8601-
soh_pct0-100-
soc_pct0-10071
soce_pct0-10061
capacity_kwh≥ 051
power_kw≥ 053
remaining_capacity_ah≥ 062
remaining_power_capability_pct0-10063
remaining_rte_pct0-10064
self_discharge_pct_month≥ 065
ohmic_resistance_mohm≥ 066
internal_resistance_increase_pct≥ 056
cycle_count≥ 0, integer68
negative_eventdeep_discharge | overheat | accident69
temp_min_c, temp_max_c≥ -27370
time_extreme_high_min, time_extreme_low_min, time_charging_extreme_high_min, time_charging_extreme_low_min≥ 070
energy_throughput_kwh, capacity_throughput_ah≥ 0-
notestring, max. 500-

Webhooks

Die API ist Pull, Webhooks sind Push. Signierte Ereignisse erreichen Ihr System, sobald sie eintreten: pass.created, model.updated, supplier.delivered und cert.expiring. Signatur per HMAC-SHA256, Wiederholung bei Fehlern. Bewährtes Muster: den Webhook als Anstoß nehmen und die betroffene Ressource anschließend über die API laden, damit die Wahrheit immer aus der API kommt. Aufrufe über die API lösen dieselben Webhooks aus wie Eingaben im Dashboard.

Versionierung und Changelog

Die Version steht im Pfad. Innerhalb von v1 kommen nur Felder und Endpunkte hinzu; bestehende Felder, Fehlercodes und Bedeutungen bleiben. Entfällt etwas, kündigen wir es mindestens zwölf Monate vorher hier, im OpenAPI-Dokument und per E-Mail an die Admins an und betreiben die Nachfolgeversion parallel unter /v2.

VersionDatumÄnderung
1.1.02026-09-09Neue Berechtigungen telemetry:read, suppliers:read, suppliers:write und audit:read. Neue Endpunkte GET /suppliers, GET und POST /supplier-requests, GET /supplier-requests/{id}, GET /audit. Bestehende Schlüssel mit passes:read wurden automatisch um telemetry:read ergänzt.
1.0.02026-09-09Erstveröffentlichung: Modelle, Pässe, Ereignisse, Telemetrie, Nachweise, Feldkatalog, Test-Schlüssel mit Dry-Run, Idempotency-Key, Cursor-Pagination.

Häufige Fragen

Ist die REST-API bereits verfügbar?
Ja. Version 1.1.0 ist in Betrieb: Modelle, Pässe, Lebenszyklus-Ereignisse, Telemetrie, Nachweise, Lieferanten-Anfragen, Audit-Trail und der Feldkatalog. Die vollständige Schnittstelle für ERP und MES gehört zum Enterprise-Paket, die BMS-Telemetrie-Schnittstelle ist ab Pro enthalten, und einen Test-Schlüssel mit vollem Funktionsumfang gibt es in jedem Paket, auch im kostenlosen Pilot.
Wie teste ich, ohne echte Pässe zu erzeugen?
Mit einem Test-Schlüssel. Er prüft jede Anfrage vollständig gegen Ihre echten Stammdaten, Berechtigungen, Pflichtfelder und Kontingente und antwortet mit dem Ergebnis, das ein Live-Aufruf hätte, ohne etwas zu speichern. Es gibt bewusst keine getrennte Sandbox-Datenbank mit ausgedachten Daten, die mit der Zeit von der Wirklichkeit abweicht.
Wer darf einen API-Schlüssel anlegen?
Ausschließlich Konten mit der Rolle Admin, wozu der Inhaber immer gehört. Die Regel wird auf drei Ebenen durchgesetzt: in der Datenbank, im Endpunkt und in der Oberfläche. Jede Aktion an einem Schlüssel steht mit Person und Zeitpunkt im Audit-Trail.
Welche Programmiersprache brauche ich?
Jede, die HTTPS und JSON kann. Es gibt kein SDK, das Sie einbinden müssen. Aus dem OpenAPI-3.1-Dokument erzeugen gängige Generatoren bei Bedarf einen typisierten Client für Java, C#, Python, TypeScript oder Go.
Wie finde ich die Pass-Kennung zu einer Seriennummer?
Über GET /passes mit dem Parameter serial. Die Seriennummer ist je Firma eindeutig, es gibt also höchstens einen Treffer. Viele Integrationen lösen die Kennung einmal auf und speichern sie neben der Seriennummer im eigenen System.
Was passiert, wenn eine Anfrage in einen Timeout läuft?
Wiederholen Sie sie mit demselben Idempotency-Key. Wurde die erste Anfrage verarbeitet, bekommen Sie die gespeicherte Antwort zurück statt eines Duplikats. Weicht der Inhalt bei gleichem Schlüssel ab, lehnt die API die Anfrage ab, statt still etwas anderes zu tun.
Kann ich einen Pass über die API löschen?
Nein, und das ist Absicht. Batteriepässe unterliegen der Aufbewahrung, deshalb gibt es in Version 1 keine Lösch-Operation. Das Ende des Lebenszyklus wird als Ereignis gemeldet, etwa Recycling, und der Pass zeigt danach den Endzustand.
Wie viele Pässe kann ich anlegen?
Das Kontingent hängt am Tarif und steht in jeder Antwort von GET /me unter usage. Das Feld enforcement sagt, was beim Überschreiten passiert: hard bedeutet Ablehnung, overage bedeutet Abrechnung je zusätzlichem Pass, contract bedeutet vereinbartes Volumen ohne Sperre.

Fragen zur Anbindung?

Im Erstgespräch klären wir, welche Daten aus welchem System kommen, welche Berechtigungen Ihre Schlüssel brauchen und wie die Serialisierung in Ihre Fertigung passt.

Stand 9. September 2026 · API-Version 1.1.0 · Keine Rechtsberatung · Verbindlich ist der Verordnungstext