Skip to content
Workflows

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

#WhoWhatOperation
1MiddlewarePolls the change feed with the stored cursor, for example every five minutes, filtered to passport changes.listChanges read
2MiddlewareReads the unit to get its serial number and model, which find the record in the ERP.getBattery read
3MiddlewareReads the unit's passport with its status, current version and GS1 Digital Link.getPassport read
4MiddlewareDownloads the QR code (PNG or SVG) when a label is to be printed.getPassportQr read
5ERPStores the Digital Link and the passport status on the serial number or equipment record; label printing reads them there.
6MiddlewareStores 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[]query

Comma-separated change actions; all when omitted. For this workflow: passport.published, passport.republished, passport.withdrawn.

cursorstringquery
The next_cursor of your last page; without it the feed starts at the beginning of your organisation's history.
limitintegerquerydefault: 50
At most 200.
200
{
  "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"
}
actionentity_typeRead next
battery.createdbatterygetBattery
passport.published, passport.republished, passport.withdrawnbattery (the unit)getPassport
condition.updated, lifecycle_event.recordedbatterygetCondition, listLifecycleEvents
model_version.approvedmodel_versiongetModelVersion
import.committed, import.approvedimporta key with scope import: getImport

Three rules make polling reliable:

  • next_cursor is set whenever the page has items, also on the last page. Store it and send it next time. An empty page has next_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

AnswerMeaningWhat to do
400 validation_error on cursorThe stored cursor cannot be usedDelete 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 typesAn unknown change actionFix the list.
401 unauthenticatedKey missing, unknown or revokedCheck the secret; alert.
403 forbiddenThe key lacks the scope readUse the read key.
404 not_found on a unitThe unit is not in your organisationSkip it and log the change id.
409 conflict on getPassportQrThe identifiers are not locked yetAsk again after the next passport.published.
429 rate_limitedToo many requestsWait Retry-After seconds; keep the cursor.
5xx, timeoutUnknown outcomeRetry with backoff; the cursor still points before the page.

On this page