Get bridge intent status
curl --request GET \
--url https://demo-api.mobula.io/api/2/bridge/status/{id}import requests
url = "https://demo-api.mobula.io/api/2/bridge/status/{id}"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://demo-api.mobula.io/api/2/bridge/status/{id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://demo-api.mobula.io/api/2/bridge/status/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://demo-api.mobula.io/api/2/bridge/status/{id}"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://demo-api.mobula.io/api/2/bridge/status/{id}")
.asString();require 'uri'
require 'net/http'
url = URI("https://demo-api.mobula.io/api/2/bridge/status/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_body{
"data": {
"status": "pending",
"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>"
}
}Bridge Status
[Alpha Preview] Look up the status of a bridge intent, or long-poll until it terminates.
GET
/
2
/
bridge
/
status
/
{id}
Get bridge intent status
curl --request GET \
--url https://demo-api.mobula.io/api/2/bridge/status/{id}import requests
url = "https://demo-api.mobula.io/api/2/bridge/status/{id}"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://demo-api.mobula.io/api/2/bridge/status/{id}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://demo-api.mobula.io/api/2/bridge/status/{id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://demo-api.mobula.io/api/2/bridge/status/{id}"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://demo-api.mobula.io/api/2/bridge/status/{id}")
.asString();require 'uri'
require 'net/http'
url = URI("https://demo-api.mobula.io/api/2/bridge/status/{id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_body{
"data": {
"status": "pending",
"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.
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:
intentIdfrom/quote(formatxxxxxxx-xxxxxxx-xxx).- The on-chain
bytes32intent ID (EVM only, emitted byMobulaBridge). - The deposit TX hash.
- The fill TX hash.
{ "data": { "id": "...", "status": "pending", "message": "Intent not found — deposit may still be processing" } }
Response
{
"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).
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 |
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
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
}
}
?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 intentfilled 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:
// 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
}
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
{
"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
{
"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
{ "type": "unsubscribe", "payload": { "type": "bridge-status", "subscriptionId": "bridge-1" } }
subscriptionId to drop every bridge-status subscription on the
connection. Everything is released automatically when the socket closes.
Example
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
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();
while(true) polling loop and how to handle stale responses
when running multiple bridges in parallel.