Connect battery passport data to your systems.
REST API, CSV import and export, fixed passport addresses and all data fields. With a test key you can try everything without changing any data.
Overview
Four objects and four routes for exchanging data. A CSV file is enough to get started; the REST API is there for ongoing operation.
- CompanyYour account with users and plan
- ModelMaster data, once per battery model
- PassportOne battery with serial number and its own address
- EventRepair, repurposing or waste
| Route | What for | Status |
|---|---|---|
| CSV import | Take over serial numbers from ERP, MES or Excel | Available |
| CSV export | Bring passport IDs and addresses back into your systems | Available |
| Passport address and QR code | Create your own labels or link to passports | Available |
| REST API | Create models and passports automatically, without manual uploads | Available |
CSV import
AvailableThe usual route from your ERP: export a list and upload it to a model. Each new serial number gets a passport.
Material;Serialnummer;Fertigungsdatum;Charge;Werk;Status BX-48V;SN-0001;01.03.2027;L2027-03;Essen;frei BX-48V;SN-0002;01.03.2027;L2027-03;Essen;frei BX-48V;SN-0003;02.03.2027;L2027-03;Essen;frei
| Rule | Specification |
|---|---|
| Encoding | UTF-8 with or without BOM. Windows-1252 is detected and converted. |
| Delimiter | Semicolon, comma or tab. The first line decides. |
| Size | At most 8 MB and 50,000 rows per file. |
| Structure | One battery per row, no line breaks inside a field. |
| Header | Optional. With a header, columns can be in any order and extra columns are ignored. Without one, the order in the table below applies. |
| Column | Field | Recognised column names | Required | Rule |
|---|---|---|---|---|
| 1 | Serial number | seriennummer, serialnummer, serial, serial number, serialnumber, serial no, sn, serien-nr, seriennr, sernr | Yes | At most 120 characters: letters, digits, spaces and - _ . / : |
| 2 | Manufacturing date | herstelldatum, produktionsdatum, fertigungsdatum, manufacturing date, production date, date, datum | Optional | 2027-03-01, 01.03.2027, 1.3.2027, 01/03/2027, 1/3/2027 or 2027/03/01 |
| 3 | Batch | charge, chargennummer, batch, lot, los | Optional | At most 120 characters |
| 4 | Place of manufacture | herstellort, produktionsort, werk, place, plant, manufacturing place | Optional | At most 190 characters |
What happens on upload
- A preview appears first; nothing is saved yet.
- Faulty rows are reported with their line number and skipped.
- Duplicate serial numbers in the file and numbers already in this model are skipped.
- Passports are only created after you confirm, in a single operation. The import appears in the audit trail.
Check a file
AvailableCheck an export before you sign in. The same rules apply as for the import in the app.
The check runs only in your browser; the file is not uploaded. Whether a number already exists in the model is only shown by the preview in the app.
Material;Serialnummer;Fertigungsdatum;Charge;Werk;Status BX-48V;SN-0001;01.03.2027;L2027-03;Essen;frei BX-48V;SN-0002;01.03.2027;L2027-03;Essen;frei BX-48V;SN-0003;02.03.2027;L2027-03;Essen;freiCSV export
AvailableFor every model you can download the list of all passports, as UTF-8 with BOM and semicolon-separated. That brings passport IDs and addresses back into ERP, MES or label printing. The download requires signing in.
Seriennummer;Pass-ID;Pass-URL;Herstelldatum;Charge;Herstellort;Status SN-0001;7KM2XQ9RTA;https://app.batterypass2027.eu/p/7KM2XQ9RTA;2027-03-01;L2027-03;Essen;Original
Passport address and QR code
AvailableEvery passport has a fixed address. That is all a QR code, a link from a manual or an ERP entry needs.
https://app.batterypass2027.eu/p/{PASS-ID}
https://app.batterypass2027.eu/p/{PASS-ID}?lang=en- The passport ID has 10 characters and avoids easily confused ones such as 0, O, 1, I and L.
- The QR code contains only this address. If the data changes, it stays valid.
- In the app, the code is available per passport as SVG and PNG, along with label sheets for printing.
- Without signing in, the passport page shows only the public data. Search engines are asked not to index it.
- With a GTIN set, there is also a GS1 Digital Link.
Create your own labels from the export
# pip install "qrcode[pil]"
import csv
import qrcode
with open("paesse_BX-48V.csv", encoding="utf-8-sig", newline="") as f:
for row in csv.DictReader(f, delimiter=";"):
qrcode.make(row["Pass-URL"]).save(f'{row["Pass-ID"]}.png')
Data fields
AvailableThese 38 fields exist in the app today, in simplified form based on Annex XIII. They are maintained in the model form. The categories are lmt, industrie, ev, sli and geraet.
General information6
| Field | Technical name | Type | Visible to | Required |
|---|---|---|---|---|
| Manufacturer (name) | manufacturer_name | text | Public | All passport categories |
| Manufacturer (address) | manufacturer_address | text | Public | All passport categories |
| Contact (email or website) | manufacturer_contact | text | Public | All passport categories |
| Place of manufacture (default) | manufacturing_place | text | Public | All passport categories |
| Weight | weight_kg | number · kg | Public | All passport categories |
| Warranty / guarantee | warranty | text | Public | Optional |
Materials & substances6
| Field | Technical name | Type | Visible to | Required |
|---|---|---|---|---|
| Battery chemistry | chemistry | text | Public | All passport categories |
| Hazardous substances | hazardous_substances | text | Public | All passport categories |
| Critical raw materials | critical_raw_materials | text | Public | All passport categories |
| Cathode composition | composition_cathode | text | Legitimate interest | All passport categories |
| Anode composition | composition_anode | text | Legitimate interest | All passport categories |
| Electrolyte composition | composition_electrolyte | text | Legitimate interest | All passport categories |
Performance & durability11
| Field | Technical name | Type | Visible to | Required |
|---|---|---|---|---|
| Rated capacity | rated_capacity_ah | number · Ah | Public | All passport categories |
| Nominal voltage | nominal_voltage_v | number · V | Public | All passport categories |
| Minimum voltage | min_voltage_v | number · V | Public | All passport categories |
| Maximum voltage | max_voltage_v | number · V | Public | All passport categories |
| Rated energy | rated_energy_kwh | number · kWh | Public | All passport categories |
| Power (original) | rated_power_w | number · W | Public | industrie, ev |
| Expected lifetime | expected_lifetime_cycles | number · Zyklen | Public | All passport categories |
| Temperature range from | temp_min_c | number · °C | Public | All passport categories |
| Temperature range to | temp_max_c | number · °C | Public | All passport categories |
| Energy efficiency (round trip) | round_trip_efficiency_pct | number · % | Public | industrie, ev |
| Internal resistance | internal_resistance_mohm | number · mΩ | Public | Optional |
Carbon footprint & recycled content6
| Field | Technical name | Type | Visible to | Required |
|---|---|---|---|---|
| Carbon footprint | carbon_footprint | number · kg CO₂e/kWh | Public | Applies later |
| Recycled cobalt content | recycled_cobalt_pct | number · % | Public | Applies later |
| Recycled lithium content | recycled_lithium_pct | number · % | Public | Applies later |
| Recycled nickel content | recycled_nickel_pct | number · % | Public | Applies later |
| Recycled lead content | recycled_lead_pct | number · % | Public | Applies later |
| Share of renewable content | renewable_content_pct | number · % | Public | Optional |
Conformity & labelling4
| Field | Technical name | Type | Visible to | Required |
|---|---|---|---|---|
| EU declaration of conformity (link) | doc_url | url | Public | All passport categories |
| Labels & symbols | labels_symbols | text | Public | All passport categories |
| Due diligence report (link) | due_diligence_url | url | Public | Applies later |
| Return & disposal | end_of_life_info | text | Public | All passport categories |
Dismantling, repair & spare parts4
| Field | Technical name | Type | Visible to | Required |
|---|---|---|---|---|
| Disassembly instructions | dismantling_info | text | Legitimate interest | All passport categories |
| Component part numbers | part_numbers | text | Legitimate interest | All passport categories |
| Spare parts source | spare_parts_contact | text | Legitimate interest | All passport categories |
| Safety measures | safety_measures | text | Legitimate interest | All passport categories |
Test reports1
| Field | Technical name | Type | Visible to | Required |
|---|---|---|---|---|
| Test reports (links or references) | test_reports | text | Authorities and test bodies | All passport categories |
REST API
AvailableCreate models and passports directly from ERP, MES or BMS and report events. JSON over HTTPS, authentication by key. Base address: https://app.batterypass2027.eu/api/v1
Get started in three steps
- Sign in as an administrator and open Settings, then API keys.
- Create a test key. It is shown only once, so keep it like a password.
- Check the connection with GET /me. If the API responds, the key and network path are correct.
export BATTERYPASS_KEY="bp_test_..." curl -s https://app.batterypass2027.eu/api/v1/me \ -H "Authorization: Bearer $BATTERYPASS_KEY"
One key, one company
The key determines which data can be reached. You create it in the app and it is shown only once.
Test keys save nothing
A test key checks each request as in live operation but changes no data and uses no quota.
Safe to retry
With the same Idempotency-Key, no second record is created even after an interruption. The document number from your ERP works well for this.
Passports are not deleted
The API has no DELETE for passports. The end of a battery’s life is reported as an event.
Test keys respond with status 200 and dry_run: true. They read real data but change nothing.
Endpoints
| Method | Path | What for |
|---|---|---|
| GET | /me | Connection test: company, plan, rights and remaining quota |
| GET | /fields | Field catalogue with requirements per category |
| GET | /models | List models, also by model code |
| POST | /models | Create a model |
| GET | /models/{id} | Read one model with data and missing required fields |
| PATCH | /models/{id} | Change master data |
| GET | /passports | List passports, also by serial number or change date |
| POST | /passports/batch | Create up to 1,000 passports in one call |
| GET | /passports/{id} | Read one passport with address and status |
| GET | /passports/{id}/events | Read a passport’s events |
| POST | /passports/{id}/events | Report an event, such as repair or waste |
| POST | /models/validate | Check a draft, save nothing |
| POST | /passports | Create one passport |
| PATCH | /passports/{id} | Change date, batch or place |
| GET | /passports/{id}/components | Read components |
| POST | /passports/{id}/components | Install a component |
| PATCH | /passports/{id}/components/{cid} | Change or remove a component |
| POST | /passports/{id}/components/{cid}/replace | Replace a component |
| GET | /passports/{id}/telemetry | Read measurement series |
| POST | /passports/{id}/telemetry | Report measurements |
| GET | /certificates | List evidence |
| GET | /certificates/{id} | Evidence with download link |
| GET | /suppliers | List suppliers |
| GET | /supplier-requests | List data requests |
| POST | /supplier-requests | Send a supplier request |
| GET | /supplier-requests/{id} | Read a request with supplied values |
| POST | /supplier-requests/{id}/accept | Accept supplied values into the model |
| GET | /audit | Read the audit trail |
Example: create passports from the ERP
curl -X POST https://app.batterypass2027.eu/api/v1/passports/batch \
-H "Authorization: Bearer $BATTERYPASS_KEY" \
-H "Idempotency-Key: erp-4711" \
-H "Content-Type: application/json" \
-d '{
"model_code": "BX-48V",
"on_conflict": "skip",
"items": [
{ "serial": "SN-0001", "manufactured_at": "2027-03-01", "batch": "L2027-03" },
{ "serial": "SN-0002", "manufactured_at": "2027-03-01", "batch": "L2027-03" }
]
}'
import os
import requests
res = requests.post(
"https://app.batterypass2027.eu/api/v1/passports/batch",
headers={
"Authorization": f"Bearer {os.environ['BATTERYPASS_KEY']}",
"Idempotency-Key": "erp-4711",
},
json={
"model_code": "BX-48V",
"on_conflict": "skip",
"items": [{"serial": "SN-0001", "manufactured_at": "2027-03-01"}],
},
timeout=30,
)
res.raise_for_status()
print(res.json()["created"])
const res = await fetch("https://app.batterypass2027.eu/api/v1/passports/batch", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BATTERYPASS_KEY}`,
"Idempotency-Key": "erp-4711",
"Content-Type": "application/json",
},
body: JSON.stringify({
model_code: "BX-48V",
on_conflict: "skip",
items: [{ serial: "SN-0001", manufactured_at: "2027-03-01" }],
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
console.log((await res.json()).created);
<?php
$ch = curl_init("https://app.batterypass2027.eu/api/v1/passports/batch");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("BATTERYPASS_KEY"),
"Idempotency-Key: erp-4711",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"model_code" => "BX-48V",
"on_conflict" => "skip",
"items" => [["serial" => "SN-0001", "manufactured_at" => "2027-03-01"]],
]),
]);
echo curl_exec($ch);
Response
HTTP/1.1 201 Created
X-Request-Id: req_4f7a91c2d03e8b56
{
"created": 1,
"skipped": 1,
"skipped_serials": ["SN-0002"],
"items": [
{
"id": "7KM2XQ9RTA",
"serial": "SN-0001",
"status": "original",
"url": "https://app.batterypass2027.eu/p/7KM2XQ9RTA"
}
]
}
Errors
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "invalid_serial",
"message": "Serial number is missing or contains invalid characters.",
"field": "items[3].serial",
"request_id": "req_a03c5e17b92d4f68"
}
}
| HTTP | Code | Meaning |
|---|---|---|
400 | invalid_request | Request is not valid JSON or contains unknown fields |
401 | unauthorized | Key is missing or invalid |
403 | insufficient_scope | The key lacks the required right |
403 | plan_required | Live keys require the Pro or Enterprise plan |
403 | email_not_verified | The company’s email address is not confirmed yet |
403 | quota_exceeded | The plan’s quota is used up |
404 | not_found | Object does not exist or belongs to another company |
409 | duplicate_serial | Serial number already exists in this model (with on_conflict: fail) |
409 | duplicate_model | The model code is already taken |
409 | idempotency_conflict | The Idempotency-Key was used for a different request |
413 | payload_too_large | The request is larger than 2 MB |
422 | invalid_serial | A serial number is invalid; the response names the field. Nothing is saved. |
422 | invalid_date | A date does not match the format YYYY-MM-DD |
422 | out_of_range | A measurement is outside the allowed range |
422 | invalid_gtin | The GTIN has a wrong length or check digit |
422 | validation_failed | Another value breaks the rules |
429 | rate_limited | Too many requests; retry after Retry-After |
500 | internal_error | An error on our side. Safe to retry with the same Idempotency-Key. |
Limits
| Requests per minute and key | Live 300, test 60 |
| Passports per call | 1.000 |
| Request size | 2 MB |
| Entries per list page | 100, at most 500 |
| Active keys per company | 10 |
| Live keys | Pro and Enterprise. Test keys are available in every plan. |
Planning more than 100,000 passports a year? Please talk to us in advance.
Permissions
Each key only gets the rights the system needs. If one is missing, the API responds with 403 and insufficient_scope. In the app you choose a template: All rights, ERP, BMS or Read only.
| Right | Allows |
|---|---|
models:read | Read models and check drafts |
models:write | Create and change models |
passports:read | Read passports, events and components |
passports:write | Create and change passports, report events, install and replace components |
telemetry:read telemetry:write | Read and report measurements |
certificates:read | Read and download evidence |
suppliers:read suppliers:write | Read suppliers and data requests, send and accept requests |
audit:read | Read the audit trail |
Requests and responses
Lists come in pages with data, has_more and next_cursor. Pass the value of next_cursor as cursor. Unknown fields are rejected so typos show up immediately. With PATCH, a missing field stays unchanged and null clears it.
| Header | Meaning |
|---|---|
Idempotency-Key | Retry without a second record, valid for 7 days. The ERP document number works well. |
X-Batterypass-Dry-Run: true | Even a live key only checks this request and saves nothing. |
X-Request-Id | ID of the call, also in errors and in the app’s log |
X-RateLimit-Limit X-RateLimit-Remaining | Quota for the current minute |
Idempotent-Replayed | true when a stored response was replayed |
SDK and API console
AvailableTwo lean clients without dependencies: they retry requests safely, page through lists themselves and split large volumes into calls of 1,000 passports. Both are single files to copy into your project. The API console is in the app under Settings, API keys.
from batterypass import Batterypass
bp = Batterypass("bp_test_...")
print(bp.me()["organization"]["plan"])
items = [{"serial": f"SN-{i:06}"} for i in range(5000)]
print(bp.create_passports_all("BX-48V", items))
for passport in bp.list_all("/passports", model_code="BX-48V"):
print(passport["serial"], passport["url"])
import { Batterypass } from "./batterypass.mjs";
const bp = new Batterypass({ apiKey: process.env.BATTERYPASS_KEY });
console.log((await bp.me()).organization.plan);
const items = Array.from({ length: 5000 }, (_, i) => ({ serial: `SN-${i}` }));
console.log(await bp.createPassportsAll("BX-48V", items));
for await (const passport of bp.listAll("/passports", { model_code: "BX-48V" })) {
console.log(passport.serial, passport.url);
}
Components
AvailableRecord which modules, cells or BMS are installed in a battery. A serial number can only be installed once at a time per passport. On replacement, the old part stays on record with its removal date.
curl -X POST https://app.batterypass2027.eu/api/v1/passports/7KM2XQ9RTA/components \
-H "Authorization: Bearer $BATTERYPASS_KEY" -H "Content-Type: application/json" \
-d '{"kind": "module", "label": "Modul 1", "serial": "M-0001", "supplier": "Zellwerk GmbH"}'
curl -X POST https://app.batterypass2027.eu/api/v1/passports/7KM2XQ9RTA/components/12/replace \
-H "Authorization: Bearer $BATTERYPASS_KEY" -H "Content-Type: application/json" \
-d '{"serial": "M-0002", "note": "Modul nach Defekt getauscht"}'
Allowed types are pack, rack, module, cell, bms and other.
Measurements
AvailableReport data from the BMS, individually or up to 1,000 measurements with items. Read the series with since and until. Measurements are available from the Pro plan. They appear in the app, not on the public passport page.
curl -X POST https://app.batterypass2027.eu/api/v1/passports/7KM2XQ9RTA/telemetry \
-H "Authorization: Bearer $BATTERYPASS_KEY" -H "Content-Type: application/json" \
-d '{"recorded_at": "2027-03-01T10:00:00Z", "soh_pct": 94.5, "cycle_count": 120}'
| Field | Range |
|---|---|
recorded_at | ISO 8601 |
soh_pct | 0–100 |
soc_pct | 0–100 |
capacity_kwh | ≥ 0 |
remaining_capacity_ah | ≥ 0 |
internal_resistance_mohm | ≥ 0 |
cycle_count | ≥ 0, integer |
temp_min_c temp_max_c | ≥ −273 |
energy_throughput_kwh | ≥ 0 |
negative_event | deep_discharge, overheat, accident |
note | max. 500 |
Evidence
AvailableUpload test reports, declarations of conformity and similar documents in the app on the model: PDF, PNG or JPG up to 10 MB. Through the API you read the list and get a download link valid for 15 minutes. Each file has a SHA-256 checksum.
curl -s "https://app.batterypass2027.eu/api/v1/certificates?model_code=BX-48V" -H "Authorization: Bearer $BATTERYPASS_KEY" curl -s https://app.batterypass2027.eu/api/v1/certificates/42 -H "Authorization: Bearer $BATTERYPASS_KEY"
Suppliers and data requests
AvailableRequest values from suppliers. They get an email with a link valid for 30 days and enter only the requested fields, without an account. Supplied values only reach the model after your approval. Available from the Starter plan, at most 20 requests per hour.
curl -X POST https://app.batterypass2027.eu/api/v1/supplier-requests \
-H "Authorization: Bearer $BATTERYPASS_KEY" -H "Content-Type: application/json" \
-d '{"model_code": "BX-48V", "supplier_name": "Zellwerk GmbH", "supplier_email": "daten@zellwerk.example",
"fields": ["chemistry", "composition_cathode"], "message": "Bitte bis Monatsende"}'
curl -s "https://app.batterypass2027.eu/api/v1/supplier-requests?status=submitted" -H "Authorization: Bearer $BATTERYPASS_KEY"
curl -X POST https://app.batterypass2027.eu/api/v1/supplier-requests/7/accept -H "Authorization: Bearer $BATTERYPASS_KEY"
GS1 Digital Link
AvailableSet a GTIN with a valid check digit on the model. The GS1 address from GTIN and serial number then leads to the passport page and appears in the API as gs1_link. Your QR codes still contain the fixed passport address, but you can create your own codes from the GS1 link. You get the GTIN itself from GS1.
https://app.batterypass2027.eu/01/{GTIN}/21/{SERIENNUMMER}
https://app.batterypass2027.eu/01/04012345678901/21/SN-0001
Audit trail
AvailableGET /audit returns all logged changes of the company, in pages and optionally from a point in time with since. Changes via the API carry the key’s name. In the app, administrators also see the latest calls per key.
Versions
The major version is in the path. Within v1, only fields and endpoints are added. Planned but not yet available: webhooks, access for authorities and recyclers, and a verifiable change chain.
| Version | Date | Change |
|---|---|---|
1.1.0 | 2026-11-06 | Components, measurements, evidence, suppliers, audit, single passport, GS1, granular rights, dry-run header, OpenAPI and health endpoint |
1.0.0 | 2026-11-06 | Models, passports, events, keys, test mode, idempotency |
Frequently asked questions
Short answers for your integration.
How do I test without creating real passports?
With a test key. It checks everything as in live operation and saves nothing.
What should I do after a timeout?
Send the same request again with the same Idempotency-Key. No second record is created.
How do I find the passport for a serial number?
With GET /passports?serial=… and model_code if needed.
Can I delete passports via the API?
No. Report the end of life as an event with the status waste.
How many passports are possible?
1,000 per call, 300 calls per minute. Your plan and our hosting also set limits. If you plan more than 100,000 passports a year, please talk to us in advance.
Try the import with your own file.
The Pilot plan is free and covers one model and 25 passports per year.