Machine API · v1
Two calls. One when a battery is built, one whenever something happens to it.
Your production line creates a passport per pack. Your test rig, your fleet telemetry and your service network append to its life history. That is the entire surface.
POST/api/v1/passports
A battery exists
Called once per pack, at end of line. Returns a serial and the URL its QR code points at.
POST/api/v1/passports/{serial}/events
Something happened to it
Health measurements, repairs, second life, recycling. Called any number of times, for years.
What lives where
Getting this wrong is the most common integration mistake, so it is worth thirty seconds.
The component — the design
The 37 regulatory fields: chemistry, carbon footprint, recycled content, material origin. Declared once, in the app, by you and your suppliers.
Never sent to this API. Every passport built from the component inherits them, and correcting one updates every battery at once.
The passport — one physical battery
Serial, build date, plant, tested capacity, and its own life history. This is what the API creates and appends to.
Build 400 packs today and you make 400 of these. They all share the component data beside it.
Authentication, and what a key can reach
Authorization: Bearer bp_live_…
Create keys in the app under API keys. The full key is shown once and stored only as a SHA-256 hash — if it is lost, revoke it and mint another.
A key cannot reach outside the organisation that issued it
Create passports for any component owned by your organisation.
Append events to your own passports, and to any active passport on the platform — that is how a workshop or recycler logs what they did.
Create a passport for another company's component. The component id is checked against the key's organisation, and a foreign id returns 404 — the same answer as an id that does not exist, so this cannot be used to probe which ones are real.
Read anybody's data. There is no GET on this API. Public data is on the QR page; everything else needs an account.
Reach an inactive passport that is not yours. Also a 404.
Scope a key to a single component when you create it and it is narrower still: one line that builds one battery gets a key that can do exactly one thing. Anything else is a 403.
Create passports
POST /api/v1/passports Authorization: Bearer bp_live_… Idempotency-Key: line3-2027-06-14-batch-0042 Content-Type: application/json
Body
| Field | Type | Notes |
|---|---|---|
component_id | uuid | Which design. Optional if the key is scoped to one component. |
count | integer | How many packs, 1–500. Serials are minted for you. |
prefix | string | Serial prefix when minting. Default BP-. |
units | array | Send this instead of count when the line has its own serials. |
defaults | object | Applied to every unit: plant, built_at, tested_kwh, health_pct. |
activate | boolean | Whether the QR codes go live on creation. Defaults to FALSE: a line knows a battery was built, not that it was sold, and the regulation keys the passport to being placed on the market. Units go live when that is recorded. Send true only if these ship direct — it has no effect unless the battery type is active. (`publish` is still accepted as the old name.) |
Simplest — let us mint the serials
{
"component_id": "bf5593ab-2c53-4c58-a3eb-8a9fc0e12a54",
"count": 40,
"defaults": { "plant": "Zwickau", "built_at": "2027-06-14T06:00:00Z" }
}Full — your serials, your end-of-line measurements
{
"component_id": "bf5593ab-2c53-4c58-a3eb-8a9fc0e12a54",
"defaults": { "plant": "Zwickau", "built_at": "2027-06-14T06:00:00Z" },
"units": [
{ "serial": "VW-77-000123", "tested_kwh": 76.8 },
{ "serial": "VW-77-000124", "tested_kwh": 76.6, "health_pct": 99.8 },
{ "serial": "VW-77-000125", "tested_kwh": 76.9, "built_at": "2027-06-14T14:20:00Z" }
]
}A unit's own value always beats defaults. Send either count or units — and if you send units, either give every one a serial or none of them.
Idempotency-Key is the one that matters
A line that loses its network mid-batch will retry. The same key with the same body replays the first response verbatim — no duplicate passports. The same key with a different body is rejected with 409, because that is almost always a bug at your end, and quietly guessing which batch was meant would be worse.
Record an event — including health
POST /api/v1/passports/{serial}/events
Authorization: Bearer bp_live_…
Content-Type: application/jsonThis is how health reaches a passport
There is deliberately no “set the health field” endpoint. A state-of-health number is only worth something if it arrives with a date and the name of whoever measured it — so it rides on an event, and a database trigger copies the newest reading onto the passport. The second-life buyer paying real money for “78%” is the person that protects.
Body
| Field | Type | Notes |
|---|---|---|
typerequired | string | One of the event types below. |
health_pct | number | 0–100. Becomes the passport's current health. |
detail | string | Plain language. Read by people, shown on the public page. |
occurred_at | ISO 8601 | When it actually happened. Defaults to now. |
Event types
Status is never set directly. What happened to the battery is the fact; the status is only a summary of it, so it follows from the event.
Fleet telemetry — a monthly reading
{ "type": "tested", "health_pct": 91.4, "detail": "BMS report, 42,180 km" }A workshop logging a repair
{
"type": "repaired",
"health_pct": 78.5,
"detail": "Module 4 replaced, pack rebalanced",
"occurred_at": "2031-03-02T09:15:00Z"
}Response
{
"recorded": { "id": "…", "type": "repaired", "occurred_at": "2031-03-02T09:15:00Z" },
"passport": {
"serial": "BP-000123",
"status": "in_service",
"health_pct": 78.5,
"version": 4,
"url": "https://your-domain/p/BP-000123"
}
}The passport block is read back from the database after the triggers have run, so it reports what actually happened rather than what we expected to happen.
Try it
This calls the live endpoint and writes real data. Use a key you can revoke.
Omitted unless you choose.
{
"count": 1,
"prefix": "BP-"
}curl -X POST /api/v1/passports \
-H "Authorization: Bearer bp_live_…" \
-H "Content-Type: application/json" \
-d '{"count":1,"prefix":"BP-"}'Errors
201Created.400Something in the body is wrong. The message says which.401Missing, unknown or revoked key.403The key is scoped to a different component.404Unknown component, unknown serial, or a passport that is neither yours nor active.409A serial already exists, or an idempotency key was reused for a different body.Every error is JSON with an error field written for a person reading a log at 3am, not a status code they have to go and look up.