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

# Bridge Status

> [Alpha Preview] Look up the status of a bridge intent, or long-poll until it terminates.

<Warning>
  **Alpha Preview** — Endpoints and response shape may change without notice.
</Warning>

Two endpoints share this page:

* `GET /api/2/bridge/status/{id}` — returns the current row immediately.
* `GET /api/2/bridge/status/{id}/wait` — long-polls server-side until the
  intent reaches a terminal state (or the timeout elapses).

## Path parameter

`id` accepts **any** of these — pass whichever you have:

* `intentId` from `/quote` (format `xxxxxxx-xxxxxxx-xxx`).
* The on-chain `bytes32` intent ID (EVM only, emitted by `MobulaBridge`).
* The deposit TX hash.
* The fill TX hash.

If nothing matches, the response is **not** an error — it's

```json theme={null}
{ "data": { "id": "...", "status": "pending", "message": "Intent not found — deposit may still be processing" } }
```

This is the expected state right after the deposit is broadcast but before
the solver has indexed it. Keep polling.

## Response

```json theme={null}
{
  "data": {
    "intentId": "a3b4ba1-e34523c-324",
    "status": "filled",
    "failureReason": null,
    "originChainId": "evm:8453",
    "destinationChainId": "solana:solana",
    "sender": "0x...",
    "recipient": "...",
    "amountIn": "0.05",
    "amountInUsd": 167.42,
    "amountOut": "0.68421052",
    "amountOutUsd": 165.10,
    "depositTxHash": "0x...",
    "fillTxHash": "5xL...",
    "fillTxHashPending": false,
    "settleTxHash": null,
    "isRelayFallback": false,
    "relayRequestId": null,
    "latencyMs": 487,
    "timestamps": {
      "depositDetected": "2026-05-23T12:00:01.123Z",
      "fillSent": "2026-05-23T12:00:01.500Z",
      "fillConfirmed": "2026-05-23T12:00:01.610Z",
      "settled": null
    },
    "createdAt": "2026-05-23T12:00:00.000Z"
  }
}
```

`latencyMs` is the deposit-detected → fill-confirmed delta. `settleTxHash` /
`timestamps.settled` populate only once the solver has been reimbursed on
the origin chain — that step is async and can lag the user-visible fill. On a
slippage refund, `failureReason.code` is `"slippage"` and a human-readable
`message` field is added telling the user to raise their slippage.

`fillTxHashPending` is `true` when the fill is **already complete**
(`status: "filled"`) but its canonical destination tx hash hasn't been indexed
yet — the case for **Hyperliquid destinations**, whose L1 hash is assigned a
moment after the funds move. While it's `true`, `fillTxHash` is `null`. Treat the
bridge as done as soon as you see `filled`; fetch the hash separately (see the
[best practice below](#best-practice-don-t-block-completion-on-the-fill-hash-hyperliquid)).

## Status lifecycle

The statuses the solver actually writes:

| Status      | Meaning                                                                                 | Terminal?            |
| ----------- | --------------------------------------------------------------------------------------- | -------------------- |
| `pending`   | Row not yet created, or deposit not yet detected.                                       | No                   |
| `filling`   | Deposit detected; fill in progress on the destination chain.                            | No                   |
| `relaying`  | Fill being delivered via the Relay fallback path (solver lacked destination inventory). | No                   |
| `refunding` | Fill couldn't complete; the origin-chain refund is being sent.                          | No                   |
| `filled`    | Fill confirmed — user has received funds.                                               | **Yes** (happy path) |
| `settled`   | Solver reimbursed on origin chain.                                                      | Yes                  |
| `refunded`  | Refund landed — user got their deposit back on the origin chain.                        | Yes                  |
| `failed`    | Refund also failed — manual intervention.                                               | Yes                  |

On deposit detection the solver writes `filling` **directly** — there is no
intermediate `deposited` status. (`deposited` and `retrying` exist in the
underlying type enum but are not currently emitted; retries are tracked in a
separate queue and the intent stays in `filling`.) A `relaying` intent resolves
to `filled` (or `refunded`) like any other; the terminal row then carries
`isRelayFallback: true` and a `relayRequestId`. Treat any non-terminal status
as "keep waiting," and any unknown status defensively.

The terminal set is `filled`, `settled`, `failed`, `refunded`. Stop polling once
you see one of them.

## `GET /status/{id}/wait`

Long-poll variant. Blocks server-side until the intent reaches `filled`,
`settled`, `failed`, or `refunded`, then returns the same shape as
`/status/{id}`. If the window elapses first, you get the **current**
(non-terminal) row and should call again.

### Query parameter

| Name                | Notes                                                                                                                                                                                                                                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `timeout`           | Milliseconds. Default `30000`, capped at `60000`.                                                                                                                                                                                                                                    |
| `waitForFillTxHash` | `true` to hold the long-poll until the canonical `fillTxHash` is present, not just until the intent is terminal. Use it to fetch the real Hyperliquid hash *after* you've already shown the bridge complete. Every other destination returns immediately (its hash is set up front). |

### How it actually waits

The server holds the request open and resolves it the moment your intent
reaches a terminal state (`filled`, `settled`, `failed`, or `refunded`) — the
result is always current, with no client-side polling interval to tune.

In practice, fills are delivered with the latency of the destination chain —
typically a few hundred ms. The 30 s default is just an upper bound; you almost
always return earlier.

### Recommended client loop

```typescript theme={null}
async function waitUntilDone(intentId: string) {
  while (true) {
    const res = await fetch(
      `${API}/api/2/bridge/status/${intentId}/wait?apiKey=YOUR_API_KEY`,
    );
    const { data } = await res.json();

    if (data.status === "filled" || data.status === "settled") return data;
    if (data.status === "failed" || data.status === "refunded") {
      throw new Error(data.message ?? `Bridge ${data.status}`);
    }
    // pending / filling / relaying — re-fire immediately
  }
}
```

Pass the key as the `?apiKey=` query param (not an `Authorization` header).
Don't add a client-side sleep — the server already blocks until something
happens; firing again with no delay keeps one open long-poll waiting for the
next state change.

### Stale-response handling

If you start a new bridge while a previous `/wait` is still in flight, the
previous response will arrive **after** your new `intentId` is active. Track
the active intent ID client-side and discard any `/wait` result that doesn't
match it — otherwise you'll attach an old fill TX to the new attempt.

### Best practice: don't block completion on the fill hash (Hyperliquid)

A Hyperliquid-destination fill is **final the instant the funds leave HL**, but
HL only assigns the canonical L1 transaction hash a moment later. The solver
therefore marks the intent `filled` immediately and patches `fillTxHash` in
afterward — so a `filled` response can briefly carry `fillTxHash: null` with
`fillTxHashPending: true`.

Show the user "bridge complete" as soon as `status` is `filled` — **do not wait
on the hash**. Then, only if you want an explorer link, make a second long-poll
with `?waitForFillTxHash=true` and update your UI when the hash lands:

```typescript theme={null}
// 1) Already terminal — show "complete" now (the hash may still be pending).
const final = await waitUntilDone(intentId);

// 2) Optional: fetch the canonical fill hash in the background, then patch the UI.
if (final.status === "filled" && final.fillTxHashPending) {
  const res = await fetch(
    `${API}/api/2/bridge/status/${intentId}/wait?waitForFillTxHash=true&apiKey=YOUR_API_KEY`,
  );
  const { data } = await res.json();
  if (data.fillTxHash) updateExplorerLink(data.fillTxHash); // your UI patch
}
```

Every other destination returns a real `fillTxHash` up front (`fillTxHashPending:
false`), so the second call returns immediately and this branch is a no-op.

## Live updates over WebSocket

`bridge-status` is the push equivalent of this endpoint: subscribe to an intent
and the server sends its status now, then again on **every** transition —
including the intermediate ones (`pending` → `filling` → `filled`) that a
one-shot long-poll can't report.

It's a channel on the standard Mobula gateway (`wss://api.mobula.io`), so the
subscribe / unsubscribe contract is the same as every other stream and one
connection can carry bridge tracking alongside your market subscriptions.

### Subscribe

```json theme={null}
{
  "type": "bridge-status",
  "authorization": "YOUR_API_KEY",
  "payload": {
    "intentId": "a3b4ba1-e34523c-324",
    "subscriptionId": "bridge-1"
  }
}
```

| Field                  | Notes                                                                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `intentId`             | The intent to follow. Accepts the same ids as `GET /status/{id}` — `intentId`, on-chain `bytes32` intent ID, deposit TX hash, fill TX hash. |
| `intentIds`            | Array form, up to 20 per subscription. Use it to resume several unfinished bridges at once; `intentId` and `intentIds` can be combined.     |
| `subscriptionId`       | Optional. Your handle for the subscription — echoed on every frame and used to unsubscribe. Auto-generated when omitted.                    |
| `subscriptionTracking` | Optional, default `false`. Sends a `{"event":"subscribed"}` acknowledgement listing the ids.                                                |
| `tag`                  | Optional. Free-form label attached to the subscription's usage records.                                                                     |

### Frames

```json theme={null}
{
  "type": "bridge-status",
  "subscriptionId": "bridge-1",
  "final": false,
  "data": {
    "intentId": "a3b4ba1-e34523c-324",
    "status": "filling",
    "depositTxHash": "0x...",
    "fillTxHash": null,
    "...": "same body as GET /status/{id}"
  }
}
```

`data` is byte-for-byte the `data` of `GET /status/{id}` — same fields, same
`fillTxHashPending` semantics, same `failureReason`.

A frame is sent only when something actually changed, so an intent that sits in
`filling` stays quiet rather than repeating itself.

Frames are pushed, not polled: the solver announces each transition the moment it
commits it and the server forwards it straight to your socket. Expect a frame
within a few milliseconds of the state change on-chain — ahead of what
`/status/{id}/wait` can return, and far ahead of any client-side polling loop.

`final` is `true` on the last frame an intent will ever produce — its status
reached `filled`, `settled`, `refunded` or `failed`. The server then stops
tracking that id: there is nothing left to unsubscribe from, and re-subscribing
to it simply replays the terminal row.

If the id has no row yet (deposit broadcast but not yet indexed) you get no
frame until one exists — the subscription is live and waiting, exactly like the
`pending` reply on the REST endpoint.

### Unsubscribe

```json theme={null}
{ "type": "unsubscribe", "payload": { "type": "bridge-status", "subscriptionId": "bridge-1" } }
```

Omit `subscriptionId` to drop every `bridge-status` subscription on the
connection. Everything is released automatically when the socket closes.

### Example

```typescript theme={null}
const ws = new WebSocket("wss://api.mobula.io");

ws.onopen = () =>
  ws.send(
    JSON.stringify({
      type: "bridge-status",
      authorization: "YOUR_API_KEY",
      payload: { intentId, subscriptionId: "bridge-1" },
    }),
  );

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type !== "bridge-status") return;

  render(msg.data.status); // pending → filling → filled

  if (msg.final) {
    if (msg.data.status === "filled" || msg.data.status === "settled") done(msg.data);
    else failed(msg.data.message ?? msg.data.status);
  }
};
```

### WebSocket or `/wait`?

Both are push — neither makes you poll. Use the WebSocket when you want the
*progress* (a status bar that moves through `filling`), when you're following
several bridges at once, or when you already hold a gateway connection. Use
`/status/{id}/wait` when a single request that returns the finished bridge is
all you need — a script, a server-side flow, a backend with no socket to spare.

## Example

```typescript theme={null}
const API = "https://api.mobula.io";
const KEY = "YOUR_API_KEY"; // pass as ?apiKey=, not an Authorization header

// Instant lookup
const inst = await fetch(`${API}/api/2/bridge/status/${intentId}?apiKey=${KEY}`);
const { data } = await inst.json();
console.log(data.status, data.fillTxHash, data.latencyMs);

// Long-poll until terminal
const wait = await fetch(`${API}/api/2/bridge/status/${intentId}/wait?apiKey=${KEY}`);
const { data: final } = await wait.json();
```

See the [Bridge Implementation guide](/guides/bridge-implementation) for the
full no-sleep `while(true)` polling loop and how to handle stale responses
when running multiple bridges in parallel.


## OpenAPI

````yaml get /2/bridge/status/{id}
openapi: 3.0.0
info:
  version: 1.0.0
  title: Mobula API
  description: >-
    Documentation of the Mobula API


    **Demo API**: The default server (demo-api.mobula.io) is a demo API with
    rate limits.

    For production use, please use api.mobula.io with an API key from
    https://admin.mobula.io
servers:
  - url: https://demo-api.mobula.io/api/
    description: Demo API (rate limited, for testing only)
  - url: https://api.mobula.io/api/
    description: Production API (requires API key)
security: []
tags:
  - name: V2 - Token
    description: Token details, price, security, ATH, and holder data
  - name: V2 - Market Data
    description: Market details, OHLCV history, and lighthouse metrics
  - name: V2 - Trades
    description: Token trades, enriched trades, and trade filters
  - name: V2 - Wallet
    description: Wallet positions, activity, trades, analysis, and labels
  - name: V2 - Assets
    description: Cross-chain asset details and price history
  - name: V2 - Swap
    description: Swap quoting and execution
  - name: V2 - Perps
    description: Perpetual futures quoting, execution, and positions
  - name: V2 - Bridge
    description: Cross-chain bridge quoting and intent status (Alpha Preview)
  - name: V2 - DeFi
    description: Bonding pools and pulse data
  - name: V2 - Search
    description: Universal fast search
  - name: V2 - Usage
    description: Per-key API and WebSocket usage history
  - name: V2 - Blockchains
    description: System metadata and chain listings
  - name: V2 - Prediction Markets
    description: >-
      Polymarket markets/events, wallet positions, and the full execution stack
      (auth, order build/submit/cancel, approvals, pUSD wrap/unwrap, deploy,
      deposit/withdraw, redeem). Alpha — see /api/2/pm/*.
  - name: V1 - Market Data
    description: Market prices, history, sparklines, pairs, and multi-data
  - name: V1 - Wallet
    description: Wallet portfolio, transactions, history, and NFTs
  - name: V1 - Token
    description: First buyers
  - name: V1 - Trades
    description: Market trades by pair
  - name: V1 - Metadata
    description: Token metadata, categories, and news
  - name: V1 - Assets
    description: List all assets
  - name: V1 - Search
    description: Search for assets, tokens, and pairs
  - name: V1 - DeFi
    description: Bonding pool pulse data
  - name: V1 - Blockchains
    description: Blockchain listings, pairs, and stats
  - name: V1 - Webhooks
    description: Webhook management
  - name: V1 - Feed
    description: Custom feed creation
paths:
  /2/bridge/status/{id}:
    get:
      tags:
        - V2 - Bridge
      summary: Get bridge intent status
      description: >-
        [Alpha Preview] Look up a bridge intent's lifecycle status by intent ID
        (bytes32) or by the deposit/fill transaction hash. Status values:
        pending (intent not yet detected), deposited, filling, filled (recipient
        has received funds), settled (solver has withdrawn deposited funds),
        retrying, refunded, failed.
      parameters:
        - schema:
            type: string
            description: Intent ID (0x + 64 hex), deposit TX hash, or fill TX hash.
          required: true
          description: Intent ID (0x + 64 hex), deposit TX hash, or fill TX hash.
          name: id
          in: path
      responses:
        '200':
          description: >-
            Bridge intent status. Returns status="pending" with a `message` when
            the intent has not yet been detected on-chain.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        description: >-
                          Echoes the requested id when the intent has not yet
                          been detected.
                      intentId:
                        type: string
                        description: On-chain intent ID (bytes32).
                      status:
                        type: string
                        enum:
                          - pending
                          - deposited
                          - filling
                          - filled
                          - settled
                          - retrying
                          - refunded
                          - failed
                      originChainId:
                        type: string
                      destinationChainId:
                        type: string
                      sender:
                        type: string
                      recipient:
                        type: string
                      amountIn:
                        type: string
                      amountOut:
                        type: string
                        nullable: true
                      depositTxHash:
                        type: string
                        nullable: true
                      fillTxHash:
                        type: string
                        nullable: true
                      settleTxHash:
                        type: string
                        nullable: true
                      latencyMs:
                        type: number
                        nullable: true
                        description: >-
                          End-to-end fill latency in milliseconds
                          (deposit-detected → fill-confirmed).
                      timestamps:
                        type: object
                        properties:
                          depositDetected:
                            type: string
                            nullable: true
                          fillSent:
                            type: string
                            nullable: true
                          fillConfirmed:
                            type: string
                            nullable: true
                          settled:
                            type: string
                            nullable: true
                      createdAt:
                        type: string
                      message:
                        type: string
                        description: Present when the intent has not yet been detected.
                    required:
                      - status
                required:
                  - data

````