# Handle API keys securely

> Where keys belong, which scope each system gets, how to rotate without downtime and what to do when a key leaks.

An API key (`bp_live_…`) belongs to your organisation, not to a person. Whoever holds it reads what a viewer of
your organisation reads, including restricted passport data and exports, and with the scope `condition` reports
measurements and events in your organisation's name. Treat every key like a password. How keys work is described
in [Authentication](https://passport.kabin.sh/docs/en/authentication); the wider picture in
[Security and data protection](https://passport.kabin.sh/docs/en/general/security).

Owners create and revoke keys in the portal under **Settings → API keys**. There is no API for it, and a key's
secret is shown only once, when it is created.

## Rules

| Rule                                                                                                                     | Why                                                                                                     |
| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| Keep keys in a secrets manager (or your platform's secret store) and hand them to the process as an environment variable | They stay out of code, images and backups                                                               |
| Never put a key into source code, a repository, a container image, a URL, a log line, a ticket or a chat                 | Each of these is copied, indexed or kept for years                                                      |
| Never ship a key to a browser, a mobile app or a device in the field                                                     | Anything on a client can be read; public passport reads need no key at all                              |
| Send keys only in the `Authorization: Bearer` header, only over HTTPS                                                    | Headers do not end up in access logs and browser histories the way URLs do                              |
| One key per system                                                                                                       | You can revoke one system without stopping the others, and the portal shows when each key was last used |
| The smallest scope that works                                                                                            | A leaked read key cannot report data; a leaked condition key cannot read your data                      |

## Which scope for which system

| System                                                        | Scope                                        |
| ------------------------------------------------------------- | -------------------------------------------- |
| ERP or MES that copies units and passports                    | `read`                                       |
| Data warehouse or reporting                                   | `read`                                       |
| Battery management system or gateway that sends measurements  | `condition`                                  |
| Service partner's system that reports inspections and repairs | `condition`                                  |
| A tool that looks up condition fields before it sends         | `read` and `condition`, or two separate keys |
| Middleware that stages units from production as an import     | `import` (members commit and approve)        |

A key is not limited to some models or units: a `condition` key you hand to a service partner can report for
every unit of your organisation. Hand it only to partners you would let do that.

## Load the key at start

Read the key from the environment once, fail early when it is missing, and never print it:

```bash title="Shell"
# In a systemd unit, a CI secret or your orchestrator's secret store, not in a script under version control
export BATTERYPASS_API_KEY="$(cat /run/secrets/batterypass_api_key)"
```

```ts title="TypeScript"
const key = process.env.BATTERYPASS_API_KEY;
if (!key?.startsWith('bp_live_')) throw new Error('BATTERYPASS_API_KEY is missing or malformed');
const headers = { Authorization: `Bearer ${key}` }; // pass this object along; never log it
```

```python title="Python"
import os
import sys

key = os.environ.get("BATTERYPASS_API_KEY", "")
if not key.startswith("bp_live_"):
    sys.exit("BATTERYPASS_API_KEY is missing or malformed")
headers = {"Authorization": f"Bearer {key}"}  # pass this dict along; never log it
```

```go title="Go"
package main

import (
	"log"
	"net/http"
	"os"
	"strings"
)

func main() {
	key := os.Getenv("BATTERYPASS_API_KEY")
	if !strings.HasPrefix(key, "bp_live_") {
		log.Fatal("BATTERYPASS_API_KEY is missing or malformed")
	}
	req, _ := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1/registry", nil)
	req.Header.Set("Authorization", "Bearer "+key) // never log req.Header
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	res.Body.Close()
	log.Println("getRegistryInfo:", res.Status)
}
```

If your HTTP client or proxy logs requests, make sure it masks the `Authorization` header.

## Rotate without downtime

Keys do not expire. Rotate them on a schedule you choose, and whenever someone who knew a key leaves. An
organisation can have 25 active keys, so old and new key fit side by side during the switch.

1. **Create the new key**

   An owner creates a key with the same scopes and a name that tells the two apart, for example
   `ERP sync 2026-10`, and puts the secret straight into the secrets manager.

2. **Deploy it**

   Point the system at the new secret and restart or reload it. Both keys work at the same time, so nothing fails
   in between.

3. **Check that the new key is used**

   In **Settings → API keys**, the new key shows a recent **Last used** time and the old one stops changing. The
   time is updated at most once a minute.

4. **Revoke the old key**

   Revoke the old key. Revocation is immediate and final; the key stays in the list as revoked.

## When a key leaks

A key leaked when it appeared anywhere outside your secrets manager: a commit, a log, a screenshot, a support
ticket, a former employee's notes.

1. **Revoke it now**

   An owner revokes the key in **Settings → API keys**. From that moment every request with it is
   `401 unauthenticated`. Do this first, before you know whether the key was misused.

2. **Replace it**

   Create a new key with the same scopes and deploy it as in the rotation above. The system that used the old key is
   down until then, which is the price of revoking first.

3. **Check what happened**

   Writes made with a key are recorded with the key as the actor: look at the portal's audit log and at lifecycle
   events with `source: api` and the key's name in `api_key`. Reads are not recorded, so assume that everything the
   key could read was read.

4. **Remove the copy**

   Delete the key from where it leaked (rewrite the commit, purge the log, edit the ticket), even though it no
   longer works, so that scanners stop flagging it.

> [!TIP]
> **Find keys before others do**
>
> Secrets have a fixed shape: `bp_live_` followed by 43 base64url characters (letters, digits, `-` and `_`). Add the pattern
> `bp_live_[A-Za-z0-9_-]{43}` to your secret scanner and pre-commit hooks. The portal shows the first 12 characters
> of each key, enough to match a finding to a key without exposing the secret.

All keys of your organisation share one limit of 600 requests per minute, so a leaked key that is still active
can also slow down your legitimate integrations ([Rate limits](https://passport.kabin.sh/docs/en/rate-limits)).
