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.

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

FieldTypeNotes
component_iduuidWhich design. Optional if the key is scoped to one component.
countintegerHow many packs, 1–500. Serials are minted for you.
prefixstringSerial prefix when minting. Default BP-.
unitsarraySend this instead of count when the line has its own serials.
defaultsobjectApplied to every unit: plant, built_at, tested_kwh, health_pct.
activatebooleanWhether 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/json

This 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

FieldTypeNotes
typerequiredstringOne of the event types below.
health_pctnumber0–100. Becomes the passport's current health.
detailstringPlain language. Read by people, shown on the public page.
occurred_atISO 8601When it actually happened. Defaults to now.

Event types

placed_on_market→ in serviceinspectedrepairedtestedthe usual one for healthfirmware_updatedentered_second_life→ second lifereceived_for_recycling→ second liferecycled→ recycleddecommissioned→ scrapped

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.

Request body
{
  "count": 1,
  "prefix": "BP-"
}
As curl
curl -X POST /api/v1/passports \
  -H "Authorization: Bearer bp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"count":1,"prefix":"BP-"}'
Paste a key to enable this.

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.