Webhook Configuration
Webhooks provide real-time notifications when payment events occur. This guide explains how webhooks flow through the system and how to configure them.
Webhook Flow
Customer pays invoice
↓
OpenNode confirms payment
↓
OpenNode sends webhook to Lightning Enable
↓
Lightning Enable processes and forwards to your endpoint
↓
Your server fulfills the order
Architecture
Lightning Enable acts as a webhook proxy:
- OpenNode → Lightning Enable: OpenNode sends payment notifications
- Lightning Enable → Your Server: We forward events to your configured endpoint
This provides:
- Consistent webhook format across payment providers
- Signature verification
- Retry handling
- Event logging
Configure Webhooks
Step 1: Set Your Webhook URL
Configure your endpoint in Lightning Enable:
curl -X PUT https://api.lightningenable.com/api/admin/merchants/{merchantId} \
-H "X-API-Key: admin-api-key" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://your-site.com/webhooks/lightning",
"webhookSecret": "your-webhook-secret"
}'
Step 2: OpenNode Webhook (Automatic)
Lightning Enable automatically configures OpenNode to send webhooks to our endpoint. No manual configuration needed in OpenNode dashboard.
Step 3: Implement Webhook Handler
Create an endpoint to receive webhooks:
// Express.js example
app.post('/webhooks/lightning', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = req.body.toString();
// Verify signature
if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
// Handle event
switch (event.event) {
case 'payment.completed':
handlePaymentCompleted(event.data);
break;
case 'payment.expired':
handlePaymentExpired(event.data);
break;
case 'refund.completed':
handleRefundCompleted(event.data);
break;
}
res.status(200).send('OK');
});
Webhook Secret
The webhook secret is used to verify that webhooks are from Lightning Enable.
Generate a Secret
Generate a secure random string:
# Using OpenSSL
openssl rand -hex 32
# Using Python
python -c "import secrets; print(secrets.token_hex(32))"
# Using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Store Securely
# Environment variable
WEBHOOK_SECRET=your-64-character-hex-string
Signature Verification
Always verify webhook signatures to ensure authenticity.
Signature Format
X-Webhook-Signature: sha256=abc123...
JavaScript Verification
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
C# Verification
public static bool VerifySignature(string payload, string signature, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expected = "sha256=" + BitConverter.ToString(hash).Replace("-", "").ToLower();
return signature == expected;
}
Python Verification
import hmac
import hashlib
def verify_signature(payload, signature, secret):
expected = 'sha256=' + hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Webhook Events
payment.completed
Sent when a Lightning invoice is paid:
{
"event": "payment.completed",
"timestamp": "2024-12-29T12:05:00Z",
"data": {
"invoiceId": "inv_abc123def456",
"orderId": "ORDER-12345",
"status": "paid",
"amount": 49.99,
"currency": "USD",
"amountSats": 125000,
"paidAt": "2024-12-29T12:05:00Z"
}
}
payment.expired
Sent when an invoice expires without payment:
{
"event": "payment.expired",
"timestamp": "2024-12-29T13:00:00Z",
"data": {
"invoiceId": "inv_abc123def456",
"orderId": "ORDER-12345",
"status": "expired",
"expiredAt": "2024-12-29T13:00:00Z"
}
}
refund.completed
Sent when a refund is processed:
{
"event": "refund.completed",
"timestamp": "2024-12-29T14:01:00Z",
"data": {
"refundId": "ref_xyz789abc",
"invoiceId": "inv_abc123def456",
"status": "completed",
"amount": 49.99,
"currency": "USD"
}
}
Webhook Requirements
Endpoint Requirements
- HTTPS in production (HTTP allowed for localhost)
- Return 200-299 status code within 30 seconds
- Accept POST requests with JSON body
Headers Sent
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | HMAC-SHA256 signature |
X-Webhook-Id | Unique webhook delivery ID |
X-Webhook-Timestamp | ISO 8601 timestamp |
Retry Policy
If your endpoint fails, we retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the webhook is marked as failed.
Testing Webhooks
Local Development
Use ngrok to expose your local server:
# Terminal 1: Start your server
npm start # http://localhost:3000
# Terminal 2: Start ngrok
ngrok http 3000
# Copy the ngrok URL (e.g., https://abc123.ngrok.io)
# Use as your webhook URL
Send Test Webhook
curl -X POST https://your-ngrok-url.ngrok.io/webhooks/lightning \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: sha256=test" \
-d '{
"event": "payment.completed",
"timestamp": "2024-12-29T12:05:00Z",
"data": {
"invoiceId": "inv_test123",
"orderId": "TEST-001",
"status": "paid",
"amount": 10.00
}
}'
Webhook Testing Checklist
- Endpoint returns 200 status
- Signature verification works
- Events are processed correctly
- Duplicate events handled (idempotency)
- Errors are logged
- Timeout handling (return 200 quickly)
Troubleshooting
Webhooks Not Received
- Check URL - Verify webhook URL is correct
- Check HTTPS - Production requires HTTPS
- Check firewall - Allow incoming connections
- Check logs - View delivery status in dashboard
Signature Mismatch
- Raw body - Use raw request body, not parsed JSON
- Correct secret - Verify webhook secret matches
- Encoding - Use UTF-8 encoding
Timeouts
- Async processing - Return 200 immediately
- Queue jobs - Process heavy work in background
- Check latency - Optimize endpoint response time
// Good - Return immediately, process async
app.post('/webhooks/lightning', (req, res) => {
// Acknowledge receipt immediately
res.status(200).send('OK');
// Process asynchronously
setImmediate(() => {
processWebhook(req.body);
});
});
Duplicate Events
Implement idempotency using the invoice ID:
async function handlePaymentCompleted(data) {
const { invoiceId } = data;
// Check if already processed
const order = await db.orders.findOne({
lightningInvoiceId: invoiceId
});
if (order.status === 'fulfilled') {
console.log('Already processed:', invoiceId);
return;
}
// Process and mark as fulfilled
await fulfillOrder(order.id);
await db.orders.update(order.id, { status: 'fulfilled' });
}
Best Practices
- Return 200 quickly - Acknowledge receipt, process later
- Verify signatures - Always validate HMAC
- Handle duplicates - Webhooks may be sent multiple times
- Log everything - Keep records for debugging
- Secure endpoint - Use HTTPS, validate signatures
- Test thoroughly - Use ngrok for local testing
Next Steps
- Testing - Test your integration
- Webhooks API - API reference
- First Payment - Complete a test payment