> ## Documentation Index
> Fetch the complete documentation index at: https://docs.li.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Escrow status

> GET /intent-factory/status: read the state of the escrow behind a cross-chain flow, with its expiry and transaction hashes, and map it to what your user sees.

`GET /intent-factory/status` returns the status of one escrow that a [`continuation.settle`](https://docs.li.fi/composer/composer-api/ops/continuation-settle) node registered. The status comes unchanged from LI.FI's settlement service (`intent-factory`). Use it to track a [continuation](https://docs.li.fi/composer/composer-api/concepts/cross-chain-flows.md), cross-chain or same-chain, to completion or refund. For the bridge of a cross-chain flow, poll LI.FI's `/v1/status`.

> [!NOTE]
>
> LI.FI plans a dedicated settlement status endpoint, which can have a different path and response shape. This endpoint keeps working while you move to the new one.

## Request

```bash theme={"system"}
curl -s "https://composer.li.quest/intent-factory/status?chainId=8453&address=0x742d35cc6634c0532925a3b844bc9e7595f7b8d0" \
  -H "x-lifi-api-key: $LIFI_API_KEY"
```

The endpoint lives on the Composer base URL, `https://composer.li.quest`. It is a read-only `GET`: polling it never changes the escrow.

### Authentication

**x-lifi-api-key** `string` *required*

Your LI.FI API key, the same key you pass to `createComposeSdk`. A request without a key fails with `401`. Keep the key on your server; see [Before you start](https://docs.li.fi/composer/composer-api/overview.md#before-you-start).

### Query parameters

Both values come from the compile response: `settlements[<settle id>].escrow.chainId` and `settlements[<settle id>].escrow.address`. Together they identify one escrow.

**chainId** `integer` *required*

The chain the escrow lives on. A positive integer. For a bridge-funded settle node this is the bridge's destination chain, not the chain the user signed on.

**address** `string` *required*

The escrow address: `0x` followed by 40 hex characters. A mixed-case address must carry a valid EIP-55 checksum, or the request fails with `400`. An all-lowercase address is always accepted, and the lookup ignores case.

## Response

A `200` response wraps the escrow's state in `data`:

```json theme={"system"}
{
  "success": true,
  "data": {
    "chainId": 8453,
    "address": "0x742D35Cc6634C0532925a3B844bC9e7595F7B8d0",
    "status": "COMPLETED",
    "expiresAt": "2026-10-27T16:00:00.000Z",
    "depositTxHash": "0x…",
    "sourceExecuteTxHash": null,
    "destinationTxHash": "0x…",
    "refundTxHash": null
  }
}
```

**success** `boolean`

`true` on a `200`.

**data** `object`

**properties**

**chainId** `integer`

The chain the escrow lives on.

**address** `string`

The escrow address, in EIP-55 checksum form, whatever case you sent.

**status** `string`

The escrow's lifecycle state. One of the [status values](#status-values) below.

**expiresAt** `string`

ISO-8601 instant after which the destination flow can no longer run and the escrow becomes refundable. It matches `settlements[<settle id>].escrow.expiresAtMs` from the compile response.

**depositTxHash** `string | null`

The transaction that funded the escrow, when known. `null` until the deposit is observed, and always `null` for a native-coin deposit. Do not rely on it to detect funding; use `status`.

**sourceExecuteTxHash** `string | null`

Always `null` for a continuation.

**destinationTxHash** `string | null`

The transaction on the escrow's chain that ran the destination flow and delivered the outcome. `null` until the escrow reaches `COMPLETED`.

**refundTxHash** `string | null`

The refund transaction. `null` unless the escrow was refunded.

## Status values

| `status` | Meaning | What to do |
| - | - | - |
| `AWAITING_FUNDS` | The escrow is registered and waiting: its balance is below `escrow.floor`, or a failed run re-armed it for a retry (see `FAILED_RETRYABLE`). | Submit the funding transaction, or wait for the bridge to deliver. After a retry, wait. |
| `READY` | The balance reached the minimum funding (`escrow.floor`). The destination flow is ready to run. | Wait. |
| `EXECUTING` | LI.FI's settlement service is running the destination flow. | Wait. |
| `COMPLETED` | The destination flow ran and the settlement contract forwarded the outcome to `destination`. `destinationTxHash` holds the transaction. | Done. |
| `FAILED_RETRYABLE` | An attempt to run the destination flow failed for a temporary reason. The escrow returns to `AWAITING_FUNDS` and the run is retried, or the escrow moves to `FAILED_FINAL`. | Wait. |
| `SIMULATION_FAILURE` | The run failed in simulation, before any transaction was sent. A temporary failure is retried; a failure that cannot succeed (for example, the destination flow would deliver less than the minimum received) moves to `FAILED_FINAL`. | Wait. |
| `FAILED_FINAL` | The run cannot succeed and will not be retried. The refund starts immediately. | Wait for `REFUNDED`. |
| `EXPIRED` | The expiry passed before a run completed. The settlement service no longer runs the destination flow. A funded escrow moves on to `AWAITING_REFUND`; an escrow that was never funded stays `EXPIRED`. | Wait for `REFUNDED`, or stop as described in [Polling](#polling). |
| `AWAITING_REFUND` | The refund to the `refund` address is in progress. | Wait. |
| `REFUNDED` | The escrow's balance of the funding token was returned to the `refund` address. `refundTxHash` holds the transaction. | Done. |
| `REFUND_FAILED` | The refund failed. A refund that failed for a temporary reason is retried automatically a limited number of times, which moves the escrow back to `AWAITING_REFUND`. | If the status stays `REFUND_FAILED`, contact LI.FI support with the escrow's `chainId` and `address`. |
| `SETTLING`, `SETTLEMENT_FAILED` | Used for cross-chain settlement only. A continuation never enters these states. | — |

> [!NOTE]
>
> A new status value can be added in a minor Composer release. Handle unknown values in your client, for example by continuing to poll.

### Typical paths

* **Happy path:** `AWAITING_FUNDS → READY → EXECUTING → COMPLETED`. The destination flow runs at most once per escrow.
* **Funded but not run:** `AWAITING_FUNDS → EXPIRED → AWAITING_REFUND → REFUNDED` for an escrow that holds funds below `escrow.floor`, and `READY → EXPIRED → AWAITING_REFUND → REFUNDED` for a `READY` escrow whose destination flow does not run before its expiry.
* **Never funded:** `AWAITING_FUNDS → EXPIRED`, and it stays `EXPIRED`. There is nothing to refund. This happens when the user never sends the transaction, the bridge fails before delivering, or an external payer never pays.
* **Failed run:** `READY → EXECUTING → FAILED_FINAL → AWAITING_REFUND → REFUNDED`. A temporary failure goes back to `AWAITING_FUNDS` and is retried until it succeeds, fails for good, or the escrow expires.

The refund is automatic and needs no action from the user. Funds that land after expiry are never used for a run. For about an hour after expiry (six checks, ten minutes apart), the settlement service keeps checking the escrow and refunds any balance it finds.

> [!WARNING]
>
> Do not send funds to an escrow after `expiresAt`. Funds that land after the settlement service stops checking, or after the escrow reached `COMPLETED` or `REFUNDED`, stay in the escrow, which `escrow.owner` controls; contact LI.FI support with the escrow's `chainId` and `address` to recover them.

## Errors

Errors use the same envelope: `{ "success": false, "error": { "type", "message" } }`.

| HTTP | `error.type` | When | What to do |
| - | - | - | - |
| `400` | `VALIDATION_ERROR` | `chainId` is missing or not a positive integer, `address` is not 40 hex characters, a mixed-case address has a wrong checksum, or the chain and address do not form a valid escrow key. `error.issues` lists each problem with its `path`. | Fix the query. Send the address in lowercase to skip the checksum check. |
| `401` | `UNAUTHORIZED` | The `x-lifi-api-key` header is missing (`API key is required`) or the key is invalid or disabled. | Send a valid API key. |
| `404` | `BUNDLE_NOT_FOUND` | No escrow is registered for this `chainId` and `address`. The endpoint does not check whether the chain is served. | Check that both values come from the same compile response. |
| `429` | | Rate limit exceeded. The limits are the same as for `POST /compose`. | Back off and retry. |
| `500` | `SERVER_ERROR` | The settlement service answered in a form the API could not read. | Retry later; contact LI.FI support if it persists. |
| `503` | `SERVICE_UNAVAILABLE` | The settlement service is temporarily unreachable. The response carries a `Retry-After` header (seconds). | Retry after `Retry-After`. |

Example `404`:

```json theme={"system"}
{
  "success": false,
  "error": {
    "type": "BUNDLE_NOT_FOUND",
    "message": "No settlement bundle for 0x742d35cc6634c0532925a3b844bc9e7595f7b8d0 on chain 8453"
  }
}
```

## What to show your user

Most statuses are steps your user doesn't need to see. A pending screen needs about five states. Map the statuses to them, and adapt the copy to your flow:

| Show | `status` | Suggested copy |
| - | - | - |
| **Bridging** | `AWAITING_FUNDS`, before the escrow has been `READY` | "Moving your funds to Base." |
| **Finishing** | `READY`, `EXECUTING`, `FAILED_RETRYABLE`, `SIMULATION_FAILURE`; `AWAITING_FUNDS` once the escrow has been `READY` at least once (a retry); `EXPIRED` until it moves to `AWAITING_REFUND` or reaches the never-funded cutoff | "Completing your deposit on Base." Retries are automatic: don't show them as errors. The status alone can't tell a funded `EXPIRED` escrow from one that was never funded, so wait for the next status. |
| **Done** | `COMPLETED` | "Deposit complete." Link `destinationTxHash`. |
| **Refunded** | `FAILED_FINAL`, `AWAITING_REFUND`, `REFUNDED` | "We couldn't complete the deposit. Your USDC is being returned to your address on Base." Link `refundTxHash` once it's set. |
| **Needs help** | `REFUND_FAILED` that doesn't recover after the automatic retries; `EXPIRED` past the never-funded cutoff (see [Polling](#polling)) | Refund failed: point your user to support with the escrow's `chainId` and `address`. Never funded: check the status of the [bridge](https://docs.li.fi/composer/composer-api/ops/lifi-bridge.md#track-the-delivery) or the [Intents order](https://docs.li.fi/composer/composer-api/ops/lifi-intents-order.md#track-and-refund-the-order); recovery follows its own refund path. An Intents order's refund isn't automatic: someone calls `lifiIntents.refund` after the order expires. |

Treat an unknown status as **Finishing** and keep polling.

## Polling

Poll the endpoint to read the escrow's current state. Stop at `COMPLETED` or `REFUNDED`. A `REFUND_FAILED` refund is retried automatically a limited number of times, so keep polling for a while before you treat it as final. Also stop when the status is still `EXPIRED` well past `expiresAt`: the escrow was never funded. The settlement service keeps checking an expired escrow for about an hour (six checks, ten minutes apart), so the sample waits 90 minutes. On `503`, wait the `Retry-After` seconds, then retry.

```ts theme={"system"}
import { setTimeout as sleep } from 'node:timers/promises';

const DONE = ['COMPLETED', 'REFUNDED'];
const NEVER_FUNDED_AFTER_MS = 90 * 60 * 1000; // about an hour of checks, plus a margin
const REFUND_RETRY_WAIT_MS = 30 * 60 * 1000; // REFUND_FAILED is retried automatically for a while

export async function waitForEscrow(chainId: number, address: string) {
  const url = new URL('https://composer.li.quest/intent-factory/status');
  url.searchParams.set('chainId', String(chainId));
  url.searchParams.set('address', address.toLowerCase()); // lowercase skips the checksum check

  let refundFailedSince: number | undefined;
  for (;;) {
    const res = await fetch(url, { headers: { 'x-lifi-api-key': process.env.LIFI_API_KEY! } });
    if (res.status === 503) {
      await sleep(Number(res.headers.get('Retry-After') ?? '10') * 1000);
      continue;
    }
    const body = await res.json();
    if (!body.success) throw new Error(`${res.status} ${body.error.type}: ${body.error.message}`);

    const { status, expiresAt } = body.data;
    if (DONE.includes(status)) return body.data;
    if (status === 'REFUND_FAILED') {
      refundFailedSince ??= Date.now();
      if (Date.now() - refundFailedSince > REFUND_RETRY_WAIT_MS) return body.data; // needs help
    } else {
      refundFailedSince = undefined;
    }
    if (status === 'EXPIRED' && Date.now() > Date.parse(expiresAt) + NEVER_FUNDED_AFTER_MS) return body.data; // never funded
    await sleep(10_000);
  }
}
```

Call it with the escrow from the compile response: `waitForEscrow(escrow.chainId, escrow.address)`.

## See also

* [Cross-chain flows](https://docs.li.fi/composer/composer-api/concepts/cross-chain-flows.md): how an escrow is funded, run and refunded.
* [Build a cross-chain flow](https://docs.li.fi/composer/composer-api/guides/build-a-cross-chain-flow.md): the end-to-end guide.
* [`continuation.settle` errors](https://docs.li.fi/composer/composer-api/ops/continuation-settle#errors): errors from `POST /compose` for a settle node.


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