# Quickstart

> Create an API key, make your first request and read a passport in a few minutes.

You need:

- the BatteryPass address of your organisation, for example `https://batterypass.example.com`;
- the owner role in that organisation, or an owner who creates the key for you;
- `curl` and `jq`, or Node.js 20+ (the TypeScript samples are ES modules, e.g. `npx tsx quickstart.mts`),
  Python 3 with `requests`, or Go.

1. **Create an API key**

   An owner signs in to the BatteryPass portal, opens **Settings → API keys** and chooses **Create API key**.
   Give the key a name that says which system uses it, and select the scope **Read** (`read`). The scope
   **Report condition data** (`condition`) is only for systems that report measurements and lifecycle events.

   The secret starts with `bp_live_` and is shown **only once**. Copy it into your secret store right away; it
   cannot be displayed again. Keys are managed only in the portal, not through the API.

2. **Set two environment variables**

   ```bash title="Shell"
   export BATTERYPASS_URL="https://batterypass.example.com"
   export BATTERYPASS_API_KEY="bp_live_..."
   ```

   `BATTERYPASS_URL` is your organisation's BatteryPass address without a trailing slash. Every sample on this
   site reads these two variables. More in [Environments and base URL](https://passport.kabin.sh/docs/en/environments).

3. **Make your first request**

   List the first five battery models of your organisation with
   [`listBatteryModels`](https://passport.kabin.sh/docs/en/api-reference/catalog/list-battery-models):

   ```bash title="cURL"
   curl -sS "$BATTERYPASS_URL/api/v1/battery-models?limit=5" \
     -H "Authorization: Bearer $BATTERYPASS_API_KEY"
   ```

   ```json title="200"
   {
     "items": [
       {
         "id": "0192f0a1-6c2e-7d4b-9a10-3e5f7b2c8d41",
         "name": "SolarVolt Home 10",
         "sku": "SV-H10",
         "gtin": "04012345000023",
         "category": "stationary",
         "archived_at": null,
         "row_version": 3,
         "created_at": "2026-01-08T09:12:44Z",
         "updated_at": "2026-03-02T15:40:10Z"
       }
     ],
     "next_cursor": null
   }
   ```

   A `401` means the header is missing or the key is unknown or revoked; a `403` means the key lacks the `read`
   scope. See [Authentication](https://passport.kabin.sh/docs/en/authentication) and [Errors](https://passport.kabin.sh/docs/en/errors).

4. **List battery models in code**

   ```ts title="TypeScript"
   const res = await fetch(`${process.env.BATTERYPASS_URL}/api/v1/battery-models?limit=50`, {
     headers: { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` },
   });
   if (!res.ok) throw new Error(`listBatteryModels: ${res.status} ${await res.text()}`);

   const page: { items: { id: string; name: string; gtin: string | null }[]; next_cursor: string | null } =
     await res.json();
   for (const model of page.items) console.log(model.name, model.gtin ?? '(no GTIN)', model.id);
   if (page.next_cursor) console.log('More models on the next page.');
   ```

   ```python title="Python"
   import os

   import requests

   res = requests.get(
       f"{os.environ['BATTERYPASS_URL']}/api/v1/battery-models",
       headers={"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"},
       params={"limit": 50},
       timeout=30,
   )
   res.raise_for_status()
   page = res.json()
   for model in page["items"]:
       print(model["name"], model["gtin"] or "(no GTIN)", model["id"])
   if page["next_cursor"]:
       print("More models on the next page.")
   ```

   ```go title="Go"
   package main

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

   func main() {
   	req, err := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1/battery-models?limit=50", nil)
   	if err != nil {
   		log.Fatal(err)
   	}
   	req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY"))
   	res, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
   	if err != nil {
   		log.Fatal(err)
   	}
   	defer res.Body.Close()
   	if res.StatusCode != http.StatusOK {
   		log.Fatalf("listBatteryModels: %s", res.Status)
   	}
   	var page struct {
   		Items []struct {
   			ID   string  `json:"id"`
   			Name string  `json:"name"`
   			GTIN *string `json:"gtin"`
   		} `json:"items"`
   		NextCursor *string `json:"next_cursor"`
   	}
   	if err := json.NewDecoder(res.Body).Decode(&page); err != nil {
   		log.Fatal(err)
   	}
   	for _, m := range page.Items {
   		gtin := "(no GTIN)"
   		if m.GTIN != nil {
   			gtin = *m.GTIN
   		}
   		fmt.Println(m.Name, gtin, m.ID)
   	}
   	if page.NextCursor != nil {
   		fmt.Println("More models on the next page.")
   	}
   }
   ```

   Lists return at most `limit` items and a `next_cursor`; [Pagination](https://passport.kabin.sh/docs/en/pagination) shows how to read
   all pages.

5. **Read the passport of a unit**

   Find a unit with a published passport with [`listBatteries`](https://passport.kabin.sh/docs/en/api-reference/catalog/list-batteries),
   then read its passport with [`getPassport`](https://passport.kabin.sh/docs/en/api-reference/passports/get-passport):

   ```bash title="cURL"
   BATTERY_ID=$(curl -sS "$BATTERYPASS_URL/api/v1/batteries?passport_status=published&limit=1" \
     -H "Authorization: Bearer $BATTERYPASS_API_KEY" | jq -r '.items[0].id')

   curl -sS "$BATTERYPASS_URL/api/v1/batteries/$BATTERY_ID/passport" \
     -H "Authorization: Bearer $BATTERYPASS_API_KEY"
   ```

   ```ts title="TypeScript"
   const base = `${process.env.BATTERYPASS_URL}/api/v1`;
   const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_API_KEY}` };

   const units = await fetch(`${base}/batteries?passport_status=published&limit=1`, { headers });
   if (!units.ok) throw new Error(`listBatteries: ${units.status}`);
   const { items }: { items: { id: string }[] } = await units.json();
   if (items.length === 0) throw new Error('No unit with a published passport yet.');

   const res = await fetch(`${base}/batteries/${items[0].id}/passport`, { headers });
   if (!res.ok) throw new Error(`getPassport: ${res.status}`);
   const passport = await res.json();
   console.log(passport.passport_id, passport.status, passport.public_url);
   ```

   ```python title="Python"
   import os

   import requests

   base = f"{os.environ['BATTERYPASS_URL']}/api/v1"
   headers = {"Authorization": f"Bearer {os.environ['BATTERYPASS_API_KEY']}"}

   units = requests.get(f"{base}/batteries", headers=headers,
                        params={"passport_status": "published", "limit": 1}, timeout=30)
   units.raise_for_status()
   items = units.json()["items"]
   if not items:
       raise SystemExit("No unit with a published passport yet.")

   res = requests.get(f"{base}/batteries/{items[0]['id']}/passport", headers=headers, timeout=30)
   res.raise_for_status()
   passport = res.json()
   print(passport["passport_id"], passport["status"], passport.get("public_url"))
   ```

   ```go title="Go"
   package main

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

   var client = &http.Client{Timeout: 30 * time.Second}

   // get sends an authorised GET to the API and decodes the JSON answer into out.
   func get(path string, out any) error {
   	req, err := http.NewRequest(http.MethodGet, os.Getenv("BATTERYPASS_URL")+"/api/v1"+path, nil)
   	if err != nil {
   		return err
   	}
   	req.Header.Set("Authorization", "Bearer "+os.Getenv("BATTERYPASS_API_KEY"))
   	res, err := client.Do(req)
   	if err != nil {
   		return err
   	}
   	defer res.Body.Close()
   	if res.StatusCode != http.StatusOK {
   		return fmt.Errorf("GET %s: %s", path, res.Status)
   	}
   	return json.NewDecoder(res.Body).Decode(out)
   }

   func main() {
   	var units struct {
   		Items []struct {
   			ID string `json:"id"`
   		} `json:"items"`
   	}
   	if err := get("/batteries?passport_status=published&limit=1", &units); err != nil {
   		log.Fatal(err)
   	}
   	if len(units.Items) == 0 {
   		log.Fatal("No unit with a published passport yet.")
   	}
   	var passport struct {
   		PassportID string `json:"passport_id"`
   		Status     string `json:"status"`
   		PublicURL  string `json:"public_url"`
   	}
   	if err := get("/batteries/"+units.Items[0].ID+"/passport", &passport); err != nil {
   		log.Fatal(err)
   	}
   	fmt.Println(passport.PassportID, passport.Status, passport.PublicURL)
   }
   ```

   ```json title="200"
   {
     "battery_id": "0192f0a3-1b7c-7e2a-8f45-6a9d0c3e5b12",
     "status": "published",
     "serial_number": "SV2027-000001",
     "gtin": "04012345000023",
     "passport_id": "urn:dpp:0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e",
     "gs1_digital_link": "https://batterypass.example.com/01/04012345000023/21/SV2027-000001",
     "public_url": "https://batterypass.example.com/pass/0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e",
     "first_published_at": "2026-03-10T08:30:00Z",
     "identifiers_locked_at": "2026-03-10T08:30:00Z",
     "row_version": 4
   }
   ```

   The response also carries `current_version` with the version number, the hashes and the signing key of the
   current version. A unit that was never published has a passport in status `draft` without `passport_id`.

6. **Read the public passport without a key**

   The public tier of a published passport needs no key. [`getPublicPassport`](https://passport.kabin.sh/docs/en/api-reference/public/get-public-passport)
   takes the UUID part of `passport_id`, without the prefix `urn:dpp:`:

   ```bash title="cURL"
   PASSPORT_UUID=$(curl -sS "$BATTERYPASS_URL/api/v1/batteries/$BATTERY_ID/passport" \
     -H "Authorization: Bearer $BATTERYPASS_API_KEY" | jq -r '.passport_id | sub("^urn:dpp:"; "")')

   curl -sS "$BATTERYPASS_URL/api/v1/public/passports/$PASSPORT_UUID" | jq '{passport_id, status, links}'
   ```

   ```ts title="TypeScript"
   // passport_id from getPassport
   const passportId = 'urn:dpp:0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e';
   const uuid = passportId.replace(/^urn:dpp:/, '');

   const res = await fetch(`${process.env.BATTERYPASS_URL}/api/v1/public/passports/${uuid}`);
   if (!res.ok) throw new Error(`getPublicPassport: ${res.status}`);
   const pub = await res.json();
   console.log(pub.status, pub.document?.model_name, pub.links.page);
   ```

   ```python title="Python"
   import os

   import requests

   passport_id = "urn:dpp:0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e"  # from getPassport
   uuid = passport_id.removeprefix("urn:dpp:")

   res = requests.get(f"{os.environ['BATTERYPASS_URL']}/api/v1/public/passports/{uuid}", timeout=30)
   res.raise_for_status()
   pub = res.json()
   print(pub["status"], pub.get("document", {}).get("model_name"), pub["links"]["page"])
   ```

   ```go title="Go"
   package main

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

   func main() {
   	passportID := "urn:dpp:0192f3c4-7e1a-7b2c-9d3e-5f6a7b8c9d0e" // from getPassport
   	uuid := strings.TrimPrefix(passportID, "urn:dpp:")

   	client := &http.Client{Timeout: 30 * time.Second}
   	res, err := client.Get(os.Getenv("BATTERYPASS_URL") + "/api/v1/public/passports/" + uuid)
   	if err != nil {
   		log.Fatal(err)
   	}
   	defer res.Body.Close()
   	if res.StatusCode != http.StatusOK {
   		log.Fatalf("getPublicPassport: %s", res.Status)
   	}
   	var pub struct {
   		Status   string `json:"status"`
   		Document *struct {
   			ModelName string `json:"model_name"`
   		} `json:"document"`
   		Links struct {
   			Page string `json:"page"`
   		} `json:"links"`
   	}
   	if err := json.NewDecoder(res.Body).Decode(&pub); err != nil {
   		log.Fatal(err)
   	}
   	model := "(no content)"
   	if pub.Document != nil {
   		model = pub.Document.ModelName
   	}
   	fmt.Println(pub.Status, model, pub.Links.Page)
   }
   ```

   `document` is the signed public projection of the current version; `verification` holds its Ed25519
   signature. Passports that are not public (for example `withdrawn`) answer with their status and no
   `document`. Public reads are cached for five minutes and limited to 300 requests per minute per client
   address.

## Next steps

[**Authentication**](https://passport.kabin.sh/docs/en/authentication): Scopes, a key's rights, rotation and 401 versus 403.

[**Errors**](https://passport.kabin.sh/docs/en/errors): Problem Details, codes and the request id.

[**Push condition data**](https://passport.kabin.sh/docs/en/guides/push-condition-data): Report measurements from a battery management system.

[**Verify a signature**](https://passport.kabin.sh/docs/en/guides/verify-signature): Check a passport offline with the public keys.
