Skip to main content

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:

  1. OpenNode → Lightning Enable: OpenNode sends payment notifications
  2. 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

HeaderDescription
Content-Typeapplication/json
X-Webhook-SignatureHMAC-SHA256 signature
X-Webhook-IdUnique webhook delivery ID
X-Webhook-TimestampISO 8601 timestamp

Retry Policy

If your endpoint fails, we retry with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 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

  1. Check URL - Verify webhook URL is correct
  2. Check HTTPS - Production requires HTTPS
  3. Check firewall - Allow incoming connections
  4. Check logs - View delivery status in dashboard

Signature Mismatch

  1. Raw body - Use raw request body, not parsed JSON
  2. Correct secret - Verify webhook secret matches
  3. Encoding - Use UTF-8 encoding

Timeouts

  1. Async processing - Return 200 immediately
  2. Queue jobs - Process heavy work in background
  3. 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

  1. Return 200 quickly - Acknowledge receipt, process later
  2. Verify signatures - Always validate HMAC
  3. Handle duplicates - Webhooks may be sent multiple times
  4. Log everything - Keep records for debugging
  5. Secure endpoint - Use HTTPS, validate signatures
  6. Test thoroughly - Use ngrok for local testing

Next Steps