Passport links back into the ERP
Follow the change feed with a read key and store each unit's GS1 Digital Link, passport status and QR code on its serial number record in the ERP.
Labels, delivery notes and service tools in your ERP need the link to each battery's passport. In this workflow
the middleware follows BatteryPass's change feed with a key of
scope read, reads the unit and its passport, and writes the GS1 Digital Link and the passport status to the
serial number record in the ERP. Nothing is written to BatteryPass.
sequenceDiagram
participant MW as Middleware
participant BP as BatteryPass API
participant ERP
loop every few minutes
MW->>BP: listChanges with stored cursor and passport types (scope read)
BP-->>MW: changes oldest first, next_cursor
MW->>BP: getBattery (scope read)
MW->>BP: getPassport (scope read)
MW->>BP: getPassportQr when a label is needed (scope read)
MW->>ERP: store Digital Link and status on the serial record
MW->>MW: store next_cursor after the ERP accepted
end
Trigger: A passport is published, re-published or withdrawn.
Key scopes: read
| # | Who | What | Operation |
|---|---|---|---|
| 1 | Middleware | Polls the change feed with the stored cursor, for example every five minutes, filtered to passport changes. | listChanges read |
| 2 | Middleware | Reads the unit to get its serial number and model, which find the record in the ERP. | getBattery read |
| 3 | Middleware | Reads the unit's passport with its status, current version and GS1 Digital Link. | getPassport read |
| 4 | Middleware | Downloads the QR code (PNG or SVG) when a label is to be printed. | getPassportQr read |
| 5 | ERP | Stores the Digital Link and the passport status on the serial number or equipment record; label printing reads them there. | |
| 6 | Middleware | Stores the cursor only after the ERP accepted the update; after a crash it repeats changes instead of losing them. |
Security
- A key with the scope read only; it cannot change anything.
- The feed delivers thin events without values; the middleware fetches the data afterwards with the same key, so an intercepted feed reveals nothing.
- Changes arrive at least once; writing to the ERP is idempotent (same link, same status).
Data minimisation
- Only the Digital Link, the passport status and the version number go into the ERP, no passport content.
The change feed
GET /api/v1/changes returns your organisation's changes oldest first. Each change names only what happened and to
which entity; read the entity for its current state.
typesstring[]queryComma-separated change actions; all when omitted. For this workflow: passport.published,
passport.republished, passport.withdrawn.
cursorstringquerynext_cursor of your last page; without it the feed starts at the beginning of your organisation's history.limitintegerquerydefault: 50{
"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 | Read next |
|---|---|---|
battery.created | battery | getBattery |
passport.published, passport.republished, passport.withdrawn | battery (the unit) | getPassport |
condition.updated, lifecycle_event.recorded | battery | getCondition, listLifecycleEvents |
model_version.approved | model_version | getModelVersion |
import.committed, import.approved | import | a key with scope import: getImport |
Three rules make polling reliable:
next_cursoris set whenever the page has items, also on the last page. Store it and send it next time. An empty page hasnext_cursor: null: keep the cursor you sent.- A change appears only once every older transaction has finished. A cursor therefore never jumps over a change that was committed late. A change may appear a few seconds after it happened.
- Store the cursor only after the ERP accepted the updates of the page. After a crash you see the same changes again (at least once); writing the same link and status twice is harmless.
Poll and write back
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 label.svg "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/passport/qr?format=svg" \
-H "Authorization: Bearer $BATTERYPASS_READ_KEY"The passport carries what the ERP needs: serial_number and gtin to find the record, gs1_digital_link for the
label ({resolver}/01/{gtin}/21/{serial}), status (published, update_needed, withdrawn, …), passport_id
and current_version.version_no. A label printer takes the QR code from
getPassportQr (format=png with size, or format=svg);
it renders the same Digital Link. getBattery adds the model,
model version and dates when your ERP record needs them.
The first run without a cursor walks through your whole history; filter with types and let it run once before
you schedule it. How the Digital Link resolves is described in
Resolve a Digital Link.
What goes into the ERP
Only the Digital Link, the passport status and the version number. Passport content stays in BatteryPass: the public page and the public JSON behind the Digital Link are always current, and restricted data must not be copied into systems with a wider audience.
Failure handling
| Answer | Meaning | What to do |
|---|---|---|
400 validation_error on cursor | The stored cursor cannot be used | Delete it; the next run starts at the beginning and repeats changes, which is harmless because writing the same link twice changes nothing. |
400 validation_error on types | An unknown change action | Fix the list. |
401 unauthenticated | Key missing, unknown or revoked | Check the secret; alert. |
403 forbidden | The key lacks the scope read | Use the read key. |
404 not_found on a unit | The unit is not in your organisation | Skip it and log the change id. |
409 conflict on getPassportQr | The identifiers are not locked yet | Ask again after the next passport.published. |
429 rate_limited | Too many requests | Wait Retry-After seconds; keep the cursor. |
5xx, timeout | Unknown outcome | Retry with backoff; the cursor still points before the page. |
Units from production
Stage the serial numbers of finished batteries as an import with a key of scope import; members review, commit, approve and publish in the portal.
Condition data from BMS and service
A battery management system or a service tool reports measurements per unit with updateCondition; newer measurements are accepted, older ones skipped, and the passport is re-published by your policy.