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

# Set up your catalog

> Register your products in Endstate before they ship: create a collection, create a unit for each item, and pair an NFC chip. The one-time setup that makes items verifiable.

This is the setup you do **before** items reach customers — in your back office or fulfillment pipeline, once per product line and once per item. By the end, each physical item has a unit and a paired chip, and is ready to verify at a tap.

The [Quickstart](/quickstart) runs this same flow with a simulated test chip. This guide is the production version: real encoded chips, real items, at scale.

<Note>
  Order matters, and the API enforces it: a collection must finish provisioning
  (`active`) before you can add units, and **pairing a chip is what issues the
  unit** — it assigns the serial and flips the unit from `pending` to `active`.
</Note>

<Steps>
  <Step title="Create a collection">
    A [collection](/concepts/collections) is a product line or drop. Create it once; every unit inside it shares its serial-number namespace. `POST /v1/collections` returns immediately with `contract.status: "deploying"` — provisioning is asynchronous.

    ```bash theme={null}
    curl -X POST https://api2.endstate.io/v1/collections \
      -H "Authorization: Bearer end_sk_test_..." \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Heritage Jacket — Fall 2026",
        "external_id": "heritage-jacket-fall-2026"
      }'
    ```

    ```json theme={null}
    {
      "id": "8e1a7f50-90ab-4cde-b012-3456789abcde",
      "name": "Heritage Jacket — Fall 2026",
      "external_id": "heritage-jacket-fall-2026",
      "redirect_url": null,
      "contract": {
        "status": "deploying",
        "address": "0x1111111111111111111111111111111111111111",
        "chain_id": 84532
      }
    }
    ```

    The collection is returned at the top level (as are all single-resource responses — see [Response shape](/conventions/responses)). Poll `GET /v1/collections/{collection_id}` until `contract.status` is `"active"` before adding units (a short interval; give up after a reasonable timeout). Creating a unit too early returns `collection.not_active`.

    Only `name` is required — the collection is created on your organization's default network, configured by Endstate at onboarding. The optional fields:

    * **`external_id`** — your own identifier for the collection, unique within your organization. The example above sets it so a re-run can look the collection up instead of creating a duplicate (see [Re-runs and conflicts](#re-runs-and-conflicts)).
    * **`redirect_url`** — where a tap on any of this collection's items sends the user. See [Tap redirects](/concepts/tap-redirects).
    * **`symbol`** — a short display symbol, 1–4 characters. Derived from `name` when omitted.
    * **`chain_id`** — override the default network. The value must be one of the networks enabled for your organization (`allowed_chain_ids` from `GET /v1/settings`); otherwise the API returns `validation.failed`.
  </Step>

  <Step title="Create a unit for each item">
    A [unit](/concepts/units) is the digital record of one physical item. Create one per item with `POST /v1/units`, using the active collection's `id`.

    ```bash theme={null}
    curl -X POST https://api2.endstate.io/v1/units \
      -H "Authorization: Bearer end_sk_test_..." \
      -H "Content-Type: application/json" \
      -d '{
        "collection_id": "8e1a7f50-90ab-4cde-b012-3456789abcde",
        "external_id": "jacket-fall26-unit-001",
        "name": "Heritage Jacket #001",
        "attributes": { "color": "olive", "size": "M" }
      }'
    ```

    The unit starts with `token.status: "pending"` and no serial — both are set when you pair a chip in the next step.

    Set `external_id` to your own SKU or serial so you can look the unit up later (`GET /v1/units?external_id=...`) without storing Endstate IDs. There's no bulk endpoint — loop the call to register every item ahead of time, before they ship.
  </Step>

  <Step title="Pair a chip (this issues the unit)">
    Pairing links a specific NFC chip to a specific unit and triggers issuance. A chip is **permanently** bound to one unit — it cannot be re-paired (`chip.already_paired`). If a paired chip is damaged before the item ships, don't re-pair it — [replace the unit's chip](/concepts/chips#replacing-a-chip) with a fresh one.

    **Encoded chip (production).** Do this during fulfillment, after the chip is encoded and before the item ships. Supply the `chip_id` (10 uppercase hex characters) and `e` (32 uppercase hex characters) from your encoding step.

    ```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": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "chip_id": "ABCDEF0123",
        "e": "C78566198547116F3A715DC1C62AF96F"
      }'
    ```

    ```json theme={null}
    {
      "chip_id": "ABCDEF0123",
      "is_test": false,
      "scan_count": 0,
      "created_at": "2026-06-15T09:00:00.000Z",
      "unit": {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "external_id": "jacket-fall26-unit-001",
        "name": "Heritage Jacket #001",
        "attributes": { "color": "olive", "size": "M" },
        "redirect_url": null,
        "created_at": "2026-06-15T08:59:00.000Z",
        "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": "pending", "serial": null }
        }
      }
    }
    ```

    <Warning>
      The `chip_id` and `e` come from your chip **encoding** process — do not derive
      them from a tap URL. The encoding `e` is consumed on pairing, and a fresh
      one-time `e` is produced at each user tap.
    </Warning>

    **Test chip (development).** Pass `is_test: true` and Endstate assigns the `chip_id` — no hardware needed. Generate tap values for it later with `POST /v1/chips/{chip_id}/tap`. See [Test without hardware](/guides/testing-without-hardware).

    ```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": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "is_test": true }'
    ```

    Pairing returns the assigned `chip_id` at the top level (the value to use for a test chip's taps). **Issuance is asynchronous and has no failure status** — poll `GET /v1/units/{unit_id}` until `collection.token.status` is `"active"`; the unit's `collection.token.serial` is set at that point. If it stays `pending` well beyond a few minutes, stop polling and contact support.
  </Step>
</Steps>

## Re-runs and conflicts

`external_id` makes setup safe to re-run. If a step reports a conflict, look the existing resource up by its `external_id` and reuse it instead of creating a duplicate.

Branch on `error.code` (never the message or HTTP status):

| `error.code`                | When                                | What to do                                                                                                                                            |
| --------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `collection.not_active`     | Creating a unit too early.          | Poll the collection until `contract.status: "active"`, then retry.                                                                                    |
| `collection.already_exists` | Re-creating a collection.           | Look it up with `GET /v1/collections?external_id=...` and reuse its `id`.                                                                             |
| `unit.already_exists`       | Re-creating a unit.                 | Look it up with `GET /v1/units?external_id=...` and reuse it.                                                                                         |
| `chip.already_paired`       | Pairing a chip that's already used. | Pairing is permanent — a chip cannot move to another unit. To swap a unit's damaged chip, use a [chip replacement](/concepts/chips#replacing-a-chip). |
| `validation.failed`         | Bad `chip_id` / `e` format, etc.    | Fix the field; see `error.details`. `chip_id` is 10 hex, `e` is 32 hex.                                                                               |

See [Errors](/conventions/errors) for the full envelope.

## Next steps

<CardGroup cols={2}>
  <Card title="Verify a unit" icon="scan-line" href="/guides/verify-a-unit">
    Now that items are registered, handle the user tap and verify authenticity
    end to end.
  </Card>

  <Card title="Tap redirects" icon="corner-up-right" href="/concepts/tap-redirects">
    Send users to your own page after a tap — per unit, per collection, or
    org-wide.
  </Card>
</CardGroup>
