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

# Status & Recovery

> How to poll transfer status and handle different outcomes

Cross-chain transfers take time to complete. This page explains how to poll the status endpoint, interpret status values, and handle each outcome correctly.

## Status Endpoint

```bash theme={"system"}
GET https://li.quest/v1/status
```

### Parameters

| Parameter | Required | Description | Source |
| - | - | - | - |
| `txHash` | **Yes** | Transaction hash from execution | From `sendTransaction` result |
| `fromChain` | No | Source chain ID (recommended) | From `quote.action.fromChainId` |
| `toChain` | No | Destination chain ID | From `quote.action.toChainId` |
| `bridge` | No | Bridge/tool identifier | From `quote.tool` |

<Note>
  Only `txHash` is required. Providing `fromChain` speeds up the response significantly.
</Note>

### Example Request

```bash theme={"system"}
curl "https://li.quest/v1/status?txHash=0xabc123..."
```

Or with optional parameters for faster response:

```bash theme={"system"}
curl "https://li.quest/v1/status?txHash=0xabc123...&fromChain=1&toChain=42161&bridge=stargateV2"
```

***

## Status Response

```json theme={"system"}
{
  "transactionId": "0x1695bded7c1634dce4a200bdec72be0ed6cc5f7153ec2dc832b271d790eb00c8",
  "sending": {
    "txHash": "0x1695bded7c1634dce4a200bdec72be0ed6cc5f7153ec2dc832b271d790eb00c8",
    "txLink": "https://etherscan.io/tx/0x1695bded7c1634dce4a200bdec72be0ed6cc5f7153ec2dc832b271d790eb00c8",
    "chainId": 1,
    "amount": "10000000",
    "token": {
      "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "chainId": 1,
      "symbol": "USDC",
      "decimals": 6,
      "name": "USD Coin",
      "coinKey": "USDC",
      "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
      "priceUSD": "0.999606"
    },
    "amountUSD": "9.9961",
    "gasPrice": "65729222",
    "gasUsed": "220266",
    "gasToken": {
      "address": "0x0000000000000000000000000000000000000000",
      "chainId": 1,
      "symbol": "ETH",
      "decimals": 18,
      "name": "ETH",
      "coinKey": "ETH",
      "priceUSD": "2923.27"
    },
    "gasAmount": "14477912813052",
    "gasAmountUSD": "0.0423",
    "timestamp": 1737625200
  },
  "receiving": {
    "txHash": "0xdef456789abc...",
    "txLink": "https://arbiscan.io/tx/0xdef456789abc...",
    "chainId": 42161,
    "amount": "9965731",
    "token": {
      "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
      "chainId": 42161,
      "symbol": "USDC",
      "decimals": 6,
      "name": "USD Coin",
      "coinKey": "USDC",
      "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
      "priceUSD": "0.999606"
    },
    "amountUSD": "9.9618",
    "timestamp": 1737625204
  },
  "lifiExplorerLink": "https://explorer.li.fi/tx/0x1695bded7c1634dce4a200bdec72be0ed6cc5f7153ec2dc832b271d790eb00c8",
  "fromAddress": "0x552008c0f6870c2f77e5cC1d2eb9bdff03e30Ea0",
  "toAddress": "0x552008c0f6870c2f77e5cC1d2eb9bdff03e30Ea0",
  "tool": "across",
  "status": "DONE",
  "substatus": "COMPLETED"
}
```

***

## Status Values

### Primary Status

| Status | Meaning | Action |
| - | - | - |
| `NOT_FOUND` or HTTP 404 (`code: 1003`) | Not indexed yet, or never mined | Poll with backoff; if it persists, query the original source-chain receipt before investigating replacement (see [Never-mined transactions](#never-mined-transactions)) |
| `PENDING` | Transfer in progress | Continue polling |
| `DONE` | Transfer finished | Check `substatus` for outcome |
| `FAILED` | Transfer failed | Handle error |

### Substatus (when status = DONE)

| Substatus | Meaning | User Outcome |
| - | - | - |
| `COMPLETED` | Success | Received exact requested token |
| `PARTIAL` | Partial success | Received different token (full value) |
| `REFUNDED` | Failed with refund | Tokens returned to sender |

***

## Decision Matrix

```
IF status === "NOT_FOUND" OR HTTP 404 (code 1003)
  → Wait and poll again for the source chain's inclusion window
  → Still unknown? Query the original hash on the source chain first
  → Receipt exists? Handle source failure or continue cross-chain tracking
  → No receipt? Check pending state; investigate a mined same-sender, same-nonce replacement
  → Evidence incomplete? Keep outcome unknown; a polling timeout is not failure

IF status === "PENDING"
  → Wait and poll again (transfer in progress)

IF status === "DONE"
  IF substatus === "COMPLETED"
    → Success! User received toToken on toChain
  
  IF substatus === "PARTIAL"
    → Partial success - see Partial Completion guide
  
  IF substatus === "REFUNDED"
    → Failed - tokens returned to user on fromChain

IF status === "FAILED"
  → Permanent failure - inform user, may need manual recovery
```

***

## Polling Implementation

### Recommended Strategy

| Attempt | Wait Time | Total Time |
| - | - | - |
| 1-6 | 10 seconds | 1 minute |
| 7-12 | 30 seconds | 4 minutes |
| 13-24 | 60 seconds | 16 minutes |
| 25+ | 120 seconds | Until timeout |

These examples bound automatic polling but do not classify unknown hashes as failed or replaced. Both HTTP 404 and a `NOT_FOUND` body remain non-terminal here. On timeout, preserve the unresolved record and use the source-chain reconciliation checks below.

### JavaScript Implementation

```javascript theme={"system"}
async function pollTransferStatus(params, options = {}) {
  const { txHash, bridge, fromChain, toChain } = params;
  const {
    maxDuration = 30 * 60 * 1000,       // 30 minutes
    onStatusUpdate = () => {}
  } = options;
  
  const startTime = Date.now();
  let attempt = 0;
  
  while (Date.now() - startTime < maxDuration) {
    attempt++;
    
    try {
      const response = await fetch(
        `https://li.quest/v1/status?txHash=${txHash}&bridge=${bridge}&fromChain=${fromChain}&toChain=${toChain}`
      );
      
      if (response.status === 404) {
        // No replacement/failure inference: LI.FI may not have indexed a mined tx.
        // Reconcile against the source chain separately; timeout preserves uncertainty.
        await sleep(getWaitTime(attempt));
        continue;
      }

      if (!response.ok) {
        throw new Error(`Status API error: ${response.status}`);
      }
      
      const status = await response.json();
      
      // Notify caller of status update
      onStatusUpdate(status, attempt);
      
      // Check for terminal states
      if (status.status === 'DONE') {
        return {
          success: status.substatus === 'COMPLETED',
          partial: status.substatus === 'PARTIAL',
          refunded: status.substatus === 'REFUNDED',
          status
        };
      }
      
      if (status.status === 'FAILED') {
        return {
          success: false,
          failed: true,
          status
        };
      }
      
    } catch (error) {
      console.error(`Polling error (attempt ${attempt}):`, error.message);
      // Transient errors (5xx, network): keep polling. 404 is handled above.
    }
    
    // Calculate wait time with backoff
    const waitTime = getWaitTime(attempt);
    await sleep(waitTime);
  }
  
  throw new Error('Status polling timeout');
}

function getWaitTime(attempt) {
  if (attempt <= 6) return 10000;      // 10 seconds
  if (attempt <= 12) return 30000;     // 30 seconds
  if (attempt <= 24) return 60000;     // 60 seconds
  return 120000;                        // 2 minutes
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
```

### Python Implementation

```python theme={"system"}
import time
import requests

def poll_transfer_status(tx_hash, bridge, from_chain, to_chain,
                         max_duration=1800, on_update=None):
    """
    Poll transfer status until completion or timeout.
    
    Args:
        tx_hash: Transaction hash
        bridge: Bridge tool name
        from_chain: Source chain ID
        to_chain: Destination chain ID
        max_duration: Maximum polling duration in seconds (default 30 min)
        on_update: Optional callback for status updates
    
    Returns:
        dict with success, partial, refunded, failed flags and status
    """
    start_time = time.time()
    attempt = 0
    
    while time.time() - start_time < max_duration:
        attempt += 1
        
        try:
            url = (
                f"https://li.quest/v1/status"
                f"?txHash={tx_hash}"
                f"&bridge={bridge}"
                f"&fromChain={from_chain}"
                f"&toChain={to_chain}"
            )
            
            response = requests.get(url)
            if response.status_code == 404:
                # LI.FI may not have indexed a mined tx. Reconcile on the source
                # chain separately; neither 404 nor timeout proves replacement/failure.
                time.sleep(get_wait_time(attempt))
                continue
            response.raise_for_status()
            status = response.json()
            
            # Notify callback
            if on_update:
                on_update(status, attempt)
            
            # Check terminal states
            if status.get('status') == 'DONE':
                substatus = status.get('substatus')
                return {
                    'success': substatus == 'COMPLETED',
                    'partial': substatus == 'PARTIAL',
                    'refunded': substatus == 'REFUNDED',
                    'status': status
                }
            
            if status.get('status') == 'FAILED':
                return {
                    'success': False,
                    'failed': True,
                    'status': status
                }
                
        except Exception as e:
            print(f"Polling error (attempt {attempt}): {e}")
        
        # Wait with backoff
        wait_time = get_wait_time(attempt)
        time.sleep(wait_time)
    
    raise Exception('Status polling timeout')


def get_wait_time(attempt):
    if attempt <= 6:
        return 10
    if attempt <= 12:
        return 30
    if attempt <= 24:
        return 60
    return 120
```

***

## Handling Each Outcome

### COMPLETED - Success

```javascript theme={"system"}
if (result.success) {
  const { receiving } = result.status;
  
  console.log('Transfer successful!');
  console.log(`Received: ${receiving.amount} ${receiving.token.symbol}`);
  console.log(`On chain: ${receiving.chainId}`);
  console.log(`Tx: ${receiving.txHash}`);
  
  // No further action needed
}
```

### PARTIAL - Different Token Received

```javascript theme={"system"}
if (result.partial) {
  const { receiving } = result.status;
  
  console.log('Transfer completed with different token');
  console.log(`Received: ${receiving.amount} ${receiving.token.symbol}`);
  
  // User may want to swap to their intended token
  // See: Partial Completion guide
}
```

### REFUNDED - Tokens Returned

```javascript theme={"system"}
if (result.refunded) {
  const { sending } = result.status;
  
  console.log('Transfer failed - tokens refunded');
  console.log(`Refunded: ${sending.amount} ${sending.token.symbol}`);
  console.log(`On chain: ${sending.chainId}`);
  
  // User's tokens are back on the source chain
  // May retry with different parameters
}
```

### FAILED - Permanent Failure

```javascript theme={"system"}
if (result.failed) {
  console.error('Transfer failed permanently');
  console.error('Error:', result.status.error);
  
  // May need manual intervention
  // Contact support with transaction details
}
```

***

## Estimated Transfer Times

<Tip>
  The quote response includes `estimate.executionDuration` (in seconds) which gives you the expected transfer time for that specific route. Always use this value rather than general estimates.
</Tip>

Transfer times vary significantly based on the bridge used and network conditions. The `executionDuration` field in the quote response provides the most accurate estimate for each specific route.

***

## Timeout Handling

If polling times out:

1. **Don't assume failure** - The transfer may still complete
2. **Save the transaction details** - txHash, bridge, fromChain, toChain
3. **Provide manual check** - User can check status later
4. **Link to explorer** - Provide transaction explorer URLs

```javascript theme={"system"}
function getExplorerUrl(chainId, txHash) {
  const explorers = {
    1: 'https://etherscan.io/tx/',
    42161: 'https://arbiscan.io/tx/',
    10: 'https://optimistic.etherscan.io/tx/',
    137: 'https://polygonscan.com/tx/',
    8453: 'https://basescan.org/tx/'
  };
  
  return `${explorers[chainId] || 'https://blockscan.com/tx/'}${txHash}`;
}

// On timeout
console.log('Status check timed out. The transfer may still be processing.');
console.log(`Check source tx: ${getExplorerUrl(fromChain, txHash)}`);
console.log('You can also check status manually at: https://li.fi/');
```

***

### Never-mined transactions

If `/status` keeps answering `404` with `code: 1003`, first query the original hash's receipt on the correct source chain. A receipt means the original transaction was mined: handle source execution failure or keep tracking the cross-chain outcome if it succeeded. Without a receipt, check whether the transaction is pending. For ordinary EVM account transactions, an advanced nonce is only a reason to investigate, not proof of replacement. Confirm a different mined transaction with the same source chain, sender and nonce, then inspect its intent before associating it with the original order. A cancellation does not continue the transfer. Use `GET /v1/analytics/transfers?wallet=<address>&fromTimestamp=<unix>&toTimestamp=<unix>&status=ALL` for candidates, not proof. Missing evidence remains unknown even after polling stops. Full recipe: [Unknown and never-mined transaction hashes](/introduction/user-flows-and-examples/status-tracking#unknown-and-never-mined-transaction-hashes).

```json theme={"system"}
{ "message": "Transaction hash '0x…' not found on chain '1'", "code": 1003 }
```

***

## Related Pages

* [Partial Completion](/agents/workflows/partial-completion) - Handle PARTIAL status
* [Transaction Execution](/agents/workflows/execution) - Execute transfers
* [Error Playbooks](/agents/reference/error-playbooks) - Handle failures


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