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

  1. Create an account and open the API tokens page.
  2. Generate a key. Copy the API key (public) and API secret — the secret is shown once.
  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. Poll /order until DONE, EXPIRED, or EMERGENCY. On emergency, call /emergency with choice=REFUND.

Authentication

Authenticated requests send JSON (UTF-8) with these headers:

HeaderValue
Content-Typeapplication/json; charset=UTF-8
X-API-KEYYour API key (UUID public key)
X-API-SIGNHMAC-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.

EndpointWeight
POST /api/v2/create50
All other authenticated API calls1
GET /rates/fixed.xml0 (public, no auth)
  • Exceeding the minute budget returns code: 429Rate 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:

{
  "code": 0,
  "msg": "OK",
  "data": { }
}
FieldTypeDescription
codenumber0 = success. Non-zero mirrors HTTP intent (401, 404, 409, 429, 503, or 400).
msgstringHuman-readable status or error.
dataobject / array / boolean / nullPayload. 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 <item>

FieldTypeDescription
<from>stringPay-in currency code
<to>stringAlways BTCLN for Lightning Swap
<in> / <out>numberReference amounts defining the rate
<amount>numberReserve hint for to (not a hard max)
<tofee>stringLightning network fee folded into quotes, not included in out
<minamount> / <maxamount>stringMin/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

FieldTypeDescription
data[].codestringPublic code used as fromCcy / toCcy
data[].coinstringAsset ticker (BTC, SOL, USDT, …)
data[].networkstringChain / Lightning network (SOL, ETH, TRX, LN)
data[].namestringDisplay name
data[].recv0|1Accepts deposits from client
data[].send0|1Can send to client
data[].tagstring|nullMemo / destination-tag field name when required
data[].contractstring|nullToken contract when not native
data[].logo / color / priorityDisplay 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

NameTypeDescription
typerequiredstringMust be fixed
fromCcyrequiredstringPay-in code (SOL, USDTTRC, …)
toCcyrequiredstringMust be BTCLN
directionrequiredstringfrom = amount is pay-in; to = amount is Lightning BTC received
amountrequired*numericAmount in the currency implied by direction. Optional if toAddress (BOLT11) is provided — invoice sats win.
toAddressoptionalstringBOLT11; 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

CodeMeaning
OFFLINE_FROM / OFFLINE_TOPair or side unavailable (stale rates demote the pair)
MAINTENANCE_FROM / MAINTENANCE_TOSide in maintenance
LIMIT_MIN / LIMIT_MAXAmount outside min/max
INVALID_FROM / INVALID_TOUnknown or unsupported currency
UNSUPPORTED_TYPEOnly fixed is allowed
UNSUPPORTED_DIRECTIONNot from or to
AMOUNT_MISMATCHProvided amount ≠ BOLT11 amount (direction=to)
MISSING_AMOUNTNo 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 BOLT11 with an amount. If amount is also sent, it must match the invoice’s BTC value.

Parameters

NameTypeDescription
typerequiredstringfixed
fromCcyrequiredstringPay-in currency code
toCcyrequiredstringBTCLN
directionrequiredstringto
amountrequired*numericBTC the invoice should receive; must match BOLT11 when both are sent
toAddressrequiredstringBOLT11 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)

FieldTypeDescription
data.idstring6-character order id
data.tokenstringOrder access token (secret — treat like a credential)
data.typestringAlways fixed here
data.statusstringSee statuses
data.time.*numberreg, start, finish, update, expiration, left
data.from.*objectDeposit side: code, amount, address, tag, confirmations, tx
data.to.*objectPayout side: BTCLN amount, bolt11 address, Lightning payment tx when paid
data.back.*objectRefund side when emergency / refunded
data.emergency.*objectstatus[], choice, repeat

POST /api/v2/order

Fetch the latest order snapshot. Same data shape as create. Weight: 1.

Parameters

NameTypeDescription
idrequiredstringOrder id from create
tokenrequiredstringdata.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 onlychoice=EXCHANGE is rejected because payout is a fixed BOLT11 amount and cannot be continued at a new market rate. Weight: 1.

Parameters

NameTypeDescription
idrequiredstringOrder id
tokenrequiredstringOrder token
choicerequiredstringMust be REFUND (explicit — no default)
addressrequiredstringRefund destination on the deposit network. Must differ from the deposit address.
tagoptionalstringMemo / 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

StatusMeaning
NEWAwaiting deposit
PENDINGDeposit seen, waiting for confirmations
EXCHANGEDeposit confirmed; preparing payout
WITHDRAWPaying the Lightning invoice
DONETerminal success — either Lightning paid, or refund broadcast completed
EXPIREDDeposit window ended without a qualifying deposit
EMERGENCYNeeds /emergency (underpay, overpay, late deposit, …)

Emergency reasons (data.emergency.status[])

ValueMeaning
LESSDeposit amount below order amount (underpay)
MOREDeposit amount above order amount
EXPIREDDeposit arrived after expiration (within late-deposit grace)
LIMITAmount 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.

CodeAssetNetworkRoleReq. confirmations
SOLSOLSolanaPay-in1
USDTSOLUSDTSolanaPay-in1
USDCSOLUSDCSolanaPay-in1
USDTTRCUSDTTron (TRC20)Pay-in1
ETHETHEthereumPay-in4
USDTUSDTEthereum (ERC20)Pay-in4
USDCETHUSDCEthereum (ERC20)Pay-in4
BTCLNBTCLightningPayout only

HTTP / code mapping

codeHTTPTypical cause
0200Success (still check data.errors on price)
400400Validation / unsupported params / bad refund address
401401Missing/invalid key or signature
403403Account disabled
404404Unknown order or bad token
409409Conflict (e.g. refund already submitted to another address)
429429Weight budget or create gate
503503Pair 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.

  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.
# 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 REFUNDDONE 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 surfaceLightning Swap
GET /rates/float.xml / type=floatNot available — fixed rate only
Arbitrary toCcy (on-chain payouts)Payout is always BTCLN
direction=from on /createCreate requires direction=to (price still supports both)
POST /api/v2/setEmailNot implemented
POST /api/v2/qrNot implemented — build QR client-side from from.address
choice=EXCHANGE on emergencyRejected — use REFUND
Affiliate refcode / afftaxIgnored
Official PHP/Python FixedFloat client librariesUse 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.