Skip to main content
GET
/
2
/
bridge
/
status
/
{id}
Get bridge intent status
curl --request GET \
  --url https://demo-api.mobula.io/api/2/bridge/status/{id}
{
  "data": {
    "id": "<string>",
    "intentId": "<string>",
    "originChainId": "<string>",
    "destinationChainId": "<string>",
    "sender": "<string>",
    "recipient": "<string>",
    "amountIn": "<string>",
    "amountOut": "<string>",
    "depositTxHash": "<string>",
    "fillTxHash": "<string>",
    "settleTxHash": "<string>",
    "latencyMs": 123,
    "timestamps": {
      "depositDetected": "<string>",
      "fillSent": "<string>",
      "fillConfirmed": "<string>",
      "settled": "<string>"
    },
    "createdAt": "<string>",
    "message": "<string>"
  }
}
Alpha Preview — Endpoints and response shape may change without notice.
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
{ "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 chain listener has indexed it. Keep polling.

Response

{
  "data": {
    "intentId": "a3b4bav-e34523c-324",
    "status": "filled",
    "failureReason": null,
    "originChainId": "evm:8453",
    "destinationChainId": "solana:solana",
    "originToken": "0x0000000000000000000000000000000000000000",
    "destinationToken": "So11111111111111111111111111111111111111112",
    "sender": "0x...",
    "recipient": "...",
    "amountIn": "0.05",
    "amountInUsd": 167.42,
    "amountOut": "0.68421052",
    "amountOutUsd": 165.10,
    "depositTxHash": "0x...",
    "fillTxHash": "5xL...",
    "fillTxHashPending": false,
    "settleTxHash": 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).

Status lifecycle

The statuses the solver actually writes:
StatusMeaningTerminal?
pendingRow not yet created, or deposit not yet detected.No
fillingDeposit detected; fill in progress on the destination chain.No
filledFill confirmed — user has received funds.Yes (happy path)
settledSolver reimbursed on origin chain.Yes
refundedFill couldn’t complete; user refunded on the origin chain.Yes
failedRefund 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.) 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

NameNotes
timeoutMilliseconds. Default 30000, capped at 60000.
waitForFillTxHashtrue 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 controller hands the intent to a centralized batched poller (IntentWaiterService) that, while any waiter is registered, runs one pass every 10 ms across all active waiters: a Redis MGET of the solver’s mirrored intent-state keys (no replication lag — the fast path), with a Postgres primary read for anything Redis didn’t resolve. The first pass that sees your row terminal (status IN ('filled','settled','failed','refunded')) resolves the request. There is no Postgres LISTEN/NOTIFY and no per-request fallback timer — that earlier implementation was replaced because it exhausted PG backend connections. In practice, fills are delivered with the latency of the destination-chain listener — typically a few hundred ms. The 30 s default is just an upper bound; you almost always return earlier.
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 — 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 filleddo 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:
// 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.

Example

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 for the full no-sleep while(true) polling loop and how to handle stale responses when running multiple bridges in parallel.

Path Parameters

id
string
required

Intent ID (0x + 64 hex), deposit TX hash, or fill TX hash.

Response

200 - application/json

Bridge intent status. Returns status="pending" with a message when the intent has not yet been detected on-chain.

data
object
required