# Record lifecycle events

> Report inspections, repairs and other events in the life of a unit with createLifecycleEvent, and read the lifecycle with listLifecycleEvents.

A lifecycle event records that something happened to a battery: it was put into service, inspected, repaired, a
component was replaced. Events are records: they change none of the unit's data. Measured values go through
[`updateCondition`](https://passport.kabin.sh/docs/en/guides/push-condition-data). Background:
[Condition data and lifecycle](https://passport.kabin.sh/docs/en/concepts/condition-and-lifecycle).

| Operation                                                                                                    | Scope       | What it does                       |
| ------------------------------------------------------------------------------------------------------------ | ----------- | ---------------------------------- |
| [`createLifecycleEvent`](https://passport.kabin.sh/docs/en/api-reference/lifecycle/create-lifecycle-event) | `condition` | Records one event for a unit       |
| [`listLifecycleEvents`](https://passport.kabin.sh/docs/en/api-reference/lifecycle/list-lifecycle-events)   | `read`      | The unit's lifecycle, newest first |

## Event types

| `type`               | Use it for                                      |
| -------------------- | ----------------------------------------------- |
| `placed_on_market`   | The battery was placed on the market.           |
| `put_into_service`   | The battery was installed and put into service. |
| `inspection`         | A check or maintenance visit.                   |
| `repair`             | A repair.                                       |
| `component_replaced` | A module, cell or other component was replaced. |
| `waste`              | The battery became waste.                       |
| `recycled`           | The battery was handed over for recycling.      |
| `custom`             | Anything else; say what in `description`.       |

The contract's `ReportedEventType` also lists `condition_update`, but `createLifecycleEvent` refuses it with
`400 validation_error` on `type`: those events are written by `updateCondition`. The second-life types
(`repurposed`, `reused`, `remanufactured`) appear in the lifecycle when a member creates a second-life unit in
the portal; they cannot be recorded through the API.

## Record an event

1. **Find the documents (optional)**

   `evidence_ids` names documents of your organisation's evidence library, for example a diagnostic report or a
   repair protocol, at most 20 and each once. Members upload documents in the portal; find their ids with
   [`listEvidence`](https://passport.kabin.sh/docs/en/api-reference/evidence/list-evidence) (key with scope `read`), for example
   `?kind=diagnostic_report`.

2. **Send the event**

   `POST /api/v1/batteries/{batteryId}/lifecycle-events`

   - **type** `string` (body, required): One of the types above.
   - **occurred_on** `date` (body, required): The day it happened (`YYYY-MM-DD`), between the unit's manufacturing date and today.
   - **description** `string` (body): At most 2000 characters.
   - **evidence_ids** `uuid[]` (body): At most 20 distinct documents of the evidence library.

   ```bash title="cURL"
   curl -sS -X POST "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/lifecycle-events" \
     -H "Authorization: Bearer $BATTERYPASS_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "type": "inspection",
       "occurred_on": "2026-10-22",
       "description": "Annual inspection: housing, connectors and BMS log checked, no findings.",
       "evidence_ids": ["0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b"]
     }'
   ```

   ```ts title="TypeScript"
   const batteryId = '0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f';
   const res = await fetch(`${process.env.BATTERYPASS_URL}/api/v1/batteries/${batteryId}/lifecycle-events`, {
     method: 'POST',
     headers: {
       Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}`,
       'Content-Type': 'application/json',
     },
     body: JSON.stringify({
       type: 'inspection',
       occurred_on: '2026-10-22',
       description: 'Annual inspection: housing, connectors and BMS log checked, no findings.',
       evidence_ids: ['0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b'],
     }),
   });
   const event = await res.json();
   if (res.status !== 201) throw new Error(`${res.status} ${event.code} (request_id ${event.request_id})`);
   console.log(event.id, event.type, event.occurred_on);
   ```

   ```python title="Python"
   import os

   import requests

   battery_id = "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f"
   res = requests.post(
       f"{os.environ['BATTERYPASS_URL']}/api/v1/batteries/{battery_id}/lifecycle-events",
       headers={"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"},
       json={
           "type": "inspection",
           "occurred_on": "2026-10-22",
           "description": "Annual inspection: housing, connectors and BMS log checked, no findings.",
           "evidence_ids": ["0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b"],
       },
       timeout=30,
   )
   event = res.json()
   if res.status_code != 201:
       raise SystemExit(f"{res.status_code} {event['code']} (request_id {event.get('request_id')})")
   print(event["id"], event["type"], event["occurred_on"])
   ```

   ```go title="Go"
   package main

   import (
   	"bytes"
   	"encoding/json"
   	"fmt"
   	"log"
   	"net/http"
   	"os"
   )

   func main() {
   	batteryID := "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f"
   	body, _ := json.Marshal(map[string]any{
   		"type":         "inspection",
   		"occurred_on":  "2026-10-22",
   		"description":  "Annual inspection: housing, connectors and BMS log checked, no findings.",
   		"evidence_ids": []string{"0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b"},
   	})
   	url := os.Getenv("BATTERYPASS_URL") + "/api/v1/batteries/" + batteryID + "/lifecycle-events"
   	req, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
   	req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY"))
   	req.Header.Set("Content-Type", "application/json")
   	res, err := http.DefaultClient.Do(req)
   	if err != nil {
   		log.Fatal(err) // do not resend blindly: check listLifecycleEvents first
   	}
   	defer res.Body.Close()
   	var event map[string]any
   	if err := json.NewDecoder(res.Body).Decode(&event); err != nil {
   		log.Fatal(err)
   	}
   	if res.StatusCode != http.StatusCreated {
   		log.Fatalf("%d %v (request_id %v)", res.StatusCode, event["code"], event["request_id"])
   	}
   	fmt.Println(event["id"], event["type"], event["occurred_on"])
   }
   ```

   ```json title="201"
   {
     "id": "0192f8c2-4d5e-7f60-8a71-b2c3d4e5f6a7",
     "battery_id": "0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f",
     "type": "inspection",
     "occurred_on": "2026-10-22",
     "description": "Annual inspection: housing, connectors and BMS log checked, no findings.",
     "source": "api",
     "api_key": { "id": "0192e4c7-8b9a-7c6d-8e5f-4a3b2c1d0e9f", "name": "Service system North" },
     "field_keys": [],
     "evidence": [
       {
         "id": "0192f7aa-1c2d-7e3f-8a4b-5c6d7e8f9a0b",
         "kind": "diagnostic_report",
         "title": "Inspection report 2026",
         "filename": "inspection-2026-10-22.pdf",
         "access_tier": "internal",
         "revision": 1,
         "superseded": false
       }
     ],
     "created_at": "2026-10-24T08:15:02Z"
   }
   ```

   An event sent with a key has `source: api` and names the key in `api_key`; events of members have
   `reported_by`, those of service partners `partner`.

## No automatic de-duplication

BatteryPass does not recognise the same event sent twice: every successful request adds an event, and
`createLifecycleEvent` takes no idempotency key. That matters when a request times out and you do not know
whether it arrived:

- Do not resend a `POST` automatically after a network error, a timeout or a `5xx`.
- Before you send it again, look for your event (same `type`, `occurred_on` and `description`,
  `source: api`) in `listLifecycleEvents`. The list is ordered by `occurred_on`, newest first, so a backdated
  event is not necessarily on the first page: read on with `next_cursor` until the page holds events that
  occurred before yours (the [retry sample](https://passport.kabin.sh/docs/en/idempotency) does exactly that).
- Keep the returned `id` in your system, so you know which events are already recorded.

A `429 rate_limited` is safe to resend after `Retry-After`: the request was refused before anything was stored.
More in [Retries and idempotency](https://passport.kabin.sh/docs/en/idempotency).

## Read the lifecycle

```bash title="cURL"
curl -sS "$BATTERYPASS_URL/api/v1/batteries/0192a3f0-5b7c-7d21-8e4f-6a2b9c1d3e5f/lifecycle-events?limit=50" \
  -H "Authorization: Bearer $BATTERYPASS_API_KEY"
```

The answer is a page `{items, next_cursor}`, newest `occurred_on` first; follow `next_cursor` for older events
([Pagination](https://passport.kabin.sh/docs/en/pagination)). The list holds every event of the unit, whoever reported it: members,
service partners through their links, condition updates (`type: condition_update` with the updated
`field_keys`), keys and the system. For a second life, `related_battery_id` and `related_serial_number` name the
other unit.

## Errors

| Status and `code`      | Typical cause                                                                                                                                                                                                                             |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 validation_error` | `type` not allowed (including `condition_update`); `occurred_on` missing, in the future or before the manufacturing date; `description` too long; an entry of `evidence_ids` that is not a document of your organisation or appears twice |
| `401 unauthenticated`  | Key missing, unknown or revoked                                                                                                                                                                                                           |
| `403 forbidden`        | The key lacks the scope `condition` (for `listLifecycleEvents`: `read`)                                                                                                                                                                   |
| `404 not_found`        | The unit does not exist in the key's organisation                                                                                                                                                                                         |
| `409 conflict`         | The unit's battery model is archived                                                                                                                                                                                                      |
| `429 rate_limited`     | Too many requests; wait for `Retry-After`                                                                                                                                                                                                 |
