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

# Chips & verification

> How NFC chips link physical products to their digital records, and how tap verification works.

Endstate chips are real, physical NFC chips — [NTAG 424 DNA](https://www.nxp.com/products/NTAG424DNA) — embedded in or attached to your products. They are genuine, tamper-resistant hardware: each chip holds a secret it never reveals, and every tap produces a fresh, single-use cryptographic value derived from it. That is what makes a tap impossible to forge or replay.

When a user taps the product, the chip delivers two values: a `chip_id` that identifies the chip and a one-time credential called an `e` value. You send those to the API, and Endstate confirms the item is authentic.

## What a chip is

A chip is a physical NTAG 424 DNA NFC tag bonded to a single product. Every chip has a `chip_id` — a 10-character hex string (e.g., `ABCDEF0123`) that appears in the tap URL your application receives. The `chip_id` is stable and public; it identifies the chip but does not prove a tap happened.

Each chip is paired to exactly one unit. Pairing is permanent on the chip's side — a chip cannot be re-paired to a different unit. The unit's side can still change until the item is claimed: a unit's chip can be swapped for a new one via [chip replacement](#replacing-a-chip), which retires the old chip rather than moving it.

Chips track `scan_count`: the number of times a tap has been successfully verified. This count increments each time a valid verification request completes (unless `dry_run` is used).

### Two kinds of chips

| Kind        | How to create                         | `is_test` |
| ----------- | ------------------------------------- | --------- |
| **Encoded** | `POST /v1/chips` with `chip_id` + `e` | `false`   |
| **Test**    | `POST /v1/chips` with `is_test: true` | `true`    |

**Encoded chips** are genuine NTAG 424 DNA NFC hardware. You supply the `chip_id` and a valid initial `e` value read from the chip when pairing it to a unit.

**Test chips** are simulated — no physical hardware required. Create one with `is_test: true` and then call `POST /v1/chips/{chip_id}/tap` to generate a fresh `e` value you can use in a verify request. Test chips let you run the full verification flow in development without having NFC hardware in hand. See [Testing without hardware](/guides/testing-without-hardware) for the complete walkthrough.

## The `e` value

Every tap produces a fresh `e` value — a 32-character hex string included in the tap URL alongside the `chip_id`. Treat the `e` value as a credential:

* **Single-use.** Once a verify request succeeds with a given `e`, that value is consumed. Submitting it again returns `chip.already_scanned` (HTTP 410).
* **Never log it.** The `e` value proves a tap occurred; logging it creates a replay risk.
* **Short window.** Pass it to the verify endpoint promptly after receiving it.

## Pairing a chip

Before a chip can be verified, it must be **paired** to a [unit](/concepts/units) — this is what links the physical hardware to its digital record. `POST /v1/chips` is the pairing primitive: it binds a chip's `chip_id` and initial `e` value to a `unit_id`.

```bash theme={null}
curl -X POST https://api2.endstate.io/v1/chips \
  -H "Authorization: Bearer end_sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "unit_id": "1b2c3d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
    "chip_id": "ABCDEF0123",
    "e": "C78566198547116F3A715DC1C62AF96F"
  }'
```

The unit must belong to a [collection](/concepts/collections). Pairing is permanent and issues the unit — it receives its serial number within the collection.

### Build your own pairing experience

Integrate the pairing API into your existing application and call it however fits your operation:

* **Pre-pair during fulfillment.** Read the chip's `chip_id` and `e` at encoding time and pair it before the product ships. This is the canonical path for a production catalog.
* **Pair from your own app.** Read chips directly with an NFC chip reader to capture each chip's `chip_id` and `e`, then pair them through your own interface — your staff or users never leave your application. You can also have Endstate redirect taps on an *unpaired* chip to your app, so a tap drops the holder straight into your pairing flow.

Contact Endstate to configure your verified domain and where unpaired taps should route.

### Use Endstate's hosted pairing screen

If you would rather not build your own, Endstate hosts a pairing screen. Physical chips ship pre-encoded to tap to a verify URL — `https://<your-domain>/verify/<chip_id>?e=<e>`. When a chip that has not been paired yet is tapped, the holder is routed to that screen, which binds the chip to a unit. The pairing it creates is part of the same system you read through this API — the chip and unit then appear in `GET /v1/chips` and `GET /v1/units`.

<Note>
  A chip is permanently bound to one unit. However it is paired, a chip cannot
  be re-paired to a different unit. If a unit's chip is damaged before the item
  is claimed, [replace the unit's chip](#replacing-a-chip) — the old chip is
  retired, never reused.
</Note>

### After a chip is paired

Tapping a paired chip redirects the user to your verified domain — the `redirect_url` returned by the [verify call](#verify-a-tap). That is where you run your verified experience.

## Replacing a chip

Until a unit is claimed, you can replace its chip — pair a new chip and retire the current one. Use this when a chip is damaged or fails QA after pairing but before the item reaches its owner.

```bash theme={null}
curl -X POST https://api2.endstate.io/v1/units/a1b2c3d4-e5f6-7890-abcd-ef1234567890/chip-replacements \
  -H "Authorization: Bearer end_sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "new_chip_id": "ABCDEF0123" }'
```

```json theme={null}
{
  "id": "00000000-0000-4000-8000-000000000000",
  "unit_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "old_chip_id": "ABCDEF0122",
  "new_chip_id": "ABCDEF0123",
  "status": "prepared",
  "created_at": "2026-06-15T12:00:00.000Z"
}
```

The rules:

* **The replacement chip must already exist in your organization and be unpaired.** This call never creates a chip — an unknown `new_chip_id` returns `chip.not_found`, and a chip that is already paired (to this unit or any other) returns `chip.already_paired`.
* **The unit must be issued and not yet claimed.** An unissued unit returns `unit.not_minted`; once the unit is claimed its chip is permanently locked and replacement returns `chip_replacement.locked`.
* **One replacement can be open per unit at a time** — a second attempt while one is settling returns `chip_replacement.in_progress`.
* **Endstate performs the swap for you.** Replacement is asynchronous; there is no client-broadcast mode. Requires an API key.

Poll `GET /v1/units/{unit_id}/chip-replacements/{replacement_id}` until the replacement settles:

| `status`    | Meaning                                               |
| ----------- | ----------------------------------------------------- |
| `prepared`  | The replacement is recorded and queued. Keep polling. |
| `submitted` | The swap is in flight. Keep polling.                  |
| `confirmed` | Done — the new chip is paired to the unit. Terminal.  |
| `failed`    | The swap failed. Terminal — create a new replacement. |

Once `confirmed`, the new chip verifies for the unit and appears in the unit's `chips`; the retired chip is detached from it.

## What verification does

Verification is the act of confirming a physical product is authentic by validating the tap's `e` value. The flow:

1. A user taps the product. The tap URL delivers `chip_id` and `e` to your application.
2. Your server calls `POST /v1/chips/{chip_id}` with the `e` value in the request body.
3. Endstate validates the credential against the chip, confirms the item is authentic, increments `scan_count`, and returns the unit's metadata plus a session token.

A successful response is proof the physical chip was present and tapped at that moment. It is not replayable.

## Verify a tap

### Request

```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"
  }'
```

#### Request fields

<ParamField body="e" type="string" required>
  The one-time credential from the tap URL. Must be 32 hex characters.
  Single-use — submitting a previously used `e` returns `chip.already_scanned`.
</ParamField>

<ParamField body="dry_run" type="boolean">
  When `true`, Endstate validates the `e` value but does not record a scan or
  issue a session token. Use this to confirm a tap is valid before committing
  it. Defaults to `false`.
</ParamField>

<ParamField body="ttl" type="integer">
  Lifetime of the issued session token in seconds. Accepted range: `60`–`3600`.
  Defaults to `600` (10 minutes). Has no effect when `dry_run` is `true`.
</ParamField>

### Response

```json theme={null}
{
  "id": "8e1a7f50-90ab-4cde-f012-3456789abcde",
  "session_token": {
    "token": "end_sess_AbCd1234EfGh5678IjKl9012MnOp",
    "expires_at": "2026-06-15T10:12:00.000Z",
    "scope": {
      "chip_id": "ABCDEF0123",
      "unit_id": "1b2c3d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
      "organization_id": "c0ffee00-1234-5678-90ab-cdef01234567"
    }
  },
  "chip": {
    "chip_id": "ABCDEF0123",
    "scan_count": 4
  },
  "unit": {
    "id": "1b2c3d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
    "name": "Canvas Tote #42",
    "external_id": "sku-tote-042",
    "attributes": { "color": "natural" },
    "collection": {
      "id": "8e1a7f50-90ab-4cde-b012-3456789abcde",
      "name": "Canvas Tote Collection",
      "external_id": null,
      "contract": {
        "address": "0x1111111111111111111111111111111111111111",
        "chain_id": 84532,
        "status": "active"
      },
      "token": {
        "status": "active",
        "serial": 42
      }
    }
  },
  "redirect_url": "https://verify.example.com/ABCDEF0123",
  "dry_run": false
}
```

#### Response fields

<ResponseField name="id" type="string | null">
  The unique ID of this scan record (UUIDv4). `null` when `dry_run` is `true` —
  no scan record is created.
</ResponseField>

<ResponseField name="session_token" type="object">
  An object containing `token` (the `end_sess_...` string), `expires_at` (ISO
  8601 expiry timestamp), and `scope` (`chip_id`, `unit_id`, `organization_id`).
  The `token` string proves this chip was just tapped — pass it to [`GET
      /v1/session-tokens/current`](/concepts/session-tokens) from a client to
  confirm its scope without exposing your API key. `null` when `dry_run` is
  `true`.
</ResponseField>

<ResponseField name="chip" type="object">
  The chip that was tapped.

  <Expandable title="chip fields">
    <ResponseField name="chip.chip_id" type="string">
      The chip's 10-character hex identifier.
    </ResponseField>

    <ResponseField name="chip.scan_count" type="integer">
      Total number of successful verifications recorded for this chip, including this one.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="unit" type="object">
  The unit this chip is paired to, including its metadata and issuance status.

  <Expandable title="unit fields">
    <ResponseField name="unit.id" type="string">
      The unit's UUIDv4.
    </ResponseField>

    <ResponseField name="unit.name" type="string">
      The unit's display name, if set.
    </ResponseField>

    <ResponseField name="unit.external_id" type="string">
      Your own identifier for this unit, if set.
    </ResponseField>

    <ResponseField name="unit.collection.token.status" type="string">
      The unit's issuance status. `pending` means issuance is in progress; `active` means the unit has a serial number and is fully issued.
    </ResponseField>

    <ResponseField name="unit.collection.token.serial" type="integer">
      The unit's serial number within its collection. Present when `token.status` is `active`.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="redirect_url" type="string | null">
  The canonical verified-domain URL for this chip
  (`https://<your-verified-domain>/verify/<chip_id>`), or `null` if your
  organization has no verified domain. You can redirect users here after a
  successful tap. Despite the shared name, this is **not** the [tap
  redirect](/concepts/tap-redirects) you configure on units and collections —
  that one controls where Endstate's hosted tap flow sends the user before
  verification.
</ResponseField>

<ResponseField name="dry_run" type="boolean">
  Echoes the `dry_run` request flag. When `true`, no scan was recorded and no
  session token was issued — `id` and `session_token` are `null`.
</ResponseField>

### Error codes

| Code                   | HTTP | When                                                                     |
| ---------------------- | ---- | ------------------------------------------------------------------------ |
| `chip.not_found`       | 404  | No chip with this `chip_id` exists in your organization.                 |
| `chip.invalid_e_value` | 422  | The `e` value is malformed or does not match this chip.                  |
| `chip.already_scanned` | 410  | This `e` value has already been used. Do not retry — obtain a fresh tap. |

<Warning>
  Do not retry a `chip.already_scanned` error with the same `e` value. The
  credential is consumed on first use. A replay indicates either a duplicate
  request or an attempted scan replay — investigate before proceeding.
</Warning>

## Chip endpoints

| Method | Path                                                     | Description                                         |
| ------ | -------------------------------------------------------- | --------------------------------------------------- |
| `POST` | `/v1/chips`                                              | Pair a chip (encoded or test) to a unit.            |
| `GET`  | `/v1/chips`                                              | List chips (cursor-paginated; filter by `is_test`). |
| `POST` | `/v1/chips/{chip_id}`                                    | Verify a tap.                                       |
| `POST` | `/v1/chips/{chip_id}/tap`                                | Generate a test `e` value (test chips only).        |
| `POST` | `/v1/units/{unit_id}/chip-replacements`                  | Replace a unit's chip (until the unit is claimed).  |
| `GET`  | `/v1/units/{unit_id}/chip-replacements/{replacement_id}` | Poll a replacement's status until `confirmed`.      |

For full request/response schemas, see the [API reference](/api-reference/introduction).

## Next steps

<CardGroup cols={2}>
  <Card title="Session tokens" icon="key" href="/concepts/session-tokens">
    Understand what the session token proves and how to introspect it from a
    client.
  </Card>

  <Card title="Verify a unit" icon="scan-line" href="/guides/verify-a-unit">
    Step-by-step guide to the full tap-to-verification flow.
  </Card>

  <Card title="Testing without hardware" icon="flask-conical" href="/guides/testing-without-hardware">
    Use test chips to run the full verification flow without NFC hardware.
  </Card>

  <Card title="API reference" icon="book-open" href="/api-reference/introduction">
    Complete endpoint reference for chips and all other resources.
  </Card>
</CardGroup>
