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

# Claims

> Transfer on-chain ownership of a unit to a recipient wallet, authorized by a chip-tap session token. Endstate can submit the transaction for you, or return one for you to broadcast.

A **claim** transfers on-chain ownership of a [unit](/concepts/units) to a recipient wallet address. It is the action that hands a verified physical item to a new owner.

Claims are authorized by a [session token](/concepts/session-tokens) — the user must have just tapped that unit's chip. This binds an ownership change to a real, recent tap of the physical product.

<Note>
  A claim hands a unit to its first owner, and Endstate can submit it for you.
  To move a unit **between owners** afterwards, use a
  [transfer](/concepts/transfers) — the current owner submits that one from
  their own wallet.
</Note>

<Tip>
  For a step-by-step walkthrough from the tap to on-chain confirmation, follow
  the [Claim a unit](/guides/claim-a-unit) guide. This page is the reference.
</Tip>

## How claiming works

<Steps>
  <Step title="Verify a tap">
    The user taps the item's chip; your server verifies it (`POST /v1/chips/     {chip_id}`) and receives a session token scoped to that chip and unit.
  </Step>

  <Step title="Create the claim">
    Call `POST /v1/units/{unit_id}/claims` with the recipient's wallet `address`
    and an execution mode, authorized by the session token. Endstate signs the
    transfer authorization for the unit's token.
  </Step>

  <Step title="Settle on-chain">
    Depending on the execution mode, Endstate either submits the transfer
    transaction for you, or returns a transaction for you to broadcast.
  </Step>

  <Step title="Poll for status">
    Poll `GET /v1/units/{unit_id}/claims/{claim_id}` until `status` is
    `claimed`.
  </Step>
</Steps>

## Execution modes

The `execution` field controls who broadcasts the on-chain transaction.

| Mode                       | What Endstate does                                                                               | What you do                                                           |
| -------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| `endstate_relay` (default) | Signs **and submits** the transfer transaction on your behalf.                                   | Poll for status.                                                      |
| `client_broadcast`         | Signs the transfer and returns a ready-to-send transaction in `submission`. Does **not** submit. | Broadcast `submission` to the network yourself, then poll for status. |

With `client_broadcast`, the response includes `submission: { to, data }` — `to` is the collection's contract address and `data` is the encoded transfer call. Send it as a transaction on the network the collection was provisioned on.

## Authorization

`POST /v1/units/{unit_id}/claims` **requires a session token** (`end_sess_...`), and that token must come from a tap of the same unit's chip. An API key alone cannot create a claim — claiming always traces back to a physical tap.

`GET /v1/units/{unit_id}/claims/{claim_id}` accepts **either** an API key or a session token, so your backend or your client can poll for status. (Client-side polling from a browser requires your web origin on your organization's CORS allowlist — see [Session tokens](/concepts/session-tokens#introspecting-a-session-token).)

## Create a claim

`POST /v1/units/{unit_id}/claims`

<ParamField body="to" type="string" required>
  The recipient's wallet address (EVM address, `0x` followed by 40 hex
  characters). Ownership of the unit transfers to this address.
</ParamField>

<ParamField body="execution" type="string" default="endstate_relay">
  `endstate_relay` — Endstate submits the transaction for you.
  `client_broadcast` — Endstate returns a transaction in `submission` for you to
  broadcast.
</ParamField>

### Endstate relay

```bash theme={null}
curl -X POST https://api2.endstate.io/v1/units/22222222-2222-4222-8222-222222222222/claims \
  -H "Authorization: Bearer end_sess_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "0x1111111111111111111111111111111111111111",
    "execution": "endstate_relay"
  }'
```

```json theme={null}
{
  "id": "00000000-0000-4000-8000-000000000000",
  "unit_id": "22222222-2222-4222-8222-222222222222",
  "to": "0x1111111111111111111111111111111111111111",
  "from": "0x2222222222222222222222222222222222222222",
  "status": "claiming",
  "created_at": "2026-06-15T12:00:00.000Z"
}
```

### Client broadcast

Pass `"execution": "client_broadcast"`. The response adds a `submission` object — broadcast it to the network yourself.

```json theme={null}
{
  "id": "00000000-0000-4000-8000-000000000000",
  "unit_id": "22222222-2222-4222-8222-222222222222",
  "to": "0x1111111111111111111111111111111111111111",
  "from": "0x2222222222222222222222222222222222222222",
  "status": "claiming",
  "submission": {
    "to": "0x3333333333333333333333333333333333333333",
    "data": "0x..."
  },
  "created_at": "2026-06-15T12:00:00.000Z"
}
```

#### Broadcasting the transaction

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 account or relayer works, and the recipient never signs. You need an RPC endpoint for that network and a sender account with gas.

<CodeGroup>
  ```ts viem 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
  });

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

  ```ts ethers theme={null}
  import { Wallet, JsonRpcProvider } from "ethers";

  const wallet = new Wallet(
    process.env.SENDER_KEY,
    new JsonRpcProvider(process.env.RPC_URL),
  );

  const tx = await wallet.sendTransaction({
    to: claim.submission.to,
    data: claim.submission.data,
  });
  ```
</CodeGroup>

Use any EVM wallet client or signer — see the [viem](https://viem.sh) or [ethers](https://docs.ethers.org) docs for wallet setup. After broadcasting, poll the claim for status.

## Claim status

`GET /v1/units/{unit_id}/claims/{claim_id}` — poll this until the claim settles.

```bash theme={null}
curl https://api2.endstate.io/v1/units/22222222-2222-4222-8222-222222222222/claims/00000000-0000-4000-8000-000000000000 \
  -H "Authorization: Bearer end_sess_..."
```

| `status`   | Meaning                                                                                  |
| ---------- | ---------------------------------------------------------------------------------------- |
| `claiming` | The transfer is in progress (signed, submitted, or awaiting confirmation). Keep polling. |
| `claimed`  | Ownership transferred and confirmed on-chain. Terminal.                                  |
| `expired`  | The signed authorization expired before it settled. Terminal — create a new claim.       |
| `failed`   | The transaction failed or was dropped. Terminal — create a new claim.                    |

The signed transfer authorization is valid for **30 minutes** from claim creation; a claim that does not settle in that window becomes `expired`. With `endstate_relay` this rarely matters — Endstate submits immediately. With `client_broadcast`, broadcast the `submission` well within the window.

## The claim object

<ResponseField name="id" type="string (UUID)">
  The claim's unique identifier.
</ResponseField>

<ResponseField name="unit_id" type="string (UUID)">
  The unit being transferred.
</ResponseField>

<ResponseField name="to" type="string">
  The recipient wallet address.
</ResponseField>

<ResponseField name="from" type="string">
  The current owner's wallet address.
</ResponseField>

<ResponseField name="status" type="string">
  One of `claiming`, `claimed`, `expired`, `failed`.
</ResponseField>

<ResponseField name="submission" type="object | null">
  Present only for `client_broadcast`. The transaction to broadcast — `to` (the
  collection's contract address) and `data` (the encoded transfer call). `null`
  or absent for `endstate_relay`.
</ResponseField>

<ResponseField name="created_at" type="string (ISO-8601)">
  When the claim was created.
</ResponseField>

## Errors

| Code                               | HTTP | When                                                                                                  |
| ---------------------------------- | ---- | ----------------------------------------------------------------------------------------------------- |
| `claim.in_progress`                | 409  | A claim is already open for this unit. Wait for it to settle before starting another.                 |
| `claim.already_to_recipient`       | 409  | The unit is already owned by, or being claimed to, that address.                                      |
| `claim.owner_unknown`              | 409  | The unit's current owner cannot be determined yet.                                                    |
| `unit.not_minted`                  | 409  | The unit has not been issued yet — pair a chip and wait for `token.status: "active"` before claiming. |
| `claim.not_found`                  | 404  | No claim with that ID exists for the unit.                                                            |
| `session_token.wrong_chip`         | 403  | The session token is bound to a different unit than the one in the path.                              |
| `session_token.invalid_or_expired` | 401  | The session token is unknown or has expired — re-verify a tap.                                        |

Branch on `error.code`, not the HTTP status. See [Errors](/conventions/errors) for the full envelope.

## Next steps

<CardGroup cols={2}>
  <Card title="Session tokens" icon="key" href="/concepts/session-tokens">
    How a tap produces the session token that authorizes a claim.
  </Card>

  <Card title="Units" icon="box" href="/concepts/units">
    The unit a claim transfers, and its issuance lifecycle.
  </Card>
</CardGroup>
