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

# Unvault an item

> Let the owner of a vaulted item ask for it back from the browser, and cancel that while it waits, with @endstate-sdk/web.

The owner of a vaulted item can ask for it back. Your page sends the request
from the owner's Endstate wallet, and a waiting period starts. When it ends,
the vault records the item's dispatch with its own tap and Endstate completes
the unvault. The owner acts once.

Both calls need the same two things:

* **The owner's wallet.** Only a wallet that holds the item now can ask for it
  back or cancel that.
* **The vault request's id.** It names the vaulted item. Neither call takes a
  tap: the vault holds the item, so its owner cannot tap it.

Unvaulting is enabled per environment with
[vaulting](https://docs.endstate.io/sdks/web/vault.md); until it is, these calls answer
`not_found.resource`.

## Keep the vault request id

[`vault()`](https://docs.endstate.io/sdks/web/reference/wallet/vault) resolves with the vault request.
Save its `id` with the item on your server when the owner vaults, and hand it
to the page that offers the unvault.

```ts theme={null}
const request = await wallet.vault({ session, vaultId: vault.id });
await saveVaultRequestId(request.unitId, request.id); // your own endpoint
```

Do this at vault time. `canVault` names the request only until the item is
vaulted, and the owner cannot tap a vaulted item, because the vault holds it,
so the page has no later call that returns the id.

Your server can still find it. `vaultRequests.list()` in `@endstate-sdk/core`
returns a unit's vault requests. At most one request of a unit is open at a
time: it reads `pending`, `submitted`, `active`, `unvault_waiting` or
`unvaulting`. From `active` on, the item is in the vault, so that request is
the vaulted item's.

```ts theme={null}
// On your server, with your secret key.
const VAULTED = ["active", "unvault_waiting", "unvaulting"];
const requests = await endstate.vaultRequests.list(unitId).all();
const requestId = requests.find((request) =>
  VAULTED.includes(request.status),
)?.id;
```

## The flow

1. **Ask for the item.** `requestUnvault({ requestId })` sends the owner's
   request from their wallet and resolves once the network included it. A few
   seconds later Endstate records it: the vault request reads
   `unvault_waiting`, with `available_at` set to the end of the waiting
   period.
2. **Wait.** The brand sets the waiting period for each vault it works with:
   never less than that vault's own minimum, and up to 30 days. The vault uses
   it to prepare the item, and the item cannot change hands while it runs. The vault or the brand can place one
   hold with a reason. A hold stops the clock, and the time it lasted is added
   to `available_at` when it is lifted.
3. **Cancel, if the owner changes their mind.** While the unvault waits,
   `cancelUnvault({ requestId })` cancels it from the wallet. The item stays
   vaulted, the vault request reads `active` again, and the owner can ask
   again later.
4. **The vault dispatches the item.** Once the waiting period has ended and
   nothing holds the unvault, the item is ready to ship. The vault records the
   dispatch with a tap of the item, Endstate completes the unvault, and the
   vault request reads `unvaulted`. The vault and the owner arrange delivery
   between them.

```tsx React theme={null}
"use client";
import { EndstateWalletError } from "@endstate-sdk/web";
import { useCancelUnvault, useRequestUnvault } from "@endstate-sdk/web/react";
import { useState } from "react";

// Inside an <EndstateProvider> with a wallet configured.
// requestId is the id vault() returned, kept by your server.
// requested is whether the owner already asked: your server records it when
// requestUnvault resolves and clears it when cancelUnvault resolves.
function UnvaultPrompt({
  requestId,
  requested,
}: {
  requestId: string;
  requested: boolean;
}) {
  const request = useRequestUnvault();
  const cancel = useCancelUnvault();
  // Starts on the cancel when the owner already asked, such as after a reload.
  const [asked, setAsked] = useState(requested);

  // Each hook's error shows a refusal, so the clicks drop the rejection.
  const ask = () =>
    request.requestUnvault({ requestId }).then(
      () => setAsked(true),
      () => {},
    );
  // Once the cancel lands, offer the unvault again.
  const keep = () =>
    cancel.cancelUnvault({ requestId }).then(
      () => setAsked(false),
      () => {},
    );

  if (!asked) {
    return (
      <>
        <button disabled={request.status === "pending"} onClick={ask}>
          Send this item back to me
        </button>
        {request.error && <p role="alert">{request.error.message}</p>}
      </>
    );
  }
  return (
    <>
      <button disabled={cancel.status === "pending"} onClick={keep}>
        Keep this item in the vault
      </button>
      {cancel.error && <p role="alert">{whyNotCancelled(cancel.error)}</p>}
    </>
  );
}

function whyNotCancelled(error: Error): string {
  if (
    !(error instanceof EndstateWalletError) ||
    error.code !== "submission.not_executable"
  ) {
    return error.message;
  }
  const cancellableAt = error.details?.cancellable_at;
  if (typeof cancellableAt === "string") {
    const opens = new Date(cancellableAt).toLocaleDateString();
    return `Your item is ready to ship. You can cancel from ${opens} if it has not shipped by then.`;
  }
  return "This cannot be cancelled right now. If you only just asked for the item, try again in a moment.";
}
```

```ts Vanilla theme={null}
// `wallet` came from createWallet(); `requestId` from your server.
await wallet.requestUnvault(requestId);

// Later, while the unvault waits:
await wallet.cancelUnvault(requestId);
```

There is no call that completes an unvault. Endstate completes it after the
vault's dispatch tap.

A cancel sent straight after the request can be refused with
`submission.not_executable`, because Endstate records the request a few
seconds after the network includes it, so show the refusal and let the owner
try again after a moment.

Render one `UnvaultPrompt` per vaulted item, with `key={requestId}`. Each hook
holds the outcome of its latest call only, and `data.resourceId` names the
vault request that outcome is for.

## When cancel is refused

The owner can cancel from the moment they ask until the item is ready to
ship, including while a hold is in place. After that the vault may already be
packing the item, so `cancelUnvault()` rejects with
`submission.not_executable`:

* **Ready to ship, not dispatched.** `error.details.cancellable_at` is the
  time a cancel opens again: 7 days after the waiting period ended. If the
  vault has still not dispatched the item by then, the owner can cancel.
* **Dispatched, or already unvaulted.** The error carries no
  `cancellable_at`. The unvault can no longer be cancelled.

```ts theme={null}
import { EndstateWalletError } from "@endstate-sdk/web";

try {
  await wallet.cancelUnvault(requestId);
} catch (error) {
  if (
    error instanceof EndstateWalletError &&
    error.code === "submission.not_executable"
  ) {
    const cancellableAt = error.details?.cancellable_at;
    if (typeof cancellableAt === "string") {
      // Ready to ship. A cancel opens again at this time.
      showCancelOpensAt(new Date(cancellableAt));
    }
  }
}
```

Keys in `error.details` are the API's own, so they stay in snake\_case.

## What the page can show

The page learns what its own calls report. `requestUnvault()` and
`cancelUnvault()` resolve with `{ transactionHash, address, resourceId }`, and
a refusal says why the call could not go ahead.

The vault request carries the rest: where the unvault is, when the waiting
period ends, whether a hold stands and when the vault dispatched the item.
Your server reads it with
[`vaultRequests.get()`](https://docs.endstate.io/sdks/core/reference/vault-requests/get) in
`@endstate-sdk/core` and hands the page what it shows.

```ts theme={null}
// On your server. A secret key (end_sk_...) never goes to a page.
import { EndstateClient, secretKey } from "@endstate-sdk/core";

const endstate = new EndstateClient({
  apiKey: secretKey(process.env.ENDSTATE_API_KEY),
});

// Behind your own endpoint, once your records tie the shopper to this item.
export async function unvaultState(unitId: string, requestId: string) {
  const request = await endstate.vaultRequests.get(unitId, requestId);
  return {
    status: request.status,
    availableAt: request.availableAt,
    heldBy: request.hold?.by ?? null,
    readyToShip: request.readyToShip,
    dispatchedAt: request.dispatchedAt,
  };
}
```

Make this read on your server. It takes your secret key, which must never
reach a browser, and it does not accept a tap session token. The owner cannot
tap a vaulted item in any case, because the vault holds it.

Decide who may see the answer before you return it. Endstate does not tell
your server which shopper is asking, and its unit read names no owner, so use
your own records: the account you saved the vault request id for when the
item was vaulted.

Return only what the page shows. The request also carries `notes`, the vault
staff's free text, and a hold's `reason`; pass them on only if the owner
should read them.

The SDK returns the fields below in camelCase: `availableAt`, `readyToShip`,
`dispatchedAt`.

**status** `string`

`active`: the item is vaulted and no unvault is under way. `unvault_waiting`:
the owner asked for the item, and it is waiting or ready to ship.
`unvaulting`: the vault dispatched the item and Endstate is completing the
unvault. `unvaulted`: the unvault is complete.

**available\_at** `string | null`

When the waiting period ends. Null until Endstate records the owner's request,
and again after a cancel. Lifting a hold moves it later by the time the hold
lasted.

**hold** `object | null`

Set while a hold stands, else null. `by` is `vault` or `brand`, whoever placed
it, and `reason` is their text.

**ready\_to\_ship** `boolean`

True once the waiting period has ended and no hold stands. The vault can
dispatch the item from then on. A cancel is refused from then until 7 days
after `available_at`, when it opens again if the vault has still not
dispatched the item.

**dispatched\_at** `string | null`

When the vault recorded the item's dispatch with its tap.

What to tell the owner at each step:

| The request reads | Tell the owner |
| - | - |
| `unvault_waiting`, no hold, not ready | The item will be ready to ship on the `available_at` date. They can cancel. |
| `unvault_waiting` with a `hold` | The request is on hold, and who placed it. The date moves later when it lifts. |
| `unvault_waiting`, `ready_to_ship` true | The item is ready to ship and the vault is preparing it. |
| `unvaulting` | The vault dispatched the item on the `dispatched_at` date. |
| `unvaulted` | The unvault is complete. The vault and the owner arrange delivery. |
| `active` | The item is vaulted. No unvault is under way, or the owner cancelled it. |

### Follow an unvault

Webhooks announce each step, so your server does not have to poll. Subscribe
an endpoint to `vault_request.unvault_requested`, `vault_request.unvault_held`,
`vault_request.unvault_hold_lifted`, `vault_request.unvault_dispatched`,
`vault_request.unvaulted` and `vault_request.unvault_cancelled`, and Endstate
sends it the request, as this read returns it, when the owner's request or
cancel is recorded, when a hold is placed or lifted, when the vault dispatches
and when the unvault completes. See
[Webhooks](https://docs.endstate.io/concepts/webhooks.md#vault-requests). No event is sent when the
waiting period ends: the item is ready to ship at `available_at` if no hold
stands. `ready_to_ship` in an event is its value when Endstate recorded the
change, so compute it from `available_at` and `hold`.

Reading the request stays available, for a page opened before an event
arrived or a server with no endpoint. Read it when the owner opens the page,
and while they watch it poll no more often than the page needs.

Endstate records the owner's request a few seconds after `requestUnvault()`
resolves, so a read made straight away can still say `active`. Keep polling
until it reads `unvault_waiting`.

Stop polling when the request reads `unvaulted`: the unvault is complete and
the request is closed. A request that reads `active` after it read
`unvault_waiting` has no unvault under way, usually because the owner
cancelled it, so there is nothing to follow until they ask again.

Treat each read as the request's current state, not as a one-way log.
Rarely, the network revises its latest records, and a status then moves back:
`unvaulted` to `unvaulting` or `active`, or `unvault_waiting` or `unvaulting`
to `active`. Read the request again when the owner next opens the page
instead of keeping `unvaulted` as final.

Each such change sends a webhook event with a higher revision, and a type you
already handled can arrive again, so act once per request, not once per
event. A request that reads `active` again after the vault dispatched the
item corrects the record; it does not mean the item is back in the vault. A
hold does not survive the change and is placed again if the unvault is
requested again. [Corrections](https://docs.endstate.io/concepts/webhooks.md#corrections) lists each
case.

This read is rate limited. Each response carries the `X-RateLimit-Limit`,
`X-RateLimit-Remaining` and `X-RateLimit-Reset` headers. A refused read
answers 429 `rate_limit.exceeded` with `Retry-After`: wait at least that long
before the next read, and back off if it repeats. Limits are set per
organization, as
[Errors](https://docs.endstate.io/conventions/errors.md#generic-and-rate-limiting) describes. Poll from
your server and share one answer between page views of the same item, not one
read per view.

## Outcomes and the wallet

Each hook returns `{ status, data, error, reset }`, with `status`
`"idle" | "pending" | "success" | "error"`. It holds the outcome of its latest
call, and `data.resourceId` names the vault request that outcome is for. A
page that shows several vaulted items renders one component per item, with
`key={requestId}`, so each item has its own outcome.

An outcome belongs to the wallet that sent it and to the sign-in. Another
wallet, a sign-out, a sign-in or `reset()` returns the hook to `"idle"`, and a
call still in flight for the old one never lands. A new tap does not reset
these two hooks, unlike the [vault hooks](https://docs.endstate.io/sdks/web/vault.md#outcomes-and-the-wallet):
an unvault takes no tap, so a tap of another item leaves its outcome in place.

Both sends go from the owner's wallet with the cost covered by Endstate; the
owner never pays for them. A call resolves once the network included the
send. Endstate records it a few seconds later, so a `cancelUnvault()` sent
straight after `requestUnvault()` resolves can reject with
`submission.not_executable`. Retry it after a moment.

If the item changes hands while it is vaulted, the new owner's wallet sends
these calls and the previous owner's is refused with `auth.forbidden`.

## Errors

**submission.not\_executable** `409`

`requestUnvault()` found an unvault already under way or finished, or
`cancelUnvault()` found nothing it can cancel: no unvault is waiting, or the
item is ready to ship. `error.details.cancellable_at`, the time a cancel opens
again, is present only while the item is ready to ship and not dispatched.
Nothing was sent.

**unvault.in\_progress** `409`

An earlier unvault of the item is still settling, so a new one cannot start
yet. Retry after `error.details.retry_at`.

**vault.request\_not\_found** `404`

The id names no vaulted item in this environment: it is unknown, or its
request never reached the vault because it is still pending or was cancelled,
rejected or expired.

**auth.forbidden** `403`

This wallet does not hold the item now.

**wallet.sponsorship\_unavailable** `frame`

The send cannot be covered right now. Nothing was sent, and the owner is never
asked to pay. Retry later.

**not\_found.resource** `404`

Vaulting is not enabled for this environment.

See [Errors](https://docs.endstate.io/conventions/errors.md#unvaults) for every unvault code, and
[`EndstateWalletError`](https://docs.endstate.io/sdks/web/reference/wallet/EndstateWalletError) for
the wallet's own.

## See also

> [card] [useRequestUnvault](https://docs.endstate.io/sdks/web/reference/react/useRequestUnvault.md)
>
> The hook that sends the owner's request to unvault.

> [card] [Vault an item](https://docs.endstate.io/sdks/web/vault.md)
>
> How the item got into the vault, and where the request id comes from.

> [card] [vaultRequests.get](https://docs.endstate.io/sdks/core/reference/vault-requests/get.md)
>
> The server-side read that carries the unvault's state.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.