> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endstate.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Credentials

> The Endstate API uses bearer tokens: an API key for server-to-server requests, a session token for chip-scoped actions, and a publishable key that identifies your organization without granting access.

## Overview

Every request to the Endstate API must include an `Authorization: Bearer <token>` header. Three credential types are accepted, and each endpoint takes a specific one.

| Credential      | Prefix            | Used by                                   | Sent as                                 |
| --------------- | ----------------- | ----------------------------------------- | --------------------------------------- |
| API key         | `end_sk_...`      | Your server                               | `Authorization: Bearer end_sk_...`      |
| Session token   | `end_sess_...`    | Your server or client, after verification | `Authorization: Bearer end_sess_...`    |
| Publishable key | `end_pk_live_...` | Your client, safe to embed in page source | `Authorization: Bearer end_pk_live_...` |

***

## API key

API keys are long-lived and scoped to your organization. They are for server-to-server use only.

* Accepted on most `/v1/*` endpoints. Some actions require a session token instead - for example, creating a claim or a transfer - and `GET /v1/session-tokens/current` accepts only a session token.
* Returned once at creation - if you lose it, you must obtain a new one.
* Issued by Endstate during onboarding.
* Rotation and revocation are handled by Endstate - there is no self-serve
  key-management endpoint. Contact Endstate to rotate a key or to revoke a
  compromised one; a replacement is issued and the old key is disabled.

<Warning>
  Never expose your API key to a browser, mobile app, or any client-side
  environment. Treat it with the same care as a database password. If an API key
  is compromised, contact Endstate immediately.
</Warning>

### Example request

<CodeGroup>
  ```ts SDK theme={null}
  await endstate.units.list();
  ```

  ```bash cURL theme={null}
  curl https://api2.endstate.io/v1/units \
    -H "Authorization: Bearer end_sk_..."
  ```
</CodeGroup>

<Accordion title="Response">
  ```json theme={null}
  {
    "units": [
      {
        "id": "8e1a7f50-90ab-4cde-f012-3456789abcde",
        "name": "Black Hoodie - Size M",
        "external_id": "sku-hoodie-black-m",
        "created_at": "2026-05-14T10:30:00.000Z"
      }
    ],
    "pagination": {
      "limit": 50,
      "has_more": false,
      "next_cursor": null
    }
  }
  ```
</Accordion>

***

## Session token

Session tokens are short-lived credentials issued when a chip is verified. They prove that a specific chip was physically tapped moments ago.

* Issued by one endpoint, `POST /v1/taps`, which takes either credential: your API key when your server verifies the tap, or your publishable key when your page verifies it directly.
* Default lifetime: **600 seconds**. An API-key caller can send a `ttl` (60 to 3600 seconds) to change it; a publishable-key caller always gets the default.
* Scoped to exactly one chip, one unit, and one organization.
* Returned once - opaque and unrecoverable after issuance.
* Required to authorize unit-scoped actions on behalf of the user who tapped - for example, creating a claim (`POST /v1/units/{unit_id}/claims`) or a transfer (`POST /v1/units/{unit_id}/transfers`).
* The only credential accepted by `GET /v1/session-tokens/current`, which lets a client confirm the scope of its session without ever seeing your API key.

<Note>
  A session token authorizes unit-scoped actions on behalf of the user who
  tapped - including **claiming** a unit (handing it to a recipient). See
  [Claims](/concepts/claims).
</Note>

### Example request

After verifying a chip, pass the returned session token as the bearer credential to introspect it:

<CodeGroup>
  ```ts SDK theme={null}
  await session.sessionTokens.current();
  ```

  ```bash cURL theme={null}
  curl https://api2.endstate.io/v1/session-tokens/current \
    -H "Authorization: Bearer end_sess_..."
  ```
</CodeGroup>

<Accordion title="Response">
  ```json theme={null}
  {
    "expires_at": "2026-05-14T10:40:00.000Z",
    "scope": {
      "chip_id": "ABCDEF0123",
      "unit_id": "8e1a7f50-90ab-4cde-f012-3456789abcde",
      "organization_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    }
  }
  ```
</Accordion>

***

## Publishable key

Publishable keys identify your organization to the API. They are safe to embed in page source, ship in a bundle, or expose in a public repository.

* Accepted by exactly one endpoint: `POST /v1/taps`, which exchanges a tap for a session token without involving your server. That endpoint also takes an API key, so the same call works from your server.
* Grants nothing on its own. It names your organization; it does not read, write, or authorize anything. The authority in that exchange is the one-time `e` value from the tap, which is unforgeable and works exactly once.
* Retrieve yours with `GET /v1/publishable-keys`, which itself requires your API key. See [Publishable keys](/settings/publishable-keys).
* Browser requests are matched against your organization's allowed origins. A request with no `Origin` header is not origin-checked, so the allow-list governs where the key may be embedded, not who may call the API - the one-time `e` value remains the authority. See [CORS origins](/settings/cors-origins).

<Note>
  A publishable key is not a lesser API key - it is a different kind of thing.
  An API key answers "may this caller act on the organization?"; a publishable
  key answers only "which organization is this?". Losing control of one does not
  expose your data.
</Note>

### Example request

<CodeGroup>
  ```ts SDK theme={null}
  const session = await endstate.verify({
    chip_id: "ABCDEF0123",
    e: "C78566198547116F3A715DC1C62AF96F",
  });
  ```

  ```bash cURL theme={null}
  curl -X POST https://api2.endstate.io/v1/taps \
    -H "Authorization: Bearer end_pk_live_..." \
    -H "Content-Type: application/json" \
    -d '{"chip_id": "ABCDEF0123", "e": "C78566198547116F3A715DC1C62AF96F"}'
  ```
</CodeGroup>

<Accordion title="Response">
  ```json theme={null}
  {
    "id": "5c2f1b90-3d4e-4a5b-8c6d-7e8f90123456",
    "session_token": {
      "token": "end_sess_...",
      "expires_at": "2026-05-14T10:40:00.000Z",
      "scope": {
        "chip_id": "ABCDEF0123",
        "unit_id": "8e1a7f50-90ab-4cde-f012-3456789abcde",
        "organization_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      }
    },
    "chip": {
      "chip_id": "ABCDEF0123",
      "scan_count": 47
    },
    "unit": {
      "id": "8e1a7f50-90ab-4cde-f012-3456789abcde",
      "name": "Black Hoodie - Size M",
      "external_id": "sku-hoodie-black-m",
      "attributes": { "color": "black", "size": "M" },
      "collection": {
        "id": "2b7c9d10-4e5f-4a6b-8c7d-9e0f12345678",
        "name": "Black Hoodie - Fall 2026",
        "external_id": "hoodie-black-fall-2026",
        "contract": {
          "address": "0x1111111111111111111111111111111111111111",
          "chain_id": 84532,
          "status": "active"
        },
        "token": {
          "status": "active",
          "serial": 12
        }
      }
    },
    "redirect_url": "https://verify.brand.example/ABCDEF0123",
    "dry_run": false
  }
  ```
</Accordion>

The response is the same whichever credential you send.

***

## Telling them apart

The API identifies credential type by prefix before any other validation:

* `end_sk_...` - API key. Full organization access; server-only.
* `end_sess_...` - Session token. Single-chip scope; short-lived.
* `end_pk_live_...` - Publishable key. Identifies your organization; grants no access by itself, so it is safe in page source. Accepted only by `POST /v1/taps` - sending one anywhere else returns `401`.

Each endpoint accepts a specific credential type, and sending the wrong one returns a `401` before any other validation. For example, an API key sent to `GET /v1/session-tokens/current` is rejected, as is a session token sent to an endpoint that requires an API key. Endpoints that accept more than one credential take whichever you send: `POST /v1/taps` accepts an API key or a publishable key, and reading claim status accepts an API key or a session token.

***

## Authentication errors

Branch on `error.code` in your error handling, not on the HTTP status or `message`.

| Code                               | HTTP | When                                                                                |
| ---------------------------------- | ---- | ----------------------------------------------------------------------------------- |
| `auth.unauthorized`                | 401  | Credential is missing, malformed, or the wrong type for this endpoint               |
| `auth.forbidden`                   | 403  | Credential is valid but does not have permission for the requested resource         |
| `session_token.invalid_or_expired` | 401  | Session token is unknown, expired, or has been revoked                              |
| `session_token.wrong_chip`         | 403  | Session token is valid but is bound to a different chip than the one in the request |

Cross-organization resource access returns **404** rather than 403 - the API deliberately does not reveal whether a resource exists in another organization. See [Errors](/conventions/errors) for the full error envelope format and code taxonomy.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Environments" icon="layers" href="/environments">
    The API base URL and how to build and test with test chips before going
    live.
  </Card>

  <Card title="Verify a unit" icon="scan-line" href="/guides/verify-a-unit">
    Walk through the end-to-end chip verification flow that issues a session
    token.
  </Card>
</CardGroup>
