Your First Payment
This tutorial walks you through creating, displaying, and confirming your first Lightning Network payment using Lightning Enable.
What We'll Build
A simple payment flow that:
- Creates a Lightning invoice
- Displays a QR code for payment
- Polls for payment 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 Payment
First, create a payment invoice by calling the Lightning Enable API:
async function createPayment(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,
successUrl: window.location.origin + '/success'
})
});
if (!response.ok) {
throw new Error('Failed to create payment');
}
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>Pay with Lightning</title>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<style>
.payment-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-unpaid { background: #fef3c7; color: #92400e; }
.status-paid { background: #dcfce7; color: #166534; }
.status-expired { background: #fee2e2; color: #991b1b; }
</style>
</head>
<body>
<div class="payment-container">
<h1>Pay $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-unpaid" id="status">
Waiting for payment...
</div>
</div>
<script>
// Payment data (in production, get from server)
const payment = {
invoiceId: 'inv_abc123',
lightningInvoice: 'lnbc250n1...'
};
// Display QR code
QRCode.toCanvas(
document.getElementById('qr'),
payment.lightningInvoice,
{ width: 256 }
);
// Display invoice text
document.getElementById('invoice').textContent = payment.lightningInvoice;
</script>
</body>
</html>
Step 3: Poll for Payment Status
Check the payment status every few seconds:
async function checkPaymentStatus(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 payment = await checkPaymentStatus(invoiceId);
switch (payment.status) {
case 'paid':
statusEl.className = 'status status-paid';
statusEl.textContent = 'Payment received!';
clearInterval(interval);
// Redirect to success page
window.location.href = '/success?order=' + payment.orderId;
break;
case 'expired':
statusEl.className = 'status status-expired';
statusEl.textContent = 'Invoice expired';
clearInterval(interval);
break;
case 'processing':
statusEl.textContent = 'Payment detected, confirming...';
break;
}
}, 3000); // Check every 3 seconds
}
// Start polling when page loads
startPolling('inv_abc123');
Step 4: Handle Success
When the payment is confirmed, redirect to a success page:
<!-- success.html -->
<!DOCTYPE html>
<html>
<head>
<title>Payment Successful</title>
</head>
<body>
<div style="text-align: center; padding: 50px;">
<h1>Payment Successful!</h1>
<p>Thank you for your payment.</p>
<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>
Complete Working Example
Here's a complete, working HTML file you can use:
<!DOCTYPE html>
<html>
<head>
<title>Lightning Payment Demo</title>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<style>
* { box-sizing: border-box; }
body {
font-family: system-ui;
background: #f9fafb;
margin: 0;
padding: 20px;
}
.container {
max-width: 400px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
h1 { margin-top: 0; }
.amount { font-size: 32px; font-weight: bold; }
.qr-wrapper {
background: white;
padding: 20px;
display: inline-block;
border-radius: 8px;
margin: 20px 0;
}
.invoice {
word-break: break-all;
font-size: 11px;
background: #f5f5f5;
padding: 10px;
border-radius: 8px;
margin: 20px 0;
}
.copy-btn {
background: #f59e0b;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
}
.status {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.status-waiting { background: #fef3c7; }
.status-paid { background: #dcfce7; }
.options {
display: flex;
gap: 10px;
margin-top: 20px;
}
.option {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
cursor: pointer;
text-align: center;
}
.option:hover { border-color: #f59e0b; }
.option.active { border-color: #f59e0b; background: #fef3c7; }
</style>
</head>
<body>
<div class="container">
<h1>Complete Payment</h1>
<div class="amount">$25.00 USD</div>
<div class="options">
<div class="option active" onclick="showLightning()">Lightning</div>
<div class="option" onclick="showOnchain()">Bitcoin</div>
<div class="option" onclick="openHosted()">Hosted</div>
</div>
<div id="lightning-view">
<div class="qr-wrapper">
<canvas id="qr-lightning"></canvas>
</div>
<div class="invoice" id="lightning-invoice"></div>
<button class="copy-btn" onclick="copyLightning()">Copy Invoice</button>
</div>
<div id="onchain-view" style="display:none;">
<div class="qr-wrapper">
<canvas id="qr-onchain"></canvas>
</div>
<div class="invoice" id="onchain-address"></div>
<button class="copy-btn" onclick="copyOnchain()">Copy Address</button>
</div>
<div class="status status-waiting" id="status">
Waiting for payment...
</div>
</div>
<script>
// Sample payment data - replace with your API response
const payment = {
invoiceId: 'inv_demo123',
lightningInvoice: 'lnbc250n1pjq8xyzpp5demo...',
onchainAddress: 'bc1qdemoxyz...',
hostedCheckoutUrl: 'https://checkout.opennode.com/demo'
};
// Generate QR codes
QRCode.toCanvas(document.getElementById('qr-lightning'),
payment.lightningInvoice, { width: 200 });
QRCode.toCanvas(document.getElementById('qr-onchain'),
'bitcoin:' + payment.onchainAddress, { width: 200 });
// Display invoice/address
document.getElementById('lightning-invoice').textContent =
payment.lightningInvoice;
document.getElementById('onchain-address').textContent =
payment.onchainAddress;
// View switching
function showLightning() {
document.getElementById('lightning-view').style.display = 'block';
document.getElementById('onchain-view').style.display = 'none';
}
function showOnchain() {
document.getElementById('lightning-view').style.display = 'none';
document.getElementById('onchain-view').style.display = 'block';
}
function openHosted() {
window.location.href = payment.hostedCheckoutUrl;
}
// Copy functions
function copyLightning() {
navigator.clipboard.writeText(payment.lightningInvoice);
alert('Copied!');
}
function copyOnchain() {
navigator.clipboard.writeText(payment.onchainAddress);
alert('Copied!');
}
// Status polling (in production, use your API)
// setInterval(checkStatus, 3000);
</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') {
// Process the order
processOrder(data.orderId, data.invoiceId);
}
res.status(200).send('OK');
});
Testing with Testnet
- Use OpenNode Testnet at dev.opennode.com
- Get testnet Bitcoin from a faucet
- Use a testnet Lightning wallet (Polar, Thunderhub)
- Pay the invoice and watch the status change
Next Steps
- Webhooks Implementation - Instant payment notifications
- Refunds API - Handle returns
- Error Handling - Handle edge cases