API documentation
Base URL: https://lightning-swap.com
· Authenticated methods under /api/v2
FixedFloat-compatible — subset of 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.
Getting started
- Create an account and open the API tokens page.
- Generate a key. Copy the API key (public) and API secret — the secret is shown once.
- Sign every authenticated POST with HMAC-SHA256 over the exact JSON body string.
- Create orders with
type=fixed,direction=to,toCcy=BTCLN, and a BOLT11 intoAddress. - Poll
/orderuntilDONE,EXPIRED, orEMERGENCY. On emergency, call/emergencywithchoice=REFUND.
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
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
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
/createcalls are blocked (Create rate limit gate reached).
Response envelope
Every JSON method returns:
{
"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
GET /rates/fixed.xml— cache indicative rates and min/max (optional; no auth).POST /api/v2/ccies— confirm pay-in codes andrecv/send.POST /api/v2/price— quotedirection=tofor a Lightning amount (orfromfor a deposit amount).POST /api/v2/create— binding order; storedata.id+data.token; showfrom.address/from.amountto the payer.POST /api/v2/order— poll until terminal.- If
status=EMERGENCY,POST /api/v2/emergencywithchoice=REFUNDand a refund address on the deposit network; poll untilDONEwithback.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 <item>
| Field | Type | Description |
|---|---|---|
<from> | string | Pay-in currency code |
<to> | string | Always BTCLN for Lightning Swap |
<in> / <out> | number | Reference amounts defining the rate |
<amount> | number | Reserve hint for to (not a hard max) |
<tofee> | string | Lightning network fee folded into quotes, not included in out |
<minamount> / <maxamount> | string | Min/max pay-in for create |
Example (live shape — SOL → BTCLN)
<rates>
<item>
<from>SOL</from>
<to>BTCLN</to>
<in>1</in>
<out>0.00117230</out>
<amount>0.00320351</amount>
<tofee>0.00000200 BTCLN</tofee>
<minamount>0.02500000 SOL</minamount>
<maxamount>1.35281076 SOL</maxamount>
</item>
…
</rates>
Sample captured from a Lightning Swap exercise run against a live rates feed.
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)
{
"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 (real exercise quote)
// 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 (stale rates demote the pair) |
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.
typemust befixed(float orders are rejected).directionmust beto— amount is the BTC the invoice receives.toCcymust beBTCLN.toAddressmust be a valid BOLT11 with an amount. Ifamountis also sent, it must match the invoice’s BTC value.
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: <opaque string> (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.
Example — create (SOL → Lightning, from exercise)
// 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)
// 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.
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).
// 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, …) |
Emergency reasons (data.emergency.status[])
| Value | Meaning |
|---|---|
LESS | Deposit amount below order amount (underpay) |
MORE | Deposit amount above order amount |
EXPIRED | Deposit arrived after expiration (within late-deposit grace) |
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 | 1 |
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 | Pair offline, deposit address/watcher unavailable, refund in flight |
Worked example — happy path (SOL → BTCLN)
Captured from a full Lightning Swap exercise (sol_sol_lightningswap_exercise.json).
Sequence: rates → ccies → price → create → poll → DONE.
- Quote
direction=tofor0.00004243BTC → pay-in0.03815503SOL. - Create with matching BOLT11; receive deposit address
DTJnkqp4…, orderQBSPEO. - Payer sends exact
from.amount. - Poll until
DONE: deposit tx onfrom.tx, Lightning payment id onto.tx.
# 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
From stress_underpay_classic_sol_sol_lightningswap.json:
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
{
"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
{
"id": "WJ3IYG",
"token": "ls2e2ac023d09704e467cbbf3d620ea54f5a315c04582770eb",
"choice": "REFUND",
"address": "BfMe1deFYJwaSeD9XoN1X8xw1PtcYjginrbvkQjS9w9U"
}
→ { "code": 0, "msg": "", "data": true }
3. Terminal state after refund broadcast
{
"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, 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 |
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. Questions about a production integration — use the support channel on your account. Reference behaviour for clients that already speak FixedFloat: ff.io/api.