Webhooks
Webhooks provide instant notifications when payment status changes. Instead of polling the API, your server receives an HTTP POST request with payment details.
Why Use Webhooks?
| Method | Pros | Cons |
|---|---|---|
| Polling | Simple to implement | Delayed detection, API overhead |
| Webhooks | Instant, efficient | Requires public endpoint |
For production use, webhooks are strongly recommended.
Setup
1. Create a Webhook Endpoint
Create an HTTP POST endpoint in your application:
// Express.js
app.post('/webhooks/lightning', express.json(), async (req, res) => {
try {
// Process webhook
console.log('Received webhook:', req.body);
// Always return 200 quickly
res.status(200).send('OK');
// Process asynchronously
await processPayment(req.body);
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Error');
}
});
2. Configure Webhook URL
Set your webhook URL in Lightning Enable:
Option A: During merchant setup
{
"callbackUrl": "https://yourapp.com/webhooks/lightning"
}
Option B: Via admin API
curl -X PUT https://api.lightningenable.com/api/admin/merchants/{id} \
-H "X-API-Key: admin-key" \
-d '{"callbackUrl": "https://yourapp.com/webhooks/lightning"}'
3. Verify Webhook Signatures
Always verify webhook signatures to ensure authenticity:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
const expectedSignature = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhooks/lightning', express.json(), (req, res) => {
const signature = req.headers['x-gateway-signature'];
if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
console.warn('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// Process verified webhook
processPayment(req.body);
res.status(200).send('OK');
});
Webhook Payload
When a payment status changes, you receive:
{
"invoiceId": "inv_abc123def456",
"orderId": "ORDER-12345",
"openNodeChargeId": "charge_xyz789",
"status": "paid",
"amount": 25.00,
"currency": "USD",
"amountSats": 62500,
"paidAt": "2024-12-29T12:03:45Z",
"metadata": {
"customerId": "cust_123"
}
}
Payload Fields
| Field | Type | Description |
|---|---|---|
invoiceId | string | Lightning Enable invoice ID |
orderId | string | Your order ID |
openNodeChargeId | string | OpenNode charge ID |
status | string | Payment status |
amount | decimal | Payment amount |
currency | string | Currency code |
amountSats | integer | Amount in satoshis |
paidAt | datetime | Payment timestamp (if paid) |
metadata | object | Your custom metadata |
Payment Statuses
| Status | Description | Action |
|---|---|---|
paid | Payment confirmed | Fulfill order |
processing | Payment detected | Wait for confirmation |
expired | Invoice expired | Notify customer |
underpaid | Insufficient amount | Handle manually |
refunded | Payment refunded | Update records |
Implementation Examples
Node.js / Express
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
hmac.update(JSON.stringify(payload));
const expected = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature || ''),
Buffer.from(expected)
);
}
app.post('/webhooks/lightning', async (req, res) => {
const signature = req.headers['x-gateway-signature'];
// Verify signature
if (!verifySignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { invoiceId, orderId, status } = req.body;
// Handle different statuses
switch (status) {
case 'paid':
await fulfillOrder(orderId);
await sendConfirmationEmail(req.body.customerEmail);
break;
case 'expired':
await markOrderExpired(orderId);
break;
case 'refunded':
await processRefund(orderId);
break;
}
res.status(200).send('OK');
});
app.listen(3000);
C# / ASP.NET Core
[ApiController]
[Route("webhooks")]
public class WebhooksController : ControllerBase
{
private readonly IOrderService _orderService;
private readonly IConfiguration _config;
public WebhooksController(IOrderService orderService, IConfiguration config)
{
_orderService = orderService;
_config = config;
}
[HttpPost("lightning")]
public async Task<IActionResult> HandleWebhook([FromBody] WebhookPayload payload)
{
// Verify signature
var signature = Request.Headers["X-Gateway-Signature"].FirstOrDefault();
if (!VerifySignature(payload, signature))
{
return Unauthorized("Invalid signature");
}
switch (payload.Status)
{
case "paid":
await _orderService.FulfillOrderAsync(payload.OrderId);
break;
case "expired":
await _orderService.ExpireOrderAsync(payload.OrderId);
break;
}
return Ok();
}
private bool VerifySignature(WebhookPayload payload, string signature)
{
var secret = _config["WebhookSecret"];
var json = JsonSerializer.Serialize(payload);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(json));
var expected = Convert.ToHexString(hash).ToLowerInvariant();
return signature == expected;
}
}
public record WebhookPayload(
string InvoiceId,
string OrderId,
string Status,
decimal Amount,
string Currency
);
Python / Flask
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
def verify_signature(payload, signature):
expected = hmac.new(
WEBHOOK_SECRET.encode(),
json.dumps(payload).encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature or '', expected)
@app.route('/webhooks/lightning', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Gateway-Signature')
if not verify_signature(request.json, signature):
return jsonify({'error': 'Invalid signature'}), 401
data = request.json
status = data.get('status')
order_id = data.get('orderId')
if status == 'paid':
fulfill_order(order_id)
elif status == 'expired':
expire_order(order_id)
return 'OK', 200
Retry Logic
Lightning Enable retries failed webhooks:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 15 minutes |
| 4th retry | 1 hour |
| Final | 24 hours |
A webhook is considered successful when your endpoint returns a 2xx status code.
Local Development
Use ngrok to test webhooks locally:
# Install ngrok
npm install -g ngrok
# Start your local server
npm start
# Expose port 3000
ngrok http 3000
Configure the ngrok URL as your webhook endpoint:
https://abc123.ngrok.io/webhooks/lightning
Best Practices
Return 200 Quickly
Process webhooks asynchronously to avoid timeouts:
app.post('/webhooks/lightning', (req, res) => {
// Return 200 immediately
res.status(200).send('OK');
// Process asynchronously
processWebhookAsync(req.body).catch(console.error);
});
Idempotent Processing
Handle duplicate webhooks gracefully:
async function processPayment(payload) {
// Check if already processed
const existing = await db.payments.findOne({ invoiceId: payload.invoiceId });
if (existing && existing.processed) {
console.log('Already processed:', payload.invoiceId);
return;
}
// Process payment
await fulfillOrder(payload.orderId);
// Mark as processed
await db.payments.updateOne(
{ invoiceId: payload.invoiceId },
{ $set: { processed: true, processedAt: new Date() } }
);
}
Log Everything
Keep detailed logs for debugging:
app.post('/webhooks/lightning', (req, res) => {
console.log('Webhook received:', {
invoiceId: req.body.invoiceId,
status: req.body.status,
signature: req.headers['x-gateway-signature']?.substring(0, 10) + '...'
});
// ... process webhook
});
Troubleshooting
Webhooks Not Received
- Verify webhook URL is publicly accessible
- Check for HTTPS requirement
- Ensure firewall allows incoming connections
- Review webhook logs in Lightning Enable dashboard
Signature Verification Fails
- Ensure you're using the correct webhook secret
- Verify JSON serialization matches
- Check for encoding issues (UTF-8)
Duplicate Webhooks
- Implement idempotent processing
- Store invoice IDs that have been processed
- Check before fulfilling orders
Next Steps
- Refunds - Handle returns
- Error Handling - Error codes and handling
- API Reference - Complete webhook documentation