This is the full file; read all of it. # Lightning Swap API reference ```sh # READ THIS FIRST: this file is 30 KB and a summary drops required details. Download it whole: curl -fsSL https://lightning-swap.com/api_docs/full.md # Skip the download only if you already have all of it: pasted, read from disk or fetched raw. ``` Base URL: `https://lightning-swap.com` · Authenticated methods under `/api/v2` > FixedFloat-compatible — subset of [ff.io/api](https://ff.io/api) > > Lightning Swap implements the same paths, headers, HMAC signing, response envelope, currency codes, and order object shape as the FixedFloat v2 API so existing FixedFloat clients (and tools built against it) can point at us with minimal changes. We are intentionally a **subset**: only fixed-rate pay-in → Lightning BTC (`BTCLN`), and only the methods needed to quote, create, poll, and refund. Surfaces we do not implement are listed under Not supported. ## Switching from FixedFloat A client already built against the FixedFloat v2 API needs three changes: 1. Point the base URL at `https://lightning-swap.com`. Paths stay `/api/v2/…`. 2. Use a Lightning Swap [API key and secret](https://lightning-swap.com/?page=api_tokens). Signing is unchanged: HMAC-SHA256 over the raw body. 3. Create orders with `type=fixed`, `direction=to` and `toCcy=BTCLN`, with the BOLT11 invoice in `toAddress`. Pay-in currency codes, order statuses and the response envelope are FixedFloat's. On `EMERGENCY`, answer with `choice=REFUND`. What we leave out is listed under Not supported. ## Getting started 1. Create an account and open the [API tokens](https://lightning-swap.com/?page=api_tokens) page. 2. Generate a key. Copy the **Lightning Swap Connect URI** (or the **API key** and **API secret** it carries) from the API tokens page. 3. Sign every authenticated POST with HMAC-SHA256 over the exact JSON body string. 4. Create orders with `type=fixed`, `direction=to`, `toCcy=BTCLN`, and a BOLT11 in `toAddress`. 5. The payer sends exactly `from.amount` to `from.address` within `time.left` (10 minutes). 6. Poll `/order` until `DONE`, `EXPIRED`, or `EMERGENCY`. On emergency, call `/emergency` with `choice=REFUND`. ## Lightning Swap Connect (LSC) URI Lightning Swap Connect is OpenReceive's compact, server-only format for configuring an authenticated swap API endpoint. One URI replaces a provider's HTTPS base URL, API key, and API secret. The [API tokens](https://lightning-swap.com/?page=api_tokens) page shows a ready-made LSC URI for every key you create — copy it and hand it to your integration as a single secret. ```uri lightning+swapconnect://lightning-swap.com?key=&secret= ``` That URI resolves to: | Value | Result | | --- | --- | | HTTPS API base URL | `https://lightning-swap.com/` | | API key | Sent as `X-API-KEY` | | API secret | HMAC key for `X-API-SIGN` | The base URL is the root; authenticated methods still live under `/api/v2` — see Authentication. LSC carries no extra privileges: it is the same key and secret in one string. LSC is not an NWC replacement. NWC connects OpenReceive to the receive-only Lightning wallet. LSC connects OpenReceive to an optional service — such as Lightning Swap — that accepts another asset and swaps it into a Lightning invoice. ### URI syntax ```uri lightning+swapconnect://host[/path]?key=KEY&secret=SECRET ``` | Component | Required | Meaning | | --- | --- | --- | | Scheme | Yes | Exactly `lightning+swapconnect` | | Host | Yes | Swap provider HTTPS host | | Port | No | Explicit HTTPS port | | Path | No | Swap provider API base path; `/` is the default | | `key` | Yes | Provider API key | | `secret` | Yes | Provider API secret | - The URI must not contain user information or a fragment. - Each required query parameter must appear exactly once. - Unknown parameters are rejected, so a misspelling cannot silently change configuration. - Query names and values use standard URI percent-encoding. Producers should use a URL implementation rather than concatenating strings; the OpenReceive Node package exports `formatLscUri()` for this. LSC v0.1 defines one swap-provider API contract. Every configured provider is assumed to implement that contract, so the URI has no selector or negotiation field. ### Endpoint mapping The custom scheme always maps to HTTPS: ```uri lightning+swapconnect://HOST[:PORT]/PATH │ └── https://HOST[:PORT]/PATH/ ``` Plain HTTP cannot be expressed. The parsed base path always has a trailing slash. A provider identifier is derived from the lower-case hostname, optional port, and path by replacing unsupported characters with `-`. Two configured URIs may not derive the same identifier. ### Environment variables Swap providers are configured with `LSC_URI_PRIMARY` and optional `LSC_URI_BACKUP`. While the primary answers, OpenReceive uses only the primary; the backup is consulted only when the primary is down. ### Security requirements > An LSC URI is a bearer credential > > Anyone who obtains it can exercise whatever permissions and budget the provider assigned to that key. - Keep LSC URIs on the server and out of browser bundles, logs, exception messages, screenshots, analytics, shell history, and committed files. - Store the complete URI as one secret. Do not split it into public and secret fragments. - Give each application a separate provider key with the smallest available permissions and budget. - Rotate the key and secret if the URI is exposed — revoke it on the API tokens page and create a new one. - Redact the complete value. Redacting only `secret` still exposes the API key and provider identity. - Do not place an LSC URI in a link, QR code, or browser address bar. URI credentials can leak through history, telemetry, referrers, clipboard managers, and process inspection. NWC intentionally defines a connection URI for a client and wallet service with cryptographic keys. LSC packages conventional HTTPS API credentials; it does not add end-to-end encryption beyond TLS and does not define a provider authorization handshake. ## Authentication Authenticated requests send JSON (UTF-8) with these headers: | Header | Value | | --- | --- | | `Content-Type` | `application/json; charset=UTF-8` | | `X-API-KEY` | Your API key (UUID public key) | | `X-API-SIGN` | `HMAC-SHA256(secret, raw_body)` as lowercase hex | Sign the *exact* bytes of the request body. For empty-body calls such as `/ccies`, POST the literal string `{}` and sign that — do not sign an empty string. ### cURL — sign and call ```bash BODY='{"type":"fixed","fromCcy":"SOL","toCcy":"BTCLN","direction":"to","amount":"0.00004243"}' SIGN=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}') curl -sS https://lightning-swap.com/api/v2/price \ -H "Content-Type: application/json; charset=UTF-8" \ -H "X-API-KEY: $API_KEY" \ -H "X-API-SIGN: $SIGN" \ -d "$BODY" ``` ### Python — sign and call ```python import hashlib, hmac, json, urllib.request def ls_post(path, payload, api_key, api_secret): body = json.dumps(payload, separators=(",", ":")) sign = hmac.new(api_secret.encode(), body.encode(), hashlib.sha256).hexdigest() req = urllib.request.Request( f"https://lightning-swap.com{path}", data=body.encode(), headers={ "Content-Type": "application/json; charset=UTF-8", "X-API-KEY": api_key, "X-API-SIGN": sign, }, method="POST", ) with urllib.request.urlopen(req) as resp: return json.load(resp) # Empty-body catalog call — body must be "{}" print(ls_post("/api/v2/ccies", {}, API_KEY, API_SECRET)) ``` Canonical JSON matters for the signature: whatever string you hash must be identical to the bytes on the wire. Prefer a compact encoder (no insignificant whitespace) and reuse that string for both signing and the POST body. ## Request limits Weight budget: **900** units per UTC minute per API token. | Endpoint | Weight | | --- | --- | | `POST /api/v2/create` | 50 | | All other authenticated API calls | 1 | | `GET /rates/fixed.xml` | 0 (public, no auth) | - Exceeding the minute budget returns `code: 429` — *Rate limit exceeded*. - When used weight in the current minute reaches **150**, further `/create` calls are blocked (`Create rate limit gate reached`). ## Response envelope Every JSON method returns: ```json { "code": 0, "msg": "OK", "data": { } } ``` | Field | Type | Description | | --- | --- | --- | | `code` | number | `0` = success. Non-zero mirrors HTTP intent (401, 404, 409, 429, 503, or 400). | | `msg` | string | Human-readable status or error. | | `data` | object / array / boolean / null | Payload. On auth failures often `null`. On `/emergency` success always a boolean. | For `/price`, FixedFloat-style validation often still returns `code: 0` with `data.errors[]` populated (e.g. `LIMIT_MIN`, `OFFLINE_FROM`). Treat a non-empty `errors` array as “do not create”. ## Typical integration flow 1. `GET /rates/fixed.xml` — cache indicative rates and min/max (optional; no auth). 2. `POST /api/v2/ccies` — confirm pay-in codes and `recv`/`send`. 3. `POST /api/v2/price` — quote `direction=to` for a Lightning amount (or `from` for a deposit amount). 4. `POST /api/v2/create` — binding order; store `data.id` + `data.token`; show `from.address` / `from.amount` to the payer. 5. `POST /api/v2/order` — poll until terminal. 6. If `status=EMERGENCY`, `POST /api/v2/emergency` with `choice=REFUND` and a refund address on the deposit network; poll until `DONE` with `back.tx.id`. `/create` is authoritative. XML rates and `/price` are for UX and limits — the deposit amount on the create response is what the payer must send. ## GET /rates/fixed.xml Public FixedFloat-shaped XML of all online pairs. No API key, no signature, no weight cost. We do **not** expose `/rates/float.xml` (float rate type is unsupported). **Parameters:** none. ### Fields per `` | Field | Type | Description | | --- | --- | --- | | `` | string | Pay-in currency code | | `` | string | Always `BTCLN` for Lightning Swap | | `` / `` | number | Reference amounts defining the rate | | `` | number | Reserve hint for `to` (not a hard max) | | `` | string | Lightning network fee folded into quotes, not included in `out` | | `` / `` | string | Min/max pay-in for create | ### Example (live shape — SOL → BTCLN) ```xml SOL BTCLN 1 0.00117230 0.00320351 0.00000700 BTCLN 0.02500000 SOL 1.35281076 SOL … ``` ## POST /api/v2/ccies Currency catalog with FixedFloat field names. Pay-in assets have `recv=1`; Lightning payout is `BTCLN` with `send=1`. **Body:** `{}` (sign that object). Weight: 1. ### Response fields | Field | Type | Description | | --- | --- | --- | | `data[].code` | string | Public code used as `fromCcy` / `toCcy` | | `data[].coin` | string | Asset ticker (BTC, SOL, USDT, …) | | `data[].network` | string | Chain / Lightning network (`SOL`, `ETH`, `TRX`, `LN`) | | `data[].name` | string | Display name | | `data[].recv` | 0\|1 | Accepts deposits from client | | `data[].send` | 0\|1 | Can send to client | | `data[].tag` | string\|null | Memo / destination-tag field name when required | | `data[].contract` | string\|null | Token contract when not native | | `data[].logo` / `color` / `priority` | — | Display metadata | ### Example response (trimmed) ```json { "code": 0, "msg": "OK", "data": [ { "code": "SOL", "coin": "SOL", "network": "SOL", "name": "Solana", "recv": 1, "send": 0, "tag": null, "contract": null, "priority": 0, "logo": "https://lightning-swap.com/assets/images/coins/svg/sol.svg", "color": "#a364fc" }, { "code": "USDTTRC", "coin": "USDT", "network": "TRX", "name": "Tether (TRC20)", "recv": 1, "send": 0, "tag": null, "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "priority": 4, "logo": "https://lightning-swap.com/assets/images/coins/svg/usdttrc.svg", "color": "#53ae94" }, { "code": "BTCLN", "coin": "BTC", "network": "LN", "name": "Bitcoin (Lightning)", "recv": 0, "send": 1, "tag": null, "contract": null, "priority": 0, "logo": "https://lightning-swap.com/assets/images/coins/svg/btcln.svg", "color": "#9157ff" } ] } ``` ## POST /api/v2/price Quote a pair. Use before create to show the user what they will pay / receive. Only `type=fixed` is supported. Both `direction=from` and `direction=to` work. Weight: 1. ### Parameters | Name | | Type | Description | | --- | --- | --- | --- | | `type` | required | string | Must be `fixed` | | `fromCcy` | required | string | Pay-in code (`SOL`, `USDTTRC`, …) | | `toCcy` | required | string | Must be `BTCLN` | | `direction` | required | string | `from` = amount is pay-in; `to` = amount is Lightning BTC received | | `amount` | required* | numeric | Amount in the currency implied by `direction`. Optional if `toAddress` (BOLT11) is provided — invoice sats win. | | `toAddress` | optional | string | BOLT11; when present, quote is derived from invoice amount | Affiliate params (`refcode`, `afftax`) and FixedFloat extras (`ccies`, `usd`) are ignored / not implemented. ### Example — direction `to` ```json // Request { "type": "fixed", "fromCcy": "SOL", "toCcy": "BTCLN", "direction": "to", "amount": "0.00004243" } // Response { "code": 0, "msg": "OK", "data": { "from": { "code": "SOL", "network": "SOL", "coin": "SOL", "amount": 0.03815503, "rate": 0.00116446, "precision": 8, "min": 0.025, "max": 0.91611682, "usd": 2.9, "btc": 0.00004488 }, "to": { "code": "BTCLN", "network": "LN", "coin": "BTC", "amount": 0.00004243, "rate": 858.76715387, "precision": 8, "min": 0.00002711, "max": 0.00106478, "usd": 2.74 }, "errors": [] } } ``` ### `data.errors` values | Code | Meaning | | --- | --- | | `OFFLINE_FROM` / `OFFLINE_TO` | Pair or side unavailable | | `MAINTENANCE_FROM` / `MAINTENANCE_TO` | Side in maintenance | | `LIMIT_MIN` / `LIMIT_MAX` | Amount outside min/max | | `INVALID_FROM` / `INVALID_TO` | Unknown or unsupported currency | | `UNSUPPORTED_TYPE` | Only `fixed` is allowed | | `UNSUPPORTED_DIRECTION` | Not `from` or `to` | | `AMOUNT_MISMATCH` | Provided `amount` ≠ BOLT11 amount (`direction=to`) | | `MISSING_AMOUNT` | No amount and no invoice | ## POST /api/v2/create Binding order create. Cost: **50** weight. Payout is always a Lightning invoice you supply — there is no on-chain BTC payout path. > **Create constraints (subset of FixedFloat):** > > - `type` must be `fixed` (float orders are rejected). > - `direction` must be `to` — amount is the BTC the invoice receives. > - `toCcy` must be `BTCLN`. > - `toAddress` must be a valid mainnet BOLT11 (`lnbc…`) with an amount. If `amount` is also sent, it must match the invoice’s BTC value. > - The invoice must have at least **15 minutes** left before it expires. ### Parameters | Name | | Type | Description | | --- | --- | --- | --- | | `type` | required | string | `fixed` | | `fromCcy` | required | string | Pay-in currency code | | `toCcy` | required | string | `BTCLN` | | `direction` | required | string | `to` | | `amount` | required* | numeric | BTC the invoice should receive; must match BOLT11 when both are sent | | `toAddress` | required | string | BOLT11 destination | ### Idempotency (Lightning Swap extension) Optionally send header `Idempotency-Key: ` (or body field `idempotency_key`). A repeated create with the same key on the same account returns the original order instead of opening a second deposit. ### Create errors Unlike `/price`, a refused create answers a non-zero `code` with the reasons in `data.errors`, e.g. `{ "code": 400, "msg": "INVOICE_EXPIRES_TOO_SOON", "data": { "errors": ["INVOICE_EXPIRES_TOO_SOON"] } }`. No order exists after an error, so a retry is safe. | `data.errors[]` | `code` | Meaning | | --- | --- | --- | | `MISSING_INVOICE` / `INVALID_INVOICE` | 400 | No `toAddress`, or not a mainnet BOLT11 | | `AMOUNTLESS_INVOICE` | 400 | Invoice has no amount | | `INVOICE_EXPIRED` / `INVOICE_EXPIRES_TOO_SOON` | 400 | Invoice expired, or expires within 15 minutes | | `AMOUNT_MISMATCH` | 400 | `amount` ≠ invoice amount | | `INVALID_FROM` / `INVALID_TO` | 400 | Unknown pay-in code, or `toCcy` not `BTCLN` | | `UNSUPPORTED_TYPE` / `UNSUPPORTED_DIRECTION` | 400 | Not `type=fixed` / `direction=to` | | `LIMIT_MIN` / `LIMIT_MAX` | 400 | Pay-in amount outside the pair's min/max | | `OFFLINE_FROM` | 503 | Pair offline | | `DEPOSIT_ADDRESS_FAILED` / `DEPOSIT_WATCH_FAILED` | 503 | Temporary — retry | ### Example — create (SOL → Lightning) ```json // Request { "type": "fixed", "fromCcy": "SOL", "toCcy": "BTCLN", "direction": "to", "amount": "0.00004243", "toAddress": "lnbc42430n1p49elex…cplvlg2n" } // Response (trimmed) { "code": 0, "msg": "OK", "data": { "id": "QBSPEO", "type": "fixed", "email": "", "status": "NEW", "time": { "reg": 1784479529, "start": null, "finish": null, "update": 1784479529, "expiration": 1784480129, "left": 600 }, "from": { "code": "SOL", "coin": "SOL", "network": "SOL", "name": "Solana", "alias": "solana", "amount": "0.03815503", "address": "DTJnkqp4Uxx4ccpzLqSnaRtoescQRgpm1s5Pra3LesXF", "tag": "", "tagName": null, "tx": { "id": null, "amount": null, "fee": null, "ccyfee": null, "timeReg": null, "timeBlock": null, "confirmations": null }, "addressAlt": null, "reqConfirmations": 1, "maxConfirmations": 16 }, "to": { "code": "BTCLN", "coin": "BTC", "network": "LN", "name": "Bitcoin (Lightning)", "alias": "lightning", "amount": "0.00004243", "address": "lnbc42430n1p49elex…cplvlg2n", "tag": "", "tagName": null, "tx": { "id": null, "amount": null, "fee": null, "ccyfee": null, "timeReg": null, "timeBlock": null, "confirmations": null } }, "back": { "code": "", "amount": null, "address": "", "tx": { "id": null, "amount": null, "fee": null, "ccyfee": null, "timeReg": null, "timeBlock": null, "confirmations": null } }, "emergency": { "status": [], "choice": "NONE", "repeat": "0" }, "token": "ls7c2edffd2acd0be11bb1b6983975d7dc8d2945400a5b012b" } } ``` Persist `data.id` and `data.token` — both are required for `/order` and `/emergency`. Show the payer `from.address`, `from.amount`, and `from.tag` when present. Deposit window is typically ~10 minutes (`time.left` at create). ### Order object fields (create & order share this shape) | Field | Type | Description | | --- | --- | --- | | `data.id` | string | 6-character order id | | `data.token` | string | Order access token (secret — treat like a credential) | | `data.type` | string | Always `fixed` here | | `data.status` | string | See statuses | | `data.time.*` | number | `reg`, `start`, `finish`, `update`, `expiration`, `left` | | `data.from.*` | object | Deposit side: code, amount, address, tag, confirmations, `tx` | | `data.to.*` | object | Payout side: BTCLN amount, bolt11 address, Lightning payment `tx` when paid | | `data.back.*` | object | Refund side when emergency / refunded | | `data.emergency.*` | object | `status[]`, `choice`, `repeat` | ## POST /api/v2/order Fetch the latest order snapshot. Same `data` shape as create. Weight: 1. ### Parameters | Name | | Type | Description | | --- | --- | --- | --- | | `id` | required | string | Order id from create | | `token` | required | string | `data.token` from create | ### Example — completed order (poll after deposit + Lightning pay) ```json // Request { "id": "QBSPEO", "token": "ls7c2edffd2acd0be11bb1b6983975d7dc8d2945400a5b012b" } // Response data (highlights) { "id": "QBSPEO", "status": "DONE", "time": { "reg": 1784479529, "start": 1784479622, "finish": 1784479625, "update": 1784479625, "expiration": 1784480129, "left": 502 }, "from": { "code": "SOL", "amount": "0.03815503", "address": "DTJnkqp4Uxx4ccpzLqSnaRtoescQRgpm1s5Pra3LesXF", "tx": { "id": "5jFuRUiLFRK3LFdghA6BVx5KkT5b58uhSWqDqmu5wJrwngKNoq8by7i7DPi55Vuwq8GyNk7yBdctM2K4oBNnHdP8", "amount": "0.03815503", "fee": "0.00000635", "ccyfee": "SOL", "timeReg": 1784479622, "timeBlock": 1784479622, "confirmations": "12" }, "reqConfirmations": 1, "maxConfirmations": 16 }, "to": { "code": "BTCLN", "amount": "0.00004243", "tx": { "id": "dd57894b0f721cfa593857f69efd10ce920f0a7710f7deb73782004d19664ac1", "amount": "0.00004243", "fee": "0.00000011", "ccyfee": "BTC", "timeReg": 1784479625, "timeBlock": 1784479625, "confirmations": "1" } }, "emergency": { "status": [], "choice": "NONE", "repeat": "0" }, "token": "ls7c2edffd2acd0be11bb1b6983975d7dc8d2945400a5b012b" } ``` When `status=DONE` after a successful swap, `to.tx.id` is the Lightning payment preimage/hash identifier for the payout. ## POST /api/v2/emergency Choose an action when `status` is `EMERGENCY`. Lightning Swap supports **refund only** — `choice=EXCHANGE` is rejected because payout is a fixed BOLT11 amount and cannot be continued at a new market rate. Weight: 1. **Deadline: 7 days.** Request the refund within 7 days of the order's last change (normally: when it entered `EMERGENCY`). After that the deposit is kept, the order stays `EMERGENCY`, and `/emergency` answers `code 400` `"refund window closed: …"`. ### Parameters | Name | | Type | Description | | --- | --- | --- | --- | | `id` | required | string | Order id | | `token` | required | string | Order token | | `choice` | required | string | Must be `REFUND` (explicit — no default) | | `address` | required | string | Refund destination on the *deposit* network. Must differ from the deposit address. | | `tag` | optional | string | Memo / destination tag when the network requires it | ### Response Success returns a boolean `data` (FixedFloat-compatible) — *not* the order object. Poll `/order` for refund progress (`back.address`, `back.tx`, then `DONE`). ```json // Request (classic underpay → refund) { "id": "WJ3IYG", "token": "ls2e2ac023d09704e467cbbf3d620ea54f5a315c04582770eb", "choice": "REFUND", "address": "BfMe1deFYJwaSeD9XoN1X8xw1PtcYjginrbvkQjS9w9U" } // Response { "code": 0, "msg": "", "data": true } ``` Idempotent: repeating the same refund address after success returns success again. A different address after submit returns `409`. ## Order statuses | Status | Meaning | | --- | --- | | `NEW` | Awaiting deposit | | `PENDING` | Deposit seen, waiting for confirmations | | `EXCHANGE` | Deposit confirmed; preparing payout | | `WITHDRAW` | Paying the Lightning invoice | | `DONE` | Terminal success — either Lightning paid, or refund broadcast completed | | `EXPIRED` | Deposit window ended without a qualifying deposit | | `EMERGENCY` | Needs `/emergency` (underpay, overpay, late deposit, …) | | `FAILED` | Terminal. Contact support with the order id | ### Emergency reasons (`data.emergency.status[]`) | Value | Meaning | | --- | --- | | `LESS` | Deposit amount below order amount (underpay) | | `MORE` | Deposit amount above order amount (overpay) | | `EXPIRED` | Deposit arrived after expiration | | `LIMIT` | Amount outside limits; used together with `LESS` or `MORE` | `emergency.choice`: `NONE` until the client calls `/emergency`, then `REFUND`. `EXCHANGE` is never set by Lightning Swap. ## Currency codes Public codes match FixedFloat naming so pair strings port cleanly. | Code | Asset | Network | Role | Req. confirmations | | --- | --- | --- | --- | --- | | `SOL` | SOL | Solana | Pay-in | 1 | | `USDTSOL` | USDT | Solana | Pay-in | 1 | | `USDCSOL` | USDC | Solana | Pay-in | 1 | | `USDTTRC` | USDT | Tron (TRC20) | Pay-in | 19 | | `ETH` | ETH | Ethereum | Pay-in | 4 | | `USDT` | USDT | Ethereum (ERC20) | Pay-in | 4 | | `USDCETH` | USDC | Ethereum (ERC20) | Pay-in | 4 | | `BTCLN` | BTC | Lightning | Payout only | — | ## HTTP / `code` mapping | `code` | HTTP | Typical cause | | --- | --- | --- | | 0 | 200 | Success (still check `data.errors` on price) | | 400 | 400 | Validation / unsupported params / bad refund address | | 401 | 401 | Missing/invalid key or signature | | 403 | 403 | Account disabled | | 404 | 404 | Unknown order or bad token | | 409 | 409 | Conflict (e.g. refund already submitted to another address) | | 429 | 429 | Weight budget or create gate | | 503 | 503 | Temporarily unavailable — retry | ## Worked example — happy path (SOL → BTCLN) Sequence: rates → ccies → price → create → poll → `DONE`. 1. Quote `direction=to` for `0.00004243` BTC → pay-in `0.03815503` SOL. 2. Create with matching BOLT11; receive deposit address `DTJnkqp4…`, order `QBSPEO`. 3. Payer sends exact `from.amount`. 4. Poll until `DONE`: deposit tx on `from.tx`, Lightning payment id on `to.tx`. ```bash # Minimal create + poll loop (bash) BODY=$(jq -nc --arg inv "$BOLT11" \ '{type:"fixed",fromCcy:"SOL",toCcy:"BTCLN",direction:"to",amount:"0.00004243",toAddress:$inv}') SIGN=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}') CREATE=$(curl -sS https://lightning-swap.com/api/v2/create \ -H "Content-Type: application/json; charset=UTF-8" \ -H "X-API-KEY: $API_KEY" -H "X-API-SIGN: $SIGN" -d "$BODY") ID=$(jq -r .data.id <<<"$CREATE") TOKEN=$(jq -r .data.token <<<"$CREATE") # … instruct payer to send .data.from.amount to .data.from.address … # then poll /api/v2/order with {"id","token"} until status is DONE / EMERGENCY / EXPIRED ``` ## Worked example — classic underpay → refund Order created for `0.038088` SOL → `0.00004265` BTC; payer sent only `0.031544` SOL → `EMERGENCY` with `LESS` → `/emergency` `REFUND` → `DONE` with refund tx on `back.tx`. ### 1. Order enters emergency after underpay ```json { "code": 0, "msg": "OK", "data": { "id": "WJ3IYG", "type": "fixed", "status": "EMERGENCY", "from": { "code": "SOL", "amount": "0.038088", "address": "7V3hvrvkZWdeFPAkDa2Z4im5FYfp1ajPpKgg5rTDWz8e", "tx": { "id": "25vM5nq7yixAszcsW3NJj4JBcFqNcP4mmXBfVpUGHTNWn8LSZX5zcEjJPpatXq7HHyUVZ9tQ4Yz4pyPaCVgwEPfH", "amount": "0.031544", "fee": "0.00000635", "ccyfee": "SOL", "confirmations": "4" } }, "to": { "code": "BTCLN", "amount": "0.00004265", "tx": { "id": null, "amount": null } }, "back": { "code": "SOL", "amount": "0.031544000", "address": "", "tx": { "id": null } }, "emergency": { "status": ["LESS"], "choice": "NONE", "repeat": "0" }, "token": "ls2e2ac023d09704e467cbbf3d620ea54f5a315c04582770eb" } } ``` ### 2. Submit refund ```json { "id": "WJ3IYG", "token": "ls2e2ac023d09704e467cbbf3d620ea54f5a315c04582770eb", "choice": "REFUND", "address": "BfMe1deFYJwaSeD9XoN1X8xw1PtcYjginrbvkQjS9w9U" } → { "code": 0, "msg": "", "data": true } ``` ### 3. Terminal state after refund broadcast ```json { "status": "DONE", "from": { "tx": { "id": "25vM5nq7yixAszcsW3NJj4JBcFqNcP4mmXBfVpUGHTNWn8LSZX5zcEjJPpatXq7HHyUVZ9tQ4Yz4pyPaCVgwEPfH", "amount": "0.031544" } }, "to": { "tx": { "id": null } }, "back": { "code": "SOL", "amount": "0.031539000", "address": "BfMe1deFYJwaSeD9XoN1X8xw1PtcYjginrbvkQjS9w9U", "tx": { "id": "LFdgqWhtpugZbCeQjm876Quva5bYKvtoHSsotZjPh7SrpgnEkf6mR2kjCxGmdCUvfsQdjBknd1VGxpUj4j3FWad", "amount": "0.031539000", "fee": "0.000005", "ccyfee": "SOL" } }, "emergency": { "status": ["LESS"], "choice": "REFUND", "repeat": "0" } } ``` Note `back.amount` matches the credited deposit (full refund). Native SOL/ETH still lose on-chain gas. `to.tx` stays empty because the Lightning invoice was never paid. ## Not supported (FixedFloat surfaces we omit) Compared to the full [FixedFloat API](https://ff.io/api), Lightning Swap does **not** implement: | FixedFloat surface | Lightning Swap | | --- | --- | | `GET /rates/float.xml` / `type=float` | Not available — fixed rate only | | Arbitrary `toCcy` (on-chain payouts) | Payout is always `BTCLN` | | `direction=from` on `/create` | Create requires `direction=to` (price still supports both) | | `POST /api/v2/setEmail` | Not implemented | | `POST /api/v2/qr` | Not implemented — build QR client-side from `from.address` | | `choice=EXCHANGE` on emergency | Rejected — use `REFUND` | | Refund on an emergency left unanswered for 7 days | Closed — the deposit is kept; `/emergency` returns `400` | | Affiliate `refcode` / `afftax` | Ignored | | Official PHP/Python FixedFloat client libraries | Use the signing examples above against our base URL | > Need keys? [Generate an API token](https://lightning-swap.com/?page=api_tokens). Questions about a production integration — use the support channel on your account. Reference behaviour for clients that already speak FixedFloat: [ff.io/api](https://ff.io/api).