Skip to main content

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?

MethodProsCons
PollingSimple to implementDelayed detection, API overhead
WebhooksInstant, efficientRequires 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

FieldTypeDescription
invoiceIdstringLightning Enable invoice ID
orderIdstringYour order ID
openNodeChargeIdstringOpenNode charge ID
statusstringPayment status
amountdecimalPayment amount
currencystringCurrency code
amountSatsintegerAmount in satoshis
paidAtdatetimePayment timestamp (if paid)
metadataobjectYour custom metadata

Payment Statuses

StatusDescriptionAction
paidPayment confirmedFulfill order
processingPayment detectedWait for confirmation
expiredInvoice expiredNotify customer
underpaidInsufficient amountHandle manually
refundedPayment refundedUpdate 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:

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry15 minutes
4th retry1 hour
Final24 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

  1. Verify webhook URL is publicly accessible
  2. Check for HTTPS requirement
  3. Ensure firewall allows incoming connections
  4. Review webhook logs in Lightning Enable dashboard

Signature Verification Fails

  1. Ensure you're using the correct webhook secret
  2. Verify JSON serialization matches
  3. Check for encoding issues (UTF-8)

Duplicate Webhooks

  1. Implement idempotent processing
  2. Store invoice IDs that have been processed
  3. Check before fulfilling orders

Next Steps