Your First Settlement
This tutorial walks through creating, displaying, and confirming your first Lightning Network settlement using Lightning Enable.
What We Will Build
A simple settlement flow that:
- Creates a Lightning invoice
- Displays a QR code for the payer
- Polls for settlement confirmation
- Shows a success message
Prerequisites
- Lightning Enable API key (from your subscription)
- OpenNode API key configured in your merchant account
- Basic HTML/JavaScript knowledge
Step 1: Create the Settlement Request
Create a settlement invoice by calling the Lightning Enable API:
async function createSettlement(orderId, amount, description) {
const response = await fetch('https://api.lightningenable.com/api/payments', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
orderId: orderId,
amount: amount,
currency: 'USD',
description: description
})
});
if (!response.ok) {
throw new Error('Failed to create settlement');
}
return await response.json();
}
The response includes everything you need:
{
"invoiceId": "inv_abc123",
"status": "unpaid",
"lightningInvoice": "lnbc250n1...",
"onchainAddress": "bc1q...",
"hostedCheckoutUrl": "https://checkout.opennode.com/...",
"expiresAt": "2024-12-29T12:15:00Z"
}
Step 2: Display the QR Code
Use a QR code library to display the Lightning invoice:
<!DOCTYPE html>
<html>
<head>
<title>Complete Settlement</title>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<style>
.settlement-container {
max-width: 400px;
margin: 50px auto;
text-align: center;
font-family: system-ui;
}
.qr-code {
margin: 20px 0;
}
.invoice-text {
word-break: break-all;
font-size: 12px;
color: #666;
background: #f5f5f5;
padding: 10px;
border-radius: 8px;
}
.status {
padding: 10px 20px;
border-radius: 20px;
display: inline-block;
margin-top: 20px;
}
.status-pending { background: #fef3c7; color: #92400e; }
.status-settled { background: #dcfce7; color: #166534; }
.status-expired { background: #fee2e2; color: #991b1b; }
</style>
</head>
<body>
<div class="settlement-container">
<h1>$25.00</h1>
<p>Scan with your Lightning wallet</p>
<div class="qr-code">
<canvas id="qr"></canvas>
</div>
<div class="invoice-text" id="invoice">
Loading...
</div>
<div class="status status-pending" id="status">
Awaiting settlement...
</div>
</div>
<script>
// Settlement data (in production, get from server)
const settlement = {
invoiceId: 'inv_abc123',
lightningInvoice: 'lnbc250n1...'
};
// Display QR code
QRCode.toCanvas(
document.getElementById('qr'),
settlement.lightningInvoice,
{ width: 256 }
);
// Display invoice text
document.getElementById('invoice').textContent = settlement.lightningInvoice;
</script>
</body>
</html>
Step 3: Poll for Settlement Status
Check the settlement status every few seconds:
async function checkSettlementStatus(invoiceId) {
const response = await fetch(
`https://api.lightningenable.com/api/payments/${invoiceId}`,
{
headers: {
'X-API-Key': 'YOUR_API_KEY'
}
}
);
return await response.json();
}
function startPolling(invoiceId) {
const statusEl = document.getElementById('status');
const interval = setInterval(async () => {
const settlement = await checkSettlementStatus(invoiceId);
switch (settlement.status) {
case 'paid':
statusEl.className = 'status status-settled';
statusEl.textContent = 'Settlement complete';
clearInterval(interval);
// Redirect to success page
window.location.href = '/success?order=' + settlement.orderId;
break;
case 'expired':
statusEl.className = 'status status-expired';
statusEl.textContent = 'Invoice expired';
clearInterval(interval);
break;
case 'processing':
statusEl.textContent = 'Settlement detected, confirming...';
break;
}
}, 3000); // Check every 3 seconds
}
// Start polling when page loads
startPolling('inv_abc123');
Step 4: Handle Success
When the settlement is confirmed, redirect to a success page:
<!-- success.html -->
<!DOCTYPE html>
<html>
<head>
<title>Settlement Complete</title>
</head>
<body>
<div style="text-align: center; padding: 50px;">
<h1>Settlement Complete</h1>
<p>Order ID: <span id="orderId"></span></p>
</div>
<script>
const params = new URLSearchParams(window.location.search);
document.getElementById('orderId').textContent = params.get('order');
</script>
</body>
</html>
Using Webhooks Instead of Polling
For production, webhooks are more reliable than polling:
// Express.js webhook handler
app.post('/webhooks/lightning', express.raw({type: '*/*'}), (req, res) => {
const signature = req.headers['x-gateway-signature'];
const payload = req.body.toString();
// Verify signature
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const data = JSON.parse(payload);
if (data.status === 'paid') {
// Settlement confirmed - update your records
processSettlement(data.orderId, data.invoiceId);
}
res.status(200).send('OK');
});
Settlement States
| State | Description |
|---|---|
unpaid | Invoice created, awaiting settlement |
processing | Settlement detected, confirming |
paid | Settlement complete |
expired | Invoice expired without settlement |
Testing with Testnet
- Use OpenNode Testnet at dev.opennode.com
- Get testnet Bitcoin from a faucet
- Use a testnet Lightning wallet (Polar, Thunderhub)
- Complete the settlement and watch the status change
Next Steps
- Webhooks Implementation - Instant settlement notifications
- Refunds API - Handle refunds
- Error Handling - Handle edge cases