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

# Claim a unit

> End-to-end: transfer ownership of a verified unit to a recipient — from the tap that authorizes it through to on-chain confirmation, in either execution mode.

This guide walks the full ownership-transfer journey: a user taps an item, you verify the tap, and you hand the unit to a recipient wallet. For the field-level reference, see the [Claims](/concepts/claims) concept page.

A claim is **authorized by a tap** — you cannot transfer a unit with an API key alone. The session token from a recent tap of that unit's chip is what proves a real person is holding the product.

## Prerequisites

* The unit is **issued** (`token.status: "active"`). A unit that has only been created but not paired to a chip cannot be claimed — you will get `unit.not_minted`. See [Verify a unit](/guides/verify-a-unit) for the setup and pairing flow.
* You can collect the **recipient's wallet address** (an EVM address) from the user.

<Steps>
  <Step title="Verify a tap to get a session token">
    The user taps the item's chip. Your server verifies it with `POST /v1/chips/{chip_id}` and receives a `session_token` scoped to that chip and unit. This is the same call covered in [Verify a unit](/guides/verify-a-unit).

    ```json theme={null}
    {
      "session_token": {
        "token": "end_sess_AbCd1234EfGh5678IjKl9012MnOp",
        "expires_at": "2026-06-15T10:12:00.000Z",
        "scope": {
          "chip_id": "ABCDEF0123",
          "unit_id": "1b2c3d4e-...",
          "organization_id": "..."
        }
      }
    }
    ```

    Use the `token` string as the bearer credential for the claim. The token is short-lived — create the claim while it is still valid.
  </Step>

  <Step title="Create the claim">
    Call `POST /v1/units/{unit_id}/claims` with the recipient's address and an execution mode, authorized by the session token. `execution` defaults to `endstate_relay`.

    ```bash theme={null}
    curl -X POST https://api2.endstate.io/v1/units/1b2c3d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e/claims \
      -H "Authorization: Bearer end_sess_AbCd1234EfGh5678IjKl9012MnOp" \
      -H "Content-Type: application/json" \
      -d '{
        "to": "0x1111111111111111111111111111111111111111",
        "execution": "endstate_relay"
      }'
    ```

    ```json theme={null}
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "unit_id": "1b2c3d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
      "to": "0x1111111111111111111111111111111111111111",
      "from": "0x2222222222222222222222222222222222222222",
      "status": "claiming",
      "created_at": "2026-06-15T12:00:00.000Z"
    }
    ```

    Endstate signs the transfer authorization for the unit's token. What happens next depends on `execution` — continue with whichever mode you chose.

    <Note>
      Only one claim can be open per unit at a time. A second attempt while one is
      in flight returns `claim.in_progress`. If the unit already belongs to the
      recipient, you get `claim.already_to_recipient`.
    </Note>
  </Step>

  <Step title="Settle the transfer">
    <Tabs>
      <Tab title="endstate_relay (default)">
        Nothing more to do — Endstate submits the on-chain transaction for you. Skip
        to the next step and poll for status.
      </Tab>

      <Tab title="client_broadcast">
        Pass `"execution": "client_broadcast"` in the previous step. The response then
        includes a `submission` object — a ready-to-send transaction you broadcast
        yourself:

        ```json theme={null}
        {
          "status": "claiming",
          "submission": {
            "to": "0x3333333333333333333333333333333333333333",
            "data": "0x..."
          }
        }
        ```

        The `submission` is already authorized by Endstate's signature, so **any
        account with gas on the collection's network can broadcast it** — your own
        funded relayer works, and the recipient never signs.

        ```ts theme={null}
        import { createWalletClient, http } from "viem";
        import { privateKeyToAccount } from "viem/accounts";

        const walletClient = createWalletClient({
          account: privateKeyToAccount(process.env.SENDER_KEY),
          transport: http(process.env.RPC_URL), // RPC for the collection's network
        });

        await walletClient.sendTransaction({
          to: claim.submission.to,
          data: claim.submission.data,
        });
        ```

        Use any EVM wallet client or signer — see [viem](https://viem.sh) or
        [ethers](https://docs.ethers.org). Broadcast within **30 minutes** of
        creating the claim — the signed authorization expires after that and the
        claim becomes `expired`. Then poll for status.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Poll until claimed">
    Poll `GET /v1/units/{unit_id}/claims/{claim_id}` until the claim reaches a terminal state. This endpoint accepts either your API key or the session token, so you can poll from your backend or a client.

    ```bash theme={null}
    curl https://api2.endstate.io/v1/units/1b2c3d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e/claims/00000000-0000-4000-8000-000000000000 \
      -H "Authorization: Bearer end_sk_test_..."
    ```

    * `claiming` — still in progress. Keep polling on a short interval.
    * `claimed` — done. Ownership transferred and confirmed on-chain.
    * `expired` / `failed` — terminal failures. Verify another tap and create a new claim.
  </Step>
</Steps>

## Handling errors

Branch on `error.code`:

| Code                               | What to do                                                                                    |
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
| `unit.not_minted`                  | The unit isn't issued yet. Pair a chip and wait for `token.status: "active"` before claiming. |
| `claim.in_progress`                | A claim is already open for this unit. Wait for it to settle, then retry if needed.           |
| `claim.already_to_recipient`       | The unit already belongs to that address — no transfer needed.                                |
| `claim.owner_unknown`              | The current owner can't be determined yet. Retry shortly.                                     |
| `session_token.invalid_or_expired` | The tap session expired. Have the user tap again to get a fresh token.                        |
| `session_token.wrong_chip`         | The session token is for a different unit. Use the token from the tap of *this* unit's chip.  |

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

## Next steps

<CardGroup cols={2}>
  <Card title="Claims" icon="arrow-right-left" href="/concepts/claims">
    The full Claims reference: fields, execution modes, and status lifecycle.
  </Card>

  <Card title="Units" icon="box" href="/concepts/units">
    The unit you just transferred — its issuance lifecycle and fields.
  </Card>
</CardGroup>
