Sending & Receiving Payments
In this guide, we'll focus on the Lightning Network's core functionality: sending and receiving payments. Building on the foundation laid in Authentication, Funding & Withdrawals, we'll walk you through creating invoices, handling incoming transactions, and sending payments. Let's dive in.
The Lightning Payment Process
Before we dive into the technical details, it's crucial to understand the basic flow of a Lightning payment:
- The receiver creates an invoice for the amount they expect to receive or a zero amount invoice for the sender to define the amount.
- The receiver shares this invoice with the sender.
- The sender uses this invoice to initiate the payment.
This process ensures that payments are routed correctly and the invoice includes information so the sender can verify the recipient if necessary. Let's explore an invoice and understand what it represents.
This invoice contains all the necessary information for the sender to complete the payment accurately and securely. Bolt11 invoices are encoded strings and are made for single use only.
Here's an example of an invoice:
lnbc173u1pntunzppp5a9dtl03wx0qf3jjxx2gxwcl60h7pt6erqhse7np8p3q7pvvpjrcqsp59mg7xgtqazwfae45p00e9gc33mxr65c3cdmhcaplrct0z0ncmmnqxqy8ayqnp4qf0ru8dxm7pht536amqu6re6jzsf4akdc8y7x9ze3npkcd2fh8he2rzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqzudjq473cqqqqqqqqqqqqqq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqzudjq473cqqqqqqqqqqqqqq9qcqzpgdqq9qyyssqc25kvkxwdh7k6t03hh09kxs30jhd04h77hy5un49cuag96twu4jhpd5s04lwha2fk2fp3mn6qtg3j6w2keyrlwrl7wywktp53r7nt3cprl3x76
You can decode these invoices for development and debugging purposes. Lightspark SDK provides methods to decode invoices, or from your browser, you can also use tools like Lightning Decoder. Lightning Decoder allows you to investigate the fields in a Bolt11 invoice.
Here are the key fields from the invoice above:
| Name | Description | Example Value |
|---|---|---|
| Prefix | Indicates the network (e.g., "lnbc" for Bitcoin mainnet) | lnbc |
| Amount | The payment amount in millisatoshis. This value is omitted for zero-amount invoices | 173u |
| Payee Public Key | The public key of the receiving node | 025e3e1da6df8375d23aeec1cd0f3a90a09af6cdc1c9e314598cc36c3549b9ef95 |
| Signature | Proves the invoice was created by the owner of the payee public key | c2a96658ce6dfd6d2df1bdde5b1a117caed7d6fef5c94e4ea5c73a82e96ee56570b6907d7eebf549b29218ee7a02d11969cab6483fb87ff388eb2c3488fd35c7 |
| Timestamp | When the invoice was created | 1723747393 |
| Expiry Time | How long the invoice is valid | 259200 |
| Payment Hash | A hash of the preimage used for security and to prove payment completion | e95abfbe2e33c098ca4632906763fa7dfc15eb2305e19f4c270c41e0b18190f0 |
| Description | Optional description of the payment | -- |
| Routing Hints | Optional hint to help route the payment. For example, if a Recipient node, R is not publicly accessible in the network, but the payment can be routed through Node N, this value will show the address of Node N. | 02a98e8c590a1b5602049d6b21d8f4c8861970aa310762f42eae1b2be88372e924 |
Zero amount invoices are a special type of invoice that allows the sender to specify the amount. They are useful for scenarios such as tipping, donations, or when the amount isn't known when the invoice is created. For these invoices, the receiver omits the "amount" field. The sender must specify the payment amount for this invoice to be processed. Like other Bolt11 invoices, zero amount invoices can only be paid a single time.
Receiving payments on the Lightning Network involves creating an invoice that the sender can use to initiate the payment. Let's start by walking through the process of creating an invoice with Lightspark SDKs.
The following will create an invoice for 100,000 millisatoshis (0.000001 BTC) with the memo "Payment for services". The
createInvoice function returns a String representing the invoice.You should tie this invoice to the receiver’s account. This way, when you receive a webhook notifying you of payment, you can credit the corresponding user's account.
const invoice = await client.createInvoice(
NODE_ID,
100000,
"Payment for services");
Once you have created the invoice, the receiver can share the encoded invoice string with the payer. This is usually done by copying and pasting, scanning a QR code, or via the NFC protocol.
The payer can then use their Lightning-enabled wallet to read the invoice. This kickstarts the payment process.
You need to subscribe to the
PAYMENT_FINISHED webhook to be notified when payment for an invoice occurs. Here's how to handle the webhook:- Set up your webhook endpoint as described in Authentication, Funding & Withdrawals.
- Configure your webhook in the Lightspark dashboard to listen for the
PAYMENT_FINISHEDevent. - When you receive a webhook, verify its authenticity and parse the event data:
Note this webhook is used for both OutgoingPayments and IncomingPayments. For an incoming payment, the value for
entity_id will look like: IncomingPayment:0185789d-9948-f96b-0000-4ae0b696c75f where that string is a UUID.import express from "express";
import {
WebhookEvent,
WebhookEventType,
WEBHOOKS_SIGNATURE_HEADER,
verifyAndParseWebhook,
} from "@lightsparkdev/lightspark-sdk";
const app = express();
app.post("/webhook", async (req, res) => {
const event = await verifyAndParseWebhook(
req.body,
req.headers[WEBHOOKS_SIGNATURE_HEADER],
process.env.LIGHTSPARK_WEBHOOK_SIGNING_KEY,
);
if (event.event_type === WebhookEventType.PAYMENT_FINISHED) {
const paymentId = event.entity_id;
await handlePaymentFinished(paymentId); // TODO Implement handle payment logic
}
res.send("OK!");
});
Once you have the parsed event, you can use the event.entity_id field to query the IncomingPayment object. This object contains detailed information about the transaction, including its current status, amount, and more. You can find the full schema of this object in the Lightspark SDK documentation.
Here's how you query IncomingPayment:
async function handlePaymentFinished(paymentId: string) {
let incoming = await client.executeRawQuery(
getTransactionQuery(paymentId)
);
if (incoming && incoming.typename === "IncomingPayment") {
console.log(`Payment status: ${incoming.status}`);
console.log(`Amount received: ${incoming.amount.originalValue}`);
// Update your internal systems with the payment status
}
}
You can match the received payment with the invoice and credit the appropriate user’s account.
The process of sending a payment using Lightspark Connect involves several steps:
- Calculating routing fees
- Initiating payment
- Monitoring its status
- Handling any errors
Let's walk through each step.
Nodes used to route payments typically assess fees. For example, if Alice sends a payment to Charlie and the payment route passes through Bob’s node, Bob can charge a fee for providing routing services.
Lightspark Predict helps optimize routes to minimize routing fees and maximize the probability of payment success.
With Lightspark SDKs, you can pass in the maximum fee that you’re willing to pay for routing a transaction. In most cases, the routing fee assessed will be significantly less than the maximum fee set.
Routing Fee Calculation Guideline:
As a general guideline, Lightspark suggests setting the maximum routing fee to whichever is greater 5 sats or 17 bps * transaction amount.
For use cases where you need a fee estimate, Lightspark offers a Fee Estimation API.
const feeEstimate = await client.getLightningFeeEstimateForInvoice(NODE_ID, invoice);
Once you have an invoice and defined routing fees, you're ready to initiate the payment.
If you're using an OSK node (the default), then before moving money from your node, you'll need to unlock it by loading
your node signing key:
// Password for test mode nodes is always "1234!@#$". On mainnet, it's the password you set when creating the node.
await client.loadNodeSigningKey(
/* nodeID */ "LightsparkNode:0185789d-9948-f96b-0000-4ae0b696c75f",
/* loaderArgs */ { password: "1234!@#$" },
);
After calling
payInvoice, Lightspark initiates the payment through the Lightning Network. The response from this call includes a transaction id that you should map to your customer transactions. At this point we recommened debiting the customer's account for the transaction amount.{
"id": "OutgoingPayment:0191d7d7-ed80-1d02-0000-06b61b3be2f8",
"createdAt": "2024-09-09T17:32:18.176560+00:00",
"updatedAt": "2024-09-09T17:32:18.215711+00:00",
"status": "PENDING",
"amount": {
"originalValue": 20000,
"originalUnit": "MILLISATOSHI",
"preferredCurrencyUnit": "USD",
"preferredCurrencyValueRounded": 1,
"preferredCurrencyValueApprox": 1.1312217194570136
},
"originId": "LightsparkNode:018d28e7-dc4a-f96b-0000-f276772e6fb1",
"typename": "OutgoingPayment",
"resolvedAt": null,
"transactionHash": "36a392d57915da018755b5105a859bae5ef079c2133a57f51d290976583c12bf",
"destinationId": "GraphNode:0189a572-6dba-cf00-0000-ac0908d34ea6",
"paymentRequestData": {
"encodedPaymentRequest": "lnbcrt200n1pnd7vfzpp5x63e94tezhdqrp64k5g94pvm4e00q7wzzva90aga9yyhvkpuz2lsdpyv4uxzmtsd3jjqumrwf5hqapqwpshjmt9de6qcqzpgxqyz5vqsp5qxpjerws3fyzxjj0gmskyrqlmltqr6ndkwh85l5whlyjny8jy5gs9qxpqysgq7xxvxg4wfuk2au953knmzy5nyux4r3adf7kcpsugtlew4pv72mgnlmxrrlcx5h322kglg9nygzjzxkf9h7a6jh0dhewrem8h3qdkurgqwvj699",
"bitcoinNetwork": "REGTEST",
"paymentHash": "36a392d57915da018755b5105a859bae5ef079c2133a57f51d290976583c12bf",
"amount": {
"originalValue": 20,
"originalUnit": "SATOSHI",
"preferredCurrencyUnit": "USD",
"preferredCurrencyValueRounded": 1,
"preferredCurrencyValueApprox": 1.1312217194570136
},
"createdAt": "2024-09-09T17:32:18+00:00",
"expiresAt": "2024-09-10T17:32:18+00:00",
"destination": {
"id": "GraphNode:0189a572-6dba-cf00-0000-ac0908d34ea6",
"createdAt": "2023-07-30T06:18:07.162759+00:00",
"updatedAt": "2024-09-09T17:01:45.292890+00:00",
"bitcoinNetwork": "REGTEST",
"displayName": "ls_test_vSViIQitob_SE",
"typename": "GraphNode",
"alias": "ls_test_vSViIQitob_SE",
"color": "#3399ff",
"conductivity": null,
"publicKey": "02253935a5703a6f0429081e08d2defce0faa15f4d75305302284751d53a4e0608"
},
"typename": "InvoiceData",
"memo": "example script payment"
},
"failureReason": null
}
If a transaction fails and it's status is
FAILED, you should credit the sender’s account.Lightning Network payments are asynchronous, so you should listen to webhooks for transaction updates. In the Receiving Payments section above you implemented a Webhook handler to receive
PAYMENT_FINISHED webhooks. To this handler, you'll need to add logic to check the entity type for OutgoingPayment grab the entity_id and query for the OutgoingPayment. For an outgoing payment, the value for entity_id will look like: OutgoingPayment:0185789d-9948-f96b-0000-4ae0b696c75f.Once you have the parsed event, you can use the
event.entity_id field to query the OutgoingPayment object. This object contains detailed information about the transaction, including its current status, amount, fees, and more. You can find the full schema of this object in the Lightspark SDK documentation.Here's how you query OutgoingPayment:
async function handlePaymentFinished(paymentId: string) {
let outgoing = await client.executeRawQuery(getTransactionQuery(paymentId));
if (outgoing) {
console.log(`Payment status: ${outgoing.status}`);
console.log(`Amount sent: ${outgoing.amount.originalValue}`);
// Update your internal systems with the payment status
}
}
To note, while Lightning payments are typically instantaneous, payment speed may vary due to the sending and receiving wallet.
-
Custodial-to-Custodial Transfers
- User funds are held by an exchange or service
- Money transfers are usually instant as it's processed by the custodial body
-
Transfers involving Self-Custody Wallets
- User controls their funds
- Money transfer requires the user node to be online and accessible
As an integrator, you should consider educating users about the Lightning payment experience. If a payment is not instant, you can show a snackbar, toast or other informative text to help your user understand that self custody wallets need to be online and may take longer to receive a Lightning payment.
Test your integrations with various wallet types and simulate different failure scenarios to ensure robust handling.
Common error causes when sending payments include:
-
No routes available due to insufficient fees. If you receive this error, use the recommended max fee we highlighted earlier - max(5 sats, 17 bps)
-
Failed transactions to self-custody wallets when the receiver is offline. In such cases, the receiver should be informed that they need to stay online for the transaction to succeed.
-
Exceeding maximum receivable amount for custodial apps. Some apps (e.g., CashApp) have a maximum amount that their users can receive in a period of time. If users face this error, they should try sending payment at a different time.
In this deep dive, we covered sending and receiving payments using Lightspark Connect. We've walked through creating invoices, sharing invoices, handling routing fees, initiating payments, and handling both outgoing and incoming transactions. We've also touched on important considerations like handing webhooks and managing errors.