# 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)).
