Sync units into your system
Copy the units of your organisation into an ERP, MES or database with cursor paging, resume after an interruption and detect what changed.
listBatteries returns the units (individual batteries) of your
organisation page by page. This guide reads all of them, stores the cursor so an interrupted run can continue,
and shows how to tell which units changed since the last run. You need a key with the scope read.
To notice changes without reading every unit again, poll the change feed
listChanges; the workflow
Passport links back into the ERP shows how.
The list
curl -sS "$BATTERYPASS_URL/api/v1/batteries?limit=200&passport_status=published" \
-H "Authorization: Bearer $BATTERYPASS_API_KEY"{
"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"
}Units come ordered by serial number, then id. limit is 1 to 200 (default 50). next_cursor is null on the
last page. These filters narrow the list and can be combined:
| Parameter | Only units |
|---|---|
battery_model_id | of this battery model |
model_version_id | of this model version |
passport_status | whose passport has this status: draft, qr_locked, published, update_needed, suspended, withdrawn, deleted_recycled |
serial_prefix | whose serial number starts with this text (1 to 50 characters) |
import_id | created by this spreadsheet import |
There is no filter for a change date. Change detection happens on your side, as described below.
Read every page
Keep the same filters for every page of a run, and store the cursor only after you have stored the page it came with. When the run is complete, delete the cursor so the next run starts at the beginning.
// 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 beginningWhat the cursor is, and what it is not
- The cursor is opaque: store it as it is and send it back unchanged. It does not expire.
- It marks a position in the serial-number order, not a point in time. A unit created during a run whose serial number sorts before the cursor is not in the rest of that run; the next run finds it. So do not use a stored cursor to ask "what is new since last time": every run is a full pass, and the cursor only lets an interrupted pass continue.
- If BatteryPass rejects a stored cursor (
400 validation_erroroncursor), delete it and start a new pass.
Pagination describes the rules for all lists.
Detect changes
Compare each unit with what you stored in the previous run:
| What changed | How you see it |
|---|---|
| A new unit | An id you have not stored yet |
The unit's own data (serial number, dates, facility, battery_status, model version, locked identifiers) | row_version is higher and updated_at is later |
The passport status, e.g. published → update_needed | passport_status differs; this does not change row_version |
| A new passport version | current_version.version_no of getPassport is higher |
| Values of the unit | revision and recorded_at per field in listBatteryValues |
Units are never deleted through the API; a battery at the end of its life shows it in battery_status and
passport_status. Reading the values of every unit costs one request per unit: limit it to the units whose
status or version changed, or spread it over several runs. The recipe
Data warehouse sync writes units and values to JSON Lines.
Limits
All keys of your organisation share 600 requests per minute. A full pass over 10 000 units with limit=200 takes
50 requests; reading values for each unit adds 10 000. Answer 429 rate_limited by waiting for Retry-After
(Rate limits, Handle errors and retries).