Integrate the Magma API
End-to-end Magma walkthrough - API key, cost estimate, buy mutation, HODL payment, polling, and recovery. Production-grade code you can copy.
A complete walk-through from zero to a confirmed inbound channel. By the end of this guide you'll have an integration that:
- Estimates cost before charging customers
- Places a buy order and pays the HODL invoice
- Tracks the order through the full lifecycle
- Handles failures cleanly (refunds, retries, terminal states)
The API is a single GraphQL endpoint:
https://magma.amboss.tech/graphqlFor shorter quickstarts see Get Started. For focused references see Buy Liquidity, Sell Liquidity, Subscriptions, and the LSP REST API.
What you'll build
Prerequisites
- An Amboss account at account.amboss.tech. Required to mint an API key for an integration that needs to manage subscriptions, sell, or read orders without a session key.
- A Lightning node with at least the inbound or routing scenario you're solving for. The node must have a reachable
pubkey@host:portsocket. - A Lightning wallet (or your own node) capable of paying the HODL invoice the API returns.
- Optional: a session-cookie store if you want anonymous buyers to come back to their own orders (web/mobile flows).
Buying liquidity is fully anonymous. You only need an API key if you want long-lived access tied to an Amboss account - e.g. selling, managing offers, or reading the same orders from multiple devices.
Quickstart
Create an API key (optional)
Skip this if you're an anonymous buyer - every liquidity.buy returns a session_key that does the same job for one identity.
Otherwise: go to account.amboss.tech/settings/api-keys and create one. Treat it like a password.
export AMBOSS_API_KEY="amboss_..."Send it as a Bearer token on every authenticated request:
Authorization: Bearer amboss_...See Authentication for the full credential matrix.
Estimate cost
Always price the channel before you charge a customer. The liquidity_per_usd query is free and reflects current market depth.
query LiquidityPerUsd {
market {
liquidity {
liquidity_per_usd { sats usd }
}
}
}curl -X POST https://magma.amboss.tech/graphql \
-H "Content-Type: application/json" \
-d '{"query":"query { market { liquidity { liquidity_per_usd { sats usd } } } }"}'import { GraphQLClient, gql } from "graphql-request";
const magma = new GraphQLClient("https://magma.amboss.tech/graphql");
const { market } = await magma.request(gql`
query {
market { liquidity { liquidity_per_usd { sats usd } } }
}
`);
const satsPerUsd = Number(market.liquidity.liquidity_per_usd.sats);
const channelSatsFor = (usd) => satsPerUsd * usd;
console.log("$50 ≈", channelSatsFor(50).toLocaleString(), "sats inbound");{
"data": {
"market": {
"liquidity": {
"liquidity_per_usd": { "sats": "543260", "usd": "501.09" }
}
}
}
}sats is inbound capacity per USD. usd is the expected dollar-receiving capacity per USD spent - at the rate above, $1 of fees can carry ~$500 of payments through this channel.
Place a buy order
liquidity.buy returns a Lightning invoice you'll pay to lock in the channel.
mutation BuyLiquidity($input: LiquidityOrderInput!) {
liquidity {
buy(input: $input) {
account { id session_key }
order { transaction_id usd_cents }
payment {
lightning_invoice
redirect_url
amount { sats }
}
}
}
}{
"input": {
"connection_uri": "[email protected]:9735",
"usd_cents": "5000",
"options": { "private": false, "rails_cluster_only": false }
}
}curl -X POST https://magma.amboss.tech/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AMBOSS_API_KEY" \
-d @- <<'JSON'
{
"query": "mutation($input: LiquidityOrderInput!) { liquidity { buy(input: $input) { account { id session_key } order { transaction_id usd_cents } payment { lightning_invoice redirect_url amount { sats } } } } }",
"variables": {
"input": {
"connection_uri": "[email protected]:9735",
"usd_cents": "5000"
}
}
}
JSONconst BUY = gql`
mutation BuyLiquidity($input: LiquidityOrderInput!) {
liquidity {
buy(input: $input) {
account { id session_key }
order { transaction_id usd_cents }
payment { lightning_invoice redirect_url amount { sats } }
}
}
}
`;
const { liquidity } = await magma.request(BUY, {
input: {
connection_uri: process.env.MY_NODE_URI,
usd_cents: "5000",
},
});
const { lightning_invoice, redirect_url } = liquidity.buy.payment;
const sessionKey = liquidity.buy.account.session_key;
const orderId = liquidity.buy.order.transaction_id;
// Persist the session_key NOW - it's the only way back to this order
// without an API key.
await db.sessions.upsert({ id: orderId, sessionKey });{
"data": {
"liquidity": {
"buy": {
"account": {
"id": "54b9e82d-39d0-42b9-9229-f67786cdf145",
"session_key": "b65b867c345468a0a2e07b3b86aa3078"
},
"order": {
"transaction_id": "ec562479-a4b8-44f4-95b4-150b310832de",
"usd_cents": "5000"
},
"payment": {
"lightning_invoice": "lnbc125u1p...",
"redirect_url": "https://btcpay.amboss.tech/i/...",
"amount": { "sats": "125000" }
}
}
}
}
}LiquidityOrderInput fields
| Field | Required | Type | Notes |
|---|---|---|---|
connection_uri | ✅ | String | pubkey@host:port for the destination node. Tor sockets work. |
usd_cents | ✅ | String | Integer string in USD cents. Minimum 500 ($5). |
redirect_url | ❌ | String | URL to send the customer to after BTCPay checkout. Defaults to a Magma status page. |
options.private | ❌ | Boolean | Open an unannounced channel. |
options.rails_cluster_only | ❌ | Boolean | Restrict matching to Amboss Rails cluster sellers (higher reliability tier). |
options.asset_id | ❌ | String | Non-BTC asset id. Omit for BTC. |
Don't retry liquidity.buy blindly. The mutation is not idempotent - every successful call creates a new order. If a previous call timed out, list user.transactions.transaction_list first and reconcile.
Pay the invoice
Present payment.lightning_invoice to whoever's paying. Three common patterns:
- End-user wallet. Render the invoice as a QR code or a
lightning:URI. The customer scans it from their wallet. - Hosted checkout. Send the customer to
payment.redirect_url. Same invoice, hosted UI with QR + copy button. - Programmatic payment from your own node. Pay
payment.lightning_invoicedirectly withlncli payinvoiceor your node's RPC.
The instant the HTLC lands, the order's payment_status goes to SUCCESSFUL_PAYMENT and its status moves to WAITING_FOR_CHANNEL_OPEN.
Track the order
Use the session_key you saved (or your API key) as a Bearer token and poll user.market.orders.get_order.
const GET_ORDER = gql`
query GetOrder($orderId: String!) {
user {
market {
orders {
get_order(order_id: $orderId) {
id
status
payment_status
channel_id
transaction_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",
]);
async function trackOrder(orderId, sessionKey) {
const client = new GraphQLClient("https://magma.amboss.tech/graphql", {
headers: { Authorization: `Bearer ${sessionKey}` },
});
while (true) {
const { user } = await client.request(GET_ORDER, { orderId });
const order = user.market.orders.get_order;
if (SUCCESS.has(order.status)) return { ok: true, order };
if (FAILURE.has(order.status)) return { ok: false, order };
// Slow polling once the funding tx is on chain
const onChain = ["SELLER_SENT_TRANSACTION", "WAITING_FOR_ON_CHAIN_CONFIRMATION"]
.includes(order.status);
await new Promise((r) => setTimeout(r, onChain ? 45_000 : 10_000));
}
}
const result = await trackOrder(orderId, sessionKey);
console.log(result.ok ? "channel opened" : `failed: ${result.order.status}`);See Tracking Orders for the full reference and Order Lifecycle for every status.
Handle failures
When trackOrder returns { ok: false, order }, you have a terminal failure. Each terminal status maps to a specific recovery:
| Status | What to tell the user | What to do |
|---|---|---|
SELLER_REJECTED / SELLER_FAILED_TO_REACT | "Seller declined; trying again." | Re-issue liquidity.buy (the original payment is automatically refunded by the HODL invoice). |
BUYER_FAILED_TO_PAY | "Payment didn't go through in time." | Generate a fresh order. The previous invoice has expired. |
SELLER_FAILED_TO_OPEN_CHANNEL / SELLER_FAILED_TO_SEND_SWAP | "Channel couldn't open. Refund issued." | Refund is automatic via HODL. Optionally retry with rails_cluster_only: true for higher reliability. |
INVALID_CHANNEL_OPENING | "Seller breached agreed terms." | Refund is automatic. Open a support ticket. |
ADMIN_CLOSED | "Order cancelled by Amboss." | Contact support. |
HODL refund. When a seller fails to open the channel, the HODL invoice never settles - the buyer's payment is released back to their wallet automatically. You do not need to call a refund endpoint.
See Errors for the broader error catalog including auth and validation failures.
Production checklist
Once the happy path works in dev, these are the things to wire up before you ship.
Idempotency
- Estimate queries (
liquidity_per_usd,offer_book) are pure - retry freely. liquidity.buyis not idempotent. Always reconcile by listing transactions before retrying after a timeout:
query Reconcile {
user {
transactions {
transaction_list(page: { limit: 10, offset: 0 }) {
list { id status created_at }
}
}
}
}- Polling is safe to repeat unboundedly - the response is deterministic.
Rate limits
Don't poll an individual order faster than once every 5 seconds. Spread bulk reads across transaction_list rather than fanning out to many get_order calls.
Storage
Persist three things for every order you create:
| Field | Why |
|---|---|
order.transaction_id | Identifies the order across all subsequent calls. |
account.session_key (if anonymous) | The only way to authenticate as this buyer later. |
payment.lightning_invoice | Lets you show the customer the invoice again if they close the tab before paying. |
Observability
Log on every status transition you observe. Aggregating those into a funnel chart (WAITING_FOR_SELLER_APPROVAL → VALID_CHANNEL_OPENING) makes it obvious when sellers are dropping orders.
Going from anonymous to authenticated
If you start with session_key (anonymous buyer flow) and later mint an API key, the two identities don't merge automatically. Plan for this:
- Stay anonymous for one-off purchases. Simplest.
- Use API keys from the start if you'll have a single backend tracking many orders.
- Don't switch mid-flow - the session-key orders remain accessible only via the session key.
Authentication
Two credentials, same Authorization: Bearer … header:
| Mode | Where it comes from | Use for |
|---|---|---|
| API key | account.amboss.tech/settings/api-keys | Long-lived integrations, selling, reading orders across devices |
| Session key | Returned once by liquidity.buy / liquidity.create_subscription | Anonymous buyer flows |
See Authentication for the full credential matrix.
Lifecycle: a buyer's view
See Order Lifecycle for the full state machine, including in-flight states this diagram collapses.
Error handling cheatsheet
Unauthorized token- bad or missing Bearer header. Mint a new key.Invalid ID- malformed UUID. Validate before sending.- Payment errors are surfaced via
payment_status(HODL_INVOICE_TIMEOUT,INVALID_PAYMENT_SECRET, etc.), not GraphQL errors. Always read bothstatusandpayment_statuswhen reasoning about an order. - Don't trust HTTP 200. GraphQL returns 200 even when
errorsis populated.
Full reference: Errors.
Next steps
Sell Liquidity
Fulfil Magma orders as a seller - accept, reject, and confirm channel openings. Authenticated mutations with full code examples.
LSP REST API (BLIP-0051)
Magma's BLIP-0051-compatible REST endpoints for Lightning Service Provider integration. Buy inbound capacity from standard LSP clients.