Skip to content

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
curl -sS "$BATTERYPASS_URL/api/v1/batteries?limit=200&passport_status=published" \
  -H "Authorization: Bearer $BATTERYPASS_API_KEY"
200 (shortened)
{
  "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:

ParameterOnly units
battery_model_idof this battery model
model_version_idof this model version
passport_statuswhose passport has this status: draft, qr_locked, published, update_needed, suspended, withdrawn, deleted_recycled
serial_prefixwhose serial number starts with this text (1 to 50 characters)
import_idcreated 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 beginning

What 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_error on cursor), 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 changedHow you see it
A new unitAn 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_neededpassport_status differs; this does not change row_version
A new passport versioncurrent_version.version_no of getPassport is higher
Values of the unitrevision 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).

On this page