Maintain models, passports, lifecycle events, telemetry and evidence documents from your own systems. JSON over HTTPS, scoped keys, a test mode that stores nothing.
There is only version 1 today. Older versions become selectable here once they exist.
Overview
The API models the same objects as the dashboard: a model is the master-data level of a product, a passport belongs to exactly one physical battery. Events and measurements hang off the passport. Everything runs through one base URL and every response is JSON, errors included.
One key, one company
The key determines the company. Foreign identifiers return 404, not 403 - there is no way to infer other companies' data from a key.
No DELETE
Battery passports are subject to retention. Version 1 knows GET, POST and PATCH. End of life is reported as an event.
Server-owned fields
Readiness, battery status, short code and registry state are computed by the server. Those fields are readable but not writable.
Same rules as the dashboard
Quotas, mandatory fields and roles apply identically. There is no way to create something through the API that the dashboard would forbid.
Step 1: create a key
API keys are created in the dashboard only, and only by an account with the admin role. The owner always has admin.
Open Integrations
In the dashboard, left navigation, under Administration. Only admins see this entry.
Create an API key
In the API access panel press Create API key. Give it a label that names the system, for example SAP-North-Prod.
Pick mode and scopes
Test for development, live for production. Then a preset or individual scopes, and optionally an expiry of 30, 90 or 365 days.
Store the plaintext once
The key is shown exactly once. Only a SHA-256 hash is stored, so nobody can read it back later, ourselves included.
AdministrationIntegrations
API accessREST API v1 for ERP, MES, PLM and BMS telemetryTest modeAPI consoleOpenAPI
3 · Copy the plaintext once - nobody can show it again
Sign in and create a key Opens the dashboard directly in the Integrations area. Without a session you land on the login page first and are taken there automatically afterwards.
Treat the key like a password: not in repositories, not in the front end, not in logs. If it leaks, rotate it in the dashboard - the old key stops working immediately and the new one is shown once.
Step 2: first call
GET /me is the connection test. The response reports company, plan, mode, effective scopes, rate limit and the remaining quota for the current contract year. If this call works, key, headers and network path are correct.
The Integrations page links to the API console. Pick an endpoint from the OpenAPI description, fill in parameters, send with your session instead of a key and read the response as a collapsible JSON tree. Every call can be copied as cURL straight into a terminal. Write calls run as a dry run there by default.
Authentication
Every request carries the Authorization header with the Bearer scheme. There are two kinds of key, told apart by their prefix.
Key
Shape
Purpose
Live
bpw_live_ + 48 hex characters
Production, actually writes
Test
bpw_test_ + 48 hex characters
Development and sign-off, never persists
The API also accepts the session token of a signed-in dashboard user; scopes then follow from the role. That is how the API console works and is not intended for integrations. A missing header or an invalid key returns 401 with WWW-Authenticate. After 30 failed attempts per IP and minute the API answers 429.
Live, test and dry run
There is deliberately no separate sandbox database. A test key works on your real master data and checks scopes, mandatory fields, serials and quotas exactly as in production, but stores nothing.
Live key
Test key
Reading
real data
real data
Writing
persisted, 201
fully validated, 200 with dry_run: true
Quota
consumed
checked only
Log
yes
yes, marked as dry run
Rate limit
600 / minute
60 / minute
A live key can send a single request as a trial too: header X-BPW-Dry-Run: true. The response headers X-BPW-Mode and X-BPW-Dry-Run always state what actually applied.
Scopes
Every key carries exactly the permissions the calling system needs. If one is missing the route returns 403 with the code insufficient_scope.
Scope
Allows
models:read
Read models, validate a draft
models:write
Create and change models
passes:read
Read passports and events, including public_url, gs1_link and short_url for your own QR codes
passes:write
Create and change passports, report events
telemetry:read
Read the telemetry time series per passport (since 1.1.0)
telemetry:write
Report measurements
certificates:read
Read and download evidence documents
suppliers:read
Read suppliers, data requests and delivered values (since 1.1.0)
suppliers:write
Invite suppliers (since 1.1.0)
audit:read
Read the audit trail (since 1.1.0)
GET /me, GET /field-catalog and GET /openapi.json need no scope. Dashboard presets: read only (every read scope), ERP sync, BMS telemetry (read and report), all, custom; the dashboard explains the endpoints each scope unlocks. At most ten active keys per company; every action on a key is recorded in the audit trail with person and timestamp.
Plans
The check runs on every single call, not just when the key is created. A plan change takes effect immediately: after moving from Enterprise to Pro a live key keeps telemetry:read and telemetry:write only.
Plan
Test key
Live key
Pilot, Starter
yes, full functionality
no
Pro
yes
telemetry:read and telemetry:write only
Enterprise
yes
all scopes
Archive, Lifetime
yes, read only
no
In retention plans every write route returns 403 with tenant_read_only. Telemetry additionally requires the Pro plan or higher, independent of the key mode.
Requests and responses
Single objects come back as a JSON object, lists inside an envelope with data, has_more and next_cursor.
Time
Timestamps are ISO 8601 in UTC with milliseconds, calendar dates are YYYY-MM-DD. Input is normalised; a date like 2026-02-30 is rejected.
null versus omitted
On POST and PATCH an omitted field stays unchanged, null clears the field. Empty strings become null.
Unknown fields
Are rejected, not ignored. A typo in the ERP surfaces immediately instead of silently losing data.
Identifiers
Lower-case UUIDs. The usual route from an ERP record to an object is GET /passes?serial=… or GET /models?code=…
Charset and caching
UTF-8, Content-Type application/json. Every response carries Cache-Control: no-store.
Response headers
Header
Meaning
X-Request-Id
Identifier of the call, also present in every error body and in the dashboard API log.
X-RateLimit-Limit / -Remaining / -Reset
Budget of the current window, reset as Unix seconds.
X-BPW-Mode
live or test, depending on the key used.
X-BPW-Dry-Run
true when this call stored nothing.
Idempotent-Replayed
true when a stored response was replayed.
Retry-After
Only on 429: wait time in seconds.
Errors
Every error uses the same envelope. type groups roughly by HTTP status, code is stable and meant for program logic, message is English and may change. param names the first offending field, inside a batch for example items[3].serial, details lists them all. Retry 429 and 5xx with backoff, never retry 4xx automatically.
400 Bad Request
HTTP/1.1400 Bad Request
X-Request-Id: req_7f3c9a21e4b84c60
{
"error": {
"type": "invalid_request",
"code": "validation_failed",
"message": "One or more fields are invalid.",
"param": "energy_kwh",
"details": [
{ "param": "energy_kwh", "code": "invalid_type", "message": "energy_kwh must be a number." },
{ "param": "gtin", "code": "invalid_gtin", "message": "gtin must be a valid GS1 GTIN." }
],
"request_id": "req_7f3c9a21e4b84c60"
}
}
HTTP
code
When
400
unknown_field
Field is not part of the schema. details names every unknown field.
400
validation_failed
Type errors in model fields, details lists every affected field.
400
missing_field
A required field is missing, for example code, model_id or serial.
400
invalid_enum
Value is not in the allowed list.
400
invalid_serial
Serial violates the GS1 AI 21 character set or is too long.
400
invalid_gtin
The GTIN check digit is invalid.
400
invalid_date / invalid_timestamp
Not a valid calendar date or not an ISO 8601 timestamp.
400
duplicate_serial_in_batch
Two entries in the same batch carry the same serial.
400
out_of_range / no_measurement
Telemetry value out of range, or no measurement supplied at all.
The object does not exist inside your company. Foreign identifiers also return 404.
404
route_not_found
The path does not exist. Typo or a missing /v1.
405
method_not_allowed
Method not allowed for this path, the Allow header names the allowed ones.
409
duplicate_code / duplicate_serial
Model code or serial already exists inside your company.
409
limit_reached
The plan quota is reached. GET /me shows the current state.
422
idempotency_key_reused
Same Idempotency-Key but a different payload.
429
rate_limit_exceeded
Window exhausted. Retry-After gives the wait in seconds.
500
internal_error
Unexpected error. Please report the request_id.
Pagination
Lists return at most 200 entries per page, 50 by default. Sorting is newest first by creation time, telemetry by measurement time. The cursor is opaque and works on timestamp and identifier rather than offsets: records created during a run neither shift nor skip anything. For incremental reconciliation remember the largest updated_at of the last page and send it as updated_since next time.
pagination.py
import requests
BASE = "https://app.batteriepasswerk.com/api/v1"
H = {"Authorization": f"Bearer {KEY}"}
def iterate(path, **params):
cursor = NonewhileTrue:
r = requests.get(f"{BASE}{path}", headers=H,
params={**params, "limit": 200, "cursor": cursor})
r.raise_for_status()
page = r.json()
yieldfrom page["data"]
ifnot page["has_more"]:
return
cursor = page["next_cursor"]
# Inkrementell: nur was sich seit dem letzten Lauf geändert hatfor p in iterate("/passes", updated_since="2026-09-01T00:00:00Z"):
print(p["serial"], p["lifecycle_status"])
Rate limits
Fixed windows of 60 seconds. Every response reports the remaining budget; exceeding it returns 429 with Retry-After. A batch of 500 passports counts as one request, so mass serialisation is rarely the constraint.
Caller
Limit
Live key
600 requests per minute
Test key
60 per minute
Console session
120 per minute
Failed authentications
30 per minute and IP
GET /openapi.json
60 per minute and IP
Idempotency
Every POST accepts the Idempotency-Key header with up to 255 characters, for example the document number from the ERP.
First execution: normal processing, the response is stored for 24 hours under company, key and idempotency key.
Retry with an identical request: the stored response plus the header Idempotent-Replayed: true, without executing again.
Retry with a different payload: 422 with the code idempotency_key_reused.
Only 2xx and 4xx are stored. After 429 or 5xx you may retry with the same key.
# Erster Versuch läuft in einen Timeout - Ergebnis unbekanntcurl-X POST https://app.batteriepasswerk.com/api/v1/passes \
-H"Authorization: Bearer $BPW_API_KEY" \
-H"Idempotency-Key: los-2026-09-0042" \
-H"Content-Type: application/json" \
-d'{ "items": [ … ] }'# Gefahrlose Wiederholung mit demselben Schlüssel# → 201 mit derselben Antwort, zusätzlich: Idempotent-Replayed: true
Account and catalog
Eighteen endpoints. Each names the required scope, its parameters and a complete example. All paths are relative to the base URL.
GET/meScope: none
Connection test and self-information
Returns company, plan, mode, effective scopes, rate limit and the quota for the current contract year. The first call of every integration.
Every mandatory-field rule with the numbers of the 71 official EU data points, the legal basis, applicability and the API fields that satisfy it. This is how an ERP maps the data points onto its own fields.
The body carries the writable model fields (see the field reference). Unknown fields are rejected. Returns 201 with the record and the current quota; with a test key 200 with dry_run and id: null.
Parameters
Name
In
Type
Description
code *
body
string
Model code, unique per company.
category
body
lmt | bess | ind | ev | device | sli
Determines which mandatory fields apply.
gtin
body
string
GS1 GTIN with a valid check digit, drives the public address.
second_life
body
boolean
Second-life exemption under Art. 7(5) and 8(4).
applicability_flags
body
object of boolean
Declared non-applicability per rule of the field catalog.
Stateless: returns the readiness a model with these values would have and lists every missing field. Ideal as a pre-check in the ERP before anything is written.
One object creates one passport, items with up to 500 entries creates a batch. A batch counts as one request against the rate limit. Duplicate serials inside a batch are always an error.
Parameters
Name
In
Type
Description
model_id *
body
uuid
Must be a model of your company.
serial *
body
string, max. 20
GS1 AI 21 character set, unique per company.
status
body
ready | review | pending | crit
Defaults to pending.
batch
body
string, max. 100
Batch or lot.
production_date
body
YYYY-MM-DD
Production date, EU data point 9.
on_conflict
body
error | skip
Batch only: skip ignores existing serials instead of failing.
Every data request sent to a supplier with its status (invited, progress, delivered, expired), the requested fields, due date and reminders. Newest first.
Parameters
Name
In
Type
Description
limit
query
integer 1-200
Page size, default 50.
cursor
query
string
Opaque cursor from next_cursor of the previous page.
Creates the supplier (matched by e-mail) and a data request for one of your models, then e-mails the password-less self-service link (valid 60 days). The link appears only in this response. Starter plan or higher, at most 60 invitations per hour and company. Dry run validates everything, creates nothing and sends nothing.
Parameters
Name
In
Type
Description
supplier_name *
body
string
Company name of the supplier, up to 200 characters.
email *
body
e-mail
Recipient of the invitation, identifies the supplier within your company.
Every write in your company as recorded by the database: who (user, API key or system), what (action such as pass.create, object, reference) and when. seq and row_hash belong to the per-company hash chain and make tampering evident. Newest first.
Parameters
Name
In
Type
Description
limit
query
integer 1-200
Page size, default 50.
cursor
query
string
Opaque cursor from next_cursor of the previous page.
entity_type
query
model | pass | cert | supplier | member | api_key
Object type.
action
query
string
Exact action, e.g. pass.create, model.update, supplier.invite.
All writable fields of a model in snake_case, identical to the naming in the dashboard. The EU column gives the number of the official EU data point where the field maps to one. Strings are capped at 4000 characters. Read-only: id, readiness, readiness_detail, created_at, updated_at and public_url.
Writable on creation: model_id, serial, status, batch and production_date; afterwards status, batch and production_date via PATCH. Everything else belongs to the server.
Field
Meaning
lifecycle_status
Legal battery status, derived from the event chain (EU 67).
retired_at
Set as soon as recycled or eol was reported.
state_of_health_pct
Latest reported telemetry value.
short_code, short_url
Short link of the public passport page.
public_url, gs1_link
GS1 Digital Link once the model has a GTIN, otherwise the identifier address.
The API is pull, webhooks are push. Signed events reach your system the moment they happen: pass.created, model.updated, supplier.delivered and cert.expiring. Signed with HMAC-SHA256, retried on failure. Proven pattern: take the webhook as the trigger and then load the affected resource through the API, so the truth always comes from the API. Calls through the API fire the same webhooks as dashboard entries.
Versioning and changelog
The version lives in the path. Within v1 only fields and endpoints are added; existing fields, error codes and semantics stay. If anything is removed we announce it at least twelve months ahead here, in the OpenAPI document and by email to the admins, and run the successor in parallel under /v2.
Version
Date
Change
1.1.0
2026-09-09
New scopes telemetry:read, suppliers:read, suppliers:write and audit:read. New endpoints GET /suppliers, GET and POST /supplier-requests, GET /supplier-requests/{id}, GET /audit. Existing keys with passes:read were extended with telemetry:read automatically.
1.0.0
2026-09-09
First release: models, passports, events, telemetry, evidence documents, field catalog, test keys with dry run, idempotency key, cursor pagination.
Frequently asked questions
Is the REST API available today?
Yes. Version 1.1.0 is in production: models, passports, lifecycle events, telemetry, evidence documents, supplier requests, the audit trail and the field catalog. The full interface for ERP and MES belongs to the Enterprise plan, the BMS telemetry interface is included from Pro, and a fully functional test key is available in every plan, including the free Pilot.
How do I test without creating real passports?
With a test key. It validates every request in full against your real master data, scopes, mandatory fields and quotas and returns the result a live call would produce, without storing anything. There is deliberately no separate sandbox database of invented data that drifts away from reality over time.
Who is allowed to create an API key?
Only accounts with the admin role, which always includes the owner. The rule is enforced on three levels: in the database, in the endpoint and in the interface. Every action on a key is recorded in the audit trail with person and timestamp.
Which programming language do I need?
Any language that speaks HTTPS and JSON. There is no SDK you have to adopt. From the OpenAPI 3.1 document, common generators will produce a typed client for Java, C#, Python, TypeScript or Go if you want one.
How do I find the passport id for a serial number?
Through GET /passes with the serial parameter. Serial numbers are unique per company, so there is at most one hit. Many integrations resolve the id once and store it next to the serial in their own system.
What happens if a request times out?
Retry it with the same Idempotency-Key. If the first request was processed you receive the stored response instead of a duplicate. If the payload differs under the same key, the API rejects the request rather than quietly doing something else.
Can I delete a passport through the API?
No, by design. Battery passports are subject to retention obligations, so version 1 has no delete operation. The end of life is reported as an event, for example recycling, after which the passport shows its final state.
How many passports can I create?
The quota depends on the plan and is reported in every GET /me response under usage. The enforcement field says what happens when you exceed it: hard means rejection, overage means billing per additional passport, contract means an agreed volume without a block.
Questions about your integration?
In an intro call we clarify which data comes from which system, which scopes your keys need and how serialisation fits your production line.