address, and the
session token the tap opened authorizes the move.
That is what sets a transfer apart from a claim, which is the
first assignment of ownership out of a tap. A claim has no prior owner, so
Endstate can submit it for you. A transfer has one, so only that current owner
can complete it: Endstate authorizes the move and hands back a transaction, but
never moves an item on an owner’s behalf.
The flow
React is the primary path.useTransfer() gives you a transfer({ to }) call
bound to the verified session the landing tap opened, and useSession() holds
the item being moved. The browser quickstart covers how
the tap is captured and verified, and Tap sessions
covers what the session is. Prefer the hooks; the vanilla
session.transfers.create is the same call without React.
transfer({ to }) resolves as soon as Endstate prepares and authorizes the
transaction, returning the Transfer with its transaction attached. The hook
exposes that result as data, any failure as error, and where the operation
stands as status - a TransferStatus of idle, preparing, settling,
confirmed, expired, failed, or error. It starts idle, moves to
preparing while the request is in flight, and lands on error if the request
throws. Because wait defaults to false, the hook stops there: it does not
poll settlement, since a transfer cannot settle until the current owner
broadcasts the returned transaction, which the hook never does. Read the
returned transfer’s own status (prepared) and observe settlement out of
band. Passing wait: true continues through settling to confirmed,
expired, or failed, but only after the owner has broadcast - so most
transfers leave it false.
A transfer takes one field:
string
required
The recipient’s EVM wallet address (
0x followed by 40 hex characters) - the
account that will receive the item. If the recipient is getting an Endstate
wallet on this page, this is the address from the wallet
((await wallet.ready()).address, or useWallet().account?.address under the
provider).The transfer reads
unit_id from the session’s scope, so you never pass it. A
session freshly opened by verify() - including the landing tap autoVerify
captures - already carries that scope; one you adopted from a bare
token does not,
and the call throws EndstateSessionScopeError until the session’s scope is
refreshed.Choosing who submits
A transfer is always put through by the current owner - Endstate authorizes the move but never submits it for you, and unlike a claim there is no Endstate-submitted mode and noexecution choice. The move is rejected unless
the owner’s own account sends it. What is changing is the signer the owner
reaches for.
Endstate wallet (coming soon)
An owner who holds an Endstate wallet will broadcast the transfer straight from it, with no separate wallet library. The Endstate web wallet provisions anaddress today but exposes no signing surface; signing operations arrive in a
later minor release. Until then, the owner broadcasts with their own signer,
below.
The current owner broadcasts
transfer (and vanilla session.transfers.create) returns a transaction
object, { to, data }, that Endstate has already authorized with its signature
and leaves for the current owner to send. The owner is the from address on the
returned transfer, and the move is rejected unless that same account sends it -
no relayer or other funded account can stand in. Endstate never submits a
transfer, so the current owner broadcasts the returned transaction with their
own signer, then reads settlement from the transfer record. The exact
transaction shape and how to broadcast it live in
session.transfers.create.
Broadcast well within about 30 minutes of preparing the transfer: the signed
authorization expires after that and the transfer settles expired.
Settlement
A transfer is created asprepared and lands on one of three terminal statuses:
prepared- authorized and waiting for the current owner to broadcast it.confirmed- done. Ownership has moved and settled.expired- the signed authorization’s window (about 30 minutes) passed before the owner broadcast it. Create a new transfer.failed- the transaction did not settle. Create a new transfer.
useTransfer().data.status, or opt into hook-driven
waiting with transfer({ to, wait: true }) once the owner has broadcast. With
the vanilla client, session.transfers.waitUntilSettled polls until the status
is terminal so you do not write the loop:
session.transfers.get(transferId), or
endstate.transfers.get(unitId, transferId) from a secret-key client on your
backend. On expired or failed, create a new transfer.
Idempotency
Creating a transfer is a mutation, so it is safe to retry with an idempotency key. PassidempotencyKey; omit it and core generates one for you. Replaying
the same key with the same body returns the original transfer rather than
opening a second one; the same key with a different body is rejected as a
conflict.
session.transfers.create({ to }, { idempotencyKey }).
Errors
Every failure throws a typed error carrying a branchablecode; in React it
surfaces as useTransfer().error. For how those errors behave and what retries
automatically, see Errors and retries; for the
full transfer code list - unit.not_minted, transfer.owner_unknown,
transfer.in_progress, transfer.already_to_recipient,
session_token.wrong_chip, and the rest - see
session.transfers.create.
Next steps
session.transfers.create
The transfer request body, response, transaction shape, and every error
code.
Claim ownership
Assign first ownership out of a tap - the sibling action to a transfer.
Wallet
Provision the recipient address a transfer sends the item to.

