Tracking Orders
How to poll the Magma API for order status updates. Recommended cadence, terminal-state detection, and a complete reference polling loop.
Magma doesn't push order-status webhooks today. To follow an order from creation to confirmed channel, your integration polls the get_order query and watches for terminal states.
This page is the recipe.
For payments (not order) webhooks (e.g. payment.completed on a Lightning invoice), see the Amboss Payments docs. The Payments product and Magma marketplace are separate APIs.
The query
Authenticate as the order's buyer (API key or session key - see Authentication) and call user.market.orders.get_order:
query GetOrder($orderId: String!) {
user {
market {
orders {
get_order(order_id: $orderId) {
id
status
payment_status
channel_id
transaction_id
timeout
blocks_until_can_be_closed
confirmations { confirmations }
fees {
buyer { sats usd }
}
}
}
}
}
}{ "orderId": "ec562479-a4b8-44f4-95b4-150b310832de" }Example response
{
"data": {
"user": {
"market": {
"orders": {
"get_order": {
"id": "ec562479-a4b8-44f4-95b4-150b310832de",
"status": "WAITING_FOR_ON_CHAIN_CONFIRMATION",
"payment_status": "SUCCESSFUL_PAYMENT",
"channel_id": null,
"transaction_id": "5e8a3f...c4f1",
"timeout": "2026-06-10T18:42:00Z",
"blocks_until_can_be_closed": 4320,
"confirmations": { "confirmations": 2 },
"fees": { "buyer": { "sats": "12500", "usd": "5.62" } }
}
}
}
}
}
}Terminal-state detection
A reliable polling loop watches for the terminal statuses (which never change after they're reached). Hard-coding the in-flight list is brittle - new in-flight states can be added; the terminal set is closed.
export const SUCCESS_STATUSES = new Set([
"VALID_CHANNEL_OPENING",
"CHANNEL_MONITORING_FINISHED",
]);
export const FAILURE_STATUSES = new Set([
"SELLER_REJECTED",
"SELLER_FAILED_TO_REACT",
"BUYER_REJECTED",
"BUYER_FAILED_TO_PAY",
"SELLER_FAILED_TO_OPEN_CHANNEL",
"SELLER_FAILED_TO_SEND_SWAP",
"INVALID_CHANNEL_OPENING",
"ADMIN_CLOSED",
]);
export const TERMINAL_STATUSES = new Set([
...SUCCESS_STATUSES,
...FAILURE_STATUSES,
]);See Order Lifecycle for the full status reference.
Recommended polling cadence
Each phase has a different expected duration. Adapt the interval so you're not hammering the API while a 6-block confirmation runs.
| Phase | Typical wait | Poll interval |
|---|---|---|
WAITING_FOR_SELLER_APPROVAL | Seconds to minutes | 5–10 s |
WAITING_FOR_BUYER_PAYMENT | Until paid (or HODL expiry) | 5–10 s |
WAITING_FOR_CHANNEL_OPEN | Seconds | 5–10 s |
SELLER_SENT_TRANSACTION | Mempool propagation | 15–30 s |
WAITING_FOR_ON_CHAIN_CONFIRMATION | ~10 min per block | 30–60 s |
ON_CHAIN_CONFIRMATION → SELLER_OPENED_CHANNEL | Seconds | 15–30 s |
Don't poll faster than 1 request per 5 seconds for an individual order. Aggressive polling will get throttled.
Reference polling loop
A complete Node.js example that adapts the interval to the order's current phase:
import { GraphQLClient, gql } from "graphql-request";
const magma = new GraphQLClient("https://magma.amboss.tech/graphql", {
headers: { Authorization: `Bearer ${process.env.AMBOSS_API_KEY}` },
});
const GET_ORDER = gql`
query GetOrder($orderId: String!) {
user {
market {
orders {
get_order(order_id: $orderId) {
id
status
payment_status
channel_id
confirmations { confirmations }
}
}
}
}
}
`;
const SUCCESS = new Set(["VALID_CHANNEL_OPENING", "CHANNEL_MONITORING_FINISHED"]);
const FAILURE = new Set([
"SELLER_REJECTED", "SELLER_FAILED_TO_REACT",
"BUYER_REJECTED", "BUYER_FAILED_TO_PAY",
"SELLER_FAILED_TO_OPEN_CHANNEL", "SELLER_FAILED_TO_SEND_SWAP",
"INVALID_CHANNEL_OPENING", "ADMIN_CLOSED",
]);
const FAST_PHASES = new Set([
"WAITING_FOR_SELLER_APPROVAL",
"WAITING_FOR_BUYER_PAYMENT",
"WAITING_FOR_CHANNEL_OPEN",
]);
const SLOW_PHASES = new Set([
"SELLER_SENT_TRANSACTION",
"WAITING_FOR_ON_CHAIN_CONFIRMATION",
"ON_CHAIN_CONFIRMATION",
"SELLER_OPENED_CHANNEL",
]);
async function trackOrder(orderId: string, maxWaitMs = 2 * 60 * 60 * 1000) {
const start = Date.now();
while (Date.now() - start < maxWaitMs) {
const { user } = await magma.request(GET_ORDER, { orderId });
const order = user.market.orders.get_order;
console.log(
`[${new Date().toISOString()}] ${order.status}` +
(order.confirmations
? ` · ${order.confirmations.confirmations} confs`
: ""),
);
if (SUCCESS.has(order.status)) return { ok: true, order };
if (FAILURE.has(order.status)) return { ok: false, order };
const intervalMs = FAST_PHASES.has(order.status)
? 7_500
: SLOW_PHASES.has(order.status)
? 45_000
: 10_000;
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`Order ${orderId} did not reach terminal state in time`);
}
const result = await trackOrder("ec562479-a4b8-44f4-95b4-150b310832de");
console.log(result.ok ? "✅ channel opened" : `❌ ${result.order.status}`);What to watch beyond status
A few other fields give you progress signals without changing the top-level status:
| Field | Useful for |
|---|---|
payment_status | Detecting payment-leg failures (e.g. HODL_INVOICE_TIMEOUT) before the status reflects them. |
transaction_id | Becomes non-null once the seller broadcasts the funding TX. |
confirmations.confirmations | Block confirmation count for the funding TX. |
channel_id | Short channel ID. Becomes non-null once the channel is announced. |
timeout | ISO timestamp of when the current phase will expire. Useful for surfacing a countdown to your users. |
Listing recent orders
If you don't have a specific order_id and just want everything recent:
query ListPurchases {
user {
market {
orders {
purchases(page: { limit: 20, offset: 0 }) {
list {
id
status
created_at
amount { satoshi { sats usd } }
}
total
}
}
}
}
}Swap purchases for sales if you're the seller side.
Next steps
- Order Lifecycle - every status explained
- Errors - what to do when an order hits a failure state
- Integration guide - the end-to-end walkthrough with polling wired in