Skip to content

The security model of an ERP integration

Keys and scopes, secrets and rotation, transport, safe retries, the outbox, thin events, stale writes, data minimisation, the audit trail and the OWASP API Top 10.

An integration between your ERP and BatteryPass moves data that ends up in signed, public battery passports. This page collects the rules that keep that path safe. Most of them are on your side of the connection; where BatteryPass enforces something itself, it says so. Key handling in day-to-day operation is described in Handle keys securely.

Keys and scopes

One key per integration and per environment. A test middleware and the production middleware get different keys, and so do two integrations that run in the same middleware. You can then revoke one without stopping the others, and the portal's Last used column tells you which one is active.

The smallest scope that works. A key has one or more of three scopes:

ScopeLets the keyWorkflows
readread models, units, values, passports, exports and the change feedPassport links back into the ERP, Product change, lookups in the other workflows
conditionreport measured condition data and lifecycle eventsCondition data, Returns and repairs
importupload, map and validate spreadsheet imports of units, and read themUnits from production

No scope lets a key commit an import, approve a value, publish, withdraw, manage members or keys, or change declared data. Those stay with members of your organisation in the portal. A key acts with the rights of a viewer plus its scopes, and it is never limited to some units: a condition key can report for every unit of your organisation.

Secrets

  • Keep each key in a secrets manager or vault (HashiCorp Vault, Azure Key Vault, AWS Secrets Manager, the iPaaS's credential store) and hand it to the process at start. Never put it into code, configuration under version control, container images, URLs, logs or tickets. The OWASP Secrets Management Cheat Sheet describes the practice in detail.
  • Send the key only in the header Authorization: Bearer bp_live_…. A request that also carries a portal session cookie is refused, so the actor is never ambiguous.
  • Mask the Authorization header in every log of your middleware and HTTP client.

Rotation. Keys do not expire. Rotate them on your own schedule and whenever someone who knew a key leaves: create a new key with the same scopes, switch the middleware to it, check that the new key shows a recent Last used time, then revoke the old one. Both work at the same time during the switch, so nothing fails. Revocation is immediate and final.

Transport

  • Call BatteryPass only over HTTPS (TLS) and verify the server certificate. Do not turn verification off in test environments either: a middleware configuration tends to travel from test to production.
  • Egress allow-listing as a second line: let the middleware reach only the BatteryPass address and the ERP. It does not replace keys and TLS, but it limits what a compromised middleware can talk to.
  • The same goes the other way: expose the ERP's APIs to the middleware only, never directly to the internet.

Retries that do not duplicate

Networks fail. A request that timed out may or may not have been carried out, so retries must be safe.

Back off. Retry network errors and 5xx answers with exponential backoff and jitter (for example 1, 2, 4, 8 seconds plus a random part, at most five attempts). Answer 429 rate_limited by waiting the seconds of the Retry-After header; all keys of your organisation share 600 requests per minute. Do not retry other 4xx answers: the request was refused as a whole, so fix it. Details are in Handle errors and retries and Rate limits.

Make each retry safe. BatteryPass accepts an Idempotency-Key header only on bulk and publishing operations of the portal, which keys cannot call. For the operations a key uses, the safety comes from the operations themselves and from your middleware:

OperationSafe to repeatWhy, or what you do
Every GET (reads, the change feed, getImport)YesThey change nothing.
updateConditionYes, with the same measured_atA measurement equal to or older than the stored one is skipped.
createImportYes, with careA repeated upload creates a second import. Its rows become duplicates once the first one is committed, so no unit is created twice; members simply do not commit the extra import.
updateImportMappingYesWith the same If-Match the repetition is refused as stale (409); read the import and you see the mapping already applied.
validateImportYesA second start while the first runs is refused (409); poll the import instead.
createLifecycleEventNoEvery call records a new event. Remember which source record (for example the service order) you already reported, and look at listLifecycleEvents after a timeout before you send again.

The IETF is working on a general Idempotency-Key header (draft-ietf-httpapi-idempotency-key-header, draft 08, not yet an RFC). It is useful background for your own APIs; BatteryPass does not accept it on the operations above.

The outbox on the ERP side

The ERP must not lose a change while the middleware or BatteryPass is unreachable, and it must not call BatteryPass inside its own business transaction. The transactional outbox solves both: in the same database transaction that posts the goods receipt, the ERP (or an extension of it) writes a row to an outbox table. The middleware reads the outbox, calls BatteryPass, and marks the row done only after a successful answer.

This gives at-least-once delivery: a row may be sent twice (after a crash between call and marking), never zero times. Combine it with the retry rules above, and keep a small table of what you already sent (source record, BatteryPass id, time) so you can de-duplicate on your side.

Thin events, then fetch

Do not move data inside notifications. Both directions work the same way:

  • From the ERP: many ERPs send thin events (a product number and the kind of change). The middleware reads the current record from the ERP's API afterwards, checks the vendor's signature, handshake or secret URL, and drops events it cannot verify.
  • From BatteryPass: the change feed lists only {id, occurred_at, entity_type, entity_id, action}, never values, actors or personal data. The middleware reads the entity with the same key afterwards, so the feed reveals nothing on its own and is always current.

The feed returns a change only once every older transaction in BatteryPass has finished, so a stored cursor never skips a change that committed late. Store the cursor only after your side has processed the page. BatteryPass sends no outbound webhooks yet; they are planned and need a decision of their own (signing, subscription management, protection of target addresses). Until then, poll.

Stale writes

Mutable resources carry a row_version. updateImportMapping requires If-Match: "<row_version>"; when someone else changed the import in between, the answer is 409 conflict and nothing is overwritten. Read the import again and decide. Condition data has its own rule: an older measurement never replaces a newer one.

Data minimisation

The GDPR asks for data that is "adequate, relevant and limited to what is necessary" (Art. 5(1)(c)). For a battery passport that means data about the battery: serial number, model, dates, measurements, events. Customer names and addresses, order numbers, employee names, prices, vehicle and location data stay in the ERP. Each workflow lists what it sends and what it leaves out, and the overview lists what never flows.

Free text is where personal data slips in: an event description such as "repaired for Mr. X at Y" ends up in your battery's history. Fill descriptions from fixed codes or templates, not from free text of a service order.

The audit trail

  • Every write with a key is recorded in your organisation's audit log with the actor type api_key and the key's name: staging and validating an import, a condition update, a lifecycle event.
  • Imports staged with a key name the key instead of a member (created_by_api_key), in the API and in the portal.
  • Values written with a key do not carry the key themselves. A condition update creates a condition_update lifecycle event, and that event names the key (api_key).
  • Condition data is stored as approved measurements without a reviewer. Declared data takes the reviewed path: imported values are approved by a reviewer, and whether a second person must approve is your organisation's four-eyes setting.
  • Reads are not recorded. Assume that a leaked key's data was read.

OWASP API Security Top 10

How the OWASP API Security Top 10 (2023) risks that matter most for an ERP integration are handled, and what remains your part:

RiskIn BatteryPassYour part
API1:2023 Broken object level authorizationEvery query is filtered by the key's organisation; another organisation's id answers 404.Use one key per organisation you connect; never mix tenants in one middleware flow.
API4:2023 Unrestricted resource consumption600 requests per minute for all keys of an organisation, 30 attempts per 15 minutes for unknown keys, request bodies of 1 MiB, uploads of 10 MB and 10 000 rows, lists of at most 200 items.Back off on 429, page with limit and cursor, and send files in batches.
API5:2023 Broken function level authorizationEach operation names its scope (x-api-key-scope); a key without it gets 403, and portal operations do not accept keys at all (401).Give each key only the scopes its workflow lists.
API7:2023 Server side request forgeryBatteryPass does not call addresses that an integration supplies; there are no webhooks yet.When the middleware fetches URLs from ERP events, allow-list the targets.
API10:2023 Unsafe consumption of APIsBatteryPass validates every value with the registry's rules, whatever the source.Treat ERP events and BatteryPass answers as untrusted input: validate, check signatures where the vendor offers them, and never execute received content.

On this page