> ## 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.

# Verify a unit

> The complete production flow for verifying an authentic physical item: handle a real user tap end-to-end, from reading the chip to issuing a verified session token.

The [Quickstart](/quickstart) covers the same verification flow using a simulated test chip. This guide is for production: real encoded NFC hardware, real items, and the decisions your integration needs to make at each stage. Read through it once before writing code — the "why" at each step shapes your architecture.

<Steps>
  <Step title="Set up and pair your items">
    Before any taps, each item needs a [collection](/concepts/collections), its own [unit](/concepts/units), and a **paired chip** — a one-time setup you run during fulfillment, not at tap time. The [Set up your catalog](/guides/set-up-catalog) guide covers it end to end: create the collection, create a unit per item, and pair the encoded chip (which issues the unit and assigns its serial).

    Two things to carry into the verify flow below:

    * On every tap, the chip's tap URL carries the `chip_id` and a **fresh one-time `e`**. The `e` you supplied at *pairing* comes from encoding and is consumed then — it is not the tap `e`.
    * A chip is permanently bound to its unit. A chip can also be paired the first time it is tapped, through a hosted pairing screen — see [Pairing a chip](/concepts/chips#pairing-a-chip), including how to route unpaired taps to your own system.
  </Step>

  <Step title="The user taps">
    When a user taps the NFC chip on the item, the chip generates a fresh one-time `e` value and opens a URL. That URL carries both the `chip_id` and the new `e` as parameters — for example:

    ```
    chip_id=ABCDEF0123
    e=C78566198547116F3A715DC1C62AF96F
    ```

    The format of this URL is fixed by the chip's encoding. Your job is to extract `chip_id` and `e` from it (or from the query parameters your app receives when the user is redirected to your domain) and forward them to your backend immediately.

    The `e` value is a credential. Treat it accordingly:

    * Do not log it.
    * Do not store it at rest.
    * Pass it to the verify endpoint promptly — it is single-use and has a short validity window.

    Your frontend does not call the Endstate API directly. It sends `chip_id` and `e` to your own server, which makes the verify call using your API key.

    <Note>
      **Reading the tap in a browser is non-trivial.** Endstate ships no browser NFC
      SDK — the tag is standard NFC. Web NFC (`NDEFReader`) works only on **Android
      Chrome** (secure context + user gesture); **desktop** needs a USB/PC-SC reader
      driven over WebUSB/WebHID; **iOS Safari** has no in-browser tap path. The
      tag's payload is just a URL — read the NDEF record, extract the URI, then pull
      `chip_id` + `e` from it.
    </Note>

    <Note>
      A raw tap URL is often a short Endstate URL that **302-redirects** to the
      destination, with the params on the redirect target. If you resolve a tap URL
      **server-side**, follow the redirect and read the params off the `Location`
      header — `fetch(url, { redirect: "manual" })`.
    </Note>
  </Step>

  <Step title="Verify the tap">
    Your backend calls `POST /v1/chips/{chip_id}` with the `e` value. This is the core verification call. A successful response is non-replayable proof that the physical chip was tapped moments ago.

    ```bash theme={null}
    curl -X POST https://api2.endstate.io/v1/chips/ABCDEF0123 \
      -H "Authorization: Bearer end_sk_test_..." \
      -H "Content-Type: application/json" \
      -d '{
        "e": "C78566198547116F3A715DC1C62AF96F"
      }'
    ```

    ```json theme={null}
    {
      "id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
      "session_token": {
        "token": "end_sess_AbCd1234EfGh5678IjKl9012MnOp",
        "expires_at": "2026-06-15T10:12:00.000Z",
        "scope": {
          "chip_id": "ABCDEF0123",
          "unit_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "organization_id": "c0ffee00-1234-5678-90ab-cdef01234567"
        }
      },
      "chip": {
        "chip_id": "ABCDEF0123",
        "scan_count": 1
      },
      "unit": {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "name": "Heritage Jacket #001",
        "external_id": "jacket-fall26-unit-001",
        "attributes": { "color": "olive", "size": "M" },
        "collection": {
          "id": "8e1a7f50-90ab-4cde-b012-3456789abcde",
          "name": "Heritage Jacket — Fall 2026",
          "external_id": "heritage-jacket-fall-2026",
          "contract": {
            "address": "0x1111111111111111111111111111111111111111",
            "chain_id": 84532,
            "status": "active"
          },
          "token": {
            "status": "active",
            "serial": 1
          }
        }
      },
      "redirect_url": "https://verify.yourdomain.com/ABCDEF0123",
      "dry_run": false
    }
    ```

    Here is what each field tells you:

    **`id`** — the unique scan record ID. Log it alongside the `X-Request-Id` response header for support requests.

    **`chip.scan_count`** — the total number of successful verifications for this chip, including this one. A value of `1` means this is the first successful tap. Higher values mean the item has been verified before. Your application decides what a high count means — Endstate records the count but does not set policy on it.

    **`unit`** — the digital record for the verified item, including any `name`, `external_id`, and `attributes` you set when creating it. Issuance status and serial number are nested under `unit.collection.token` — `status: "active"` and a `serial` value confirm the unit is fully issued. Use this data to render an item detail view.

    **`session_token`** — an object containing a short-lived `token` string (`end_sess_...`) that proves this specific chip was just tapped. Valid for 600 seconds by default. Pass the `token` string to a browser or mobile client. See the next step.

    **`redirect_url`** — the canonical page to send the user to after a successful tap, or `null` if your account has no verified domain configured. When present, you can redirect the user here directly; the URL already carries the context needed to display the correct item.
  </Step>

  <Step title="Hand the session token to your client">
    The `session_token` from the verify response is designed to cross the server–client boundary safely. Your API key (`end_sk_...`) must never leave your server — but the session token can.

    After verifying the tap server-side, pass the `token` string from the `session_token` object to your browser or mobile client. The client can then call `GET /v1/session-tokens/current` using that token string as its bearer credential:

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

    ```json theme={null}
    {
      "chip_id": "ABCDEF0123",
      "unit_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "organization_id": "c0ffee00-1234-5678-90ab-cdef01234567",
      "expires_at": "2026-06-15T10:12:00.000Z"
    }
    ```

    This tells the client exactly which chip was tapped, which unit it belongs to, and when the session expires. The client can use this to confirm scope without a second round-trip to your server.

    The default session lifetime is 600 seconds. If your user flow needs more or less time, pass `ttl` (integer, 60–3600) in the verify request body.

    See [Session tokens](/concepts/session-tokens) for the full security model and use cases.
  </Step>
</Steps>

## Handle failures

Branch on `error.code` — never on HTTP status or `message`. The three codes specific to chip verification are:

| Code                   | HTTP | When                                                     | What to do                                                                                                                          |
| ---------------------- | ---- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `chip.invalid_e_value` | 422  | The `e` value is malformed or does not match this chip.  | Do not retry with the same `e`. Have the user tap again to produce a fresh one.                                                     |
| `chip.already_scanned` | 410  | This `e` was already used in a prior verify call.        | Do not retry. A replay means either a duplicate request from your code or an attempted scan replay — investigate before proceeding. |
| `chip.not_found`       | 404  | No chip with this `chip_id` exists in your organization. | The `chip_id` in the tap URL does not match any paired chip. Check that pairing completed successfully.                             |

For the full error envelope and handling conventions, see [Errors](/conventions/errors).

<Tip>
  Use `dry_run: true` in the verify request body to check that a chip decodes
  correctly without recording a scan or issuing a session token. This is useful
  during QA to confirm a newly encoded chip is readable before releasing the
  item. A `dry_run` verify call counts neither toward `scan_count` nor toward
  any quota.
</Tip>

<Note>
  The session token also authorizes **claiming** a unit on behalf of the user
  who tapped — handing it to a recipient. See [Claims](/concepts/claims) for the
  full flow, execution modes, and status polling.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Claim a unit" icon="arrow-right-left" href="/guides/claim-a-unit">
    Transfer ownership of the verified unit to a recipient, authorized by the
    session token.
  </Card>

  <Card title="Session tokens" icon="key" href="/concepts/session-tokens">
    Understand the credential you just received and how to use it on a client.
  </Card>
</CardGroup>
