Refunds
Lightning Enable supports refunding payments back to customers via Lightning Network or on-chain Bitcoin.
Overview
Refunds work differently than traditional payment processors:
- Lightning Refunds - Customer provides a Lightning invoice, you pay it
- On-Chain Refunds - Customer provides a Bitcoin address, you send funds
Refunds are processed by OpenNode and debited from your OpenNode balance.
Creating a Refund
Via Lightning Invoice
The customer provides a Lightning invoice from their wallet:
curl -X POST https://api.lightningenable.com/api/refunds \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"invoiceId": "inv_abc123",
"amount": 25.00,
"currency": "USD",
"lightningInvoice": "lnbc250000n1p...",
"reason": "Customer requested refund"
}'
Via Bitcoin Address
The customer provides their Bitcoin address:
curl -X POST https://api.lightningenable.com/api/refunds \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"invoiceId": "inv_abc123",
"amount": 25.00,
"currency": "USD",
"bitcoinAddress": "bc1q...",
"reason": "Item damaged during shipping"
}'
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
invoiceId | string | Yes | Original payment invoice ID |
amount | decimal | Yes | Refund amount |
currency | string | Yes | Currency code (USD, EUR, etc.) |
lightningInvoice | string | One of | Customer's Lightning invoice |
bitcoinAddress | string | One of | Customer's Bitcoin address |
reason | string | No | Reason for refund |
note
Either lightningInvoice or bitcoinAddress is required, but not both.
Refund Response
{
"refundId": "ref_xyz789",
"invoiceId": "inv_abc123",
"status": "pending",
"amount": 25.00,
"currency": "USD",
"amountSats": 62500,
"type": "lightning",
"createdAt": "2024-12-29T14:00:00Z"
}
Response Fields
| Field | Type | Description |
|---|---|---|
refundId | string | Unique refund ID |
invoiceId | string | Original payment invoice |
status | string | Refund status |
amount | decimal | Refund amount |
currency | string | Currency code |
amountSats | integer | Amount in satoshis |
type | string | lightning or onchain |
createdAt | datetime | Refund creation time |
Refund Statuses
| Status | Description |
|---|---|
pending | Refund created, processing |
completed | Refund successfully sent |
failed | Refund failed (check reason) |
Checking Refund Status
curl -X GET https://api.lightningenable.com/api/refunds/ref_xyz789 \
-H "X-API-Key: your-api-key"
Response:
{
"refundId": "ref_xyz789",
"invoiceId": "inv_abc123",
"status": "completed",
"amount": 25.00,
"currency": "USD",
"amountSats": 62500,
"type": "lightning",
"createdAt": "2024-12-29T14:00:00Z",
"completedAt": "2024-12-29T14:00:02Z"
}
Listing Refunds
All Refunds
curl -X GET https://api.lightningenable.com/api/refunds \
-H "X-API-Key: your-api-key"
Refunds for a Specific Invoice
curl -X GET https://api.lightningenable.com/api/refunds/invoice/inv_abc123 \
-H "X-API-Key: your-api-key"
Implementation Examples
Node.js
async function processRefund(orderId, customerLightningInvoice) {
// Get original payment
const order = await db.orders.findOne({ orderId });
if (!order || !order.invoiceId) {
throw new Error('Order not found');
}
// Create refund
const response = await fetch('https://api.lightningenable.com/api/refunds', {
method: 'POST',
headers: {
'X-API-Key': process.env.LIGHTNING_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
invoiceId: order.invoiceId,
amount: order.amount,
currency: order.currency,
lightningInvoice: customerLightningInvoice,
reason: 'Customer refund request'
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Refund failed: ${error.message}`);
}
const refund = await response.json();
// Update order status
await db.orders.updateOne(
{ orderId },
{ $set: { refundId: refund.refundId, status: 'refunded' } }
);
return refund;
}
C# / .NET
public class RefundService
{
private readonly HttpClient _client;
public RefundService(IConfiguration config)
{
_client = new HttpClient
{
BaseAddress = new Uri("https://api.lightningenable.com")
};
_client.DefaultRequestHeaders.Add("X-API-Key", config["LightningApiKey"]);
}
public async Task<RefundResponse> CreateRefundAsync(RefundRequest request)
{
var response = await _client.PostAsJsonAsync("/api/refunds", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<RefundResponse>();
}
}
public record RefundRequest(
string InvoiceId,
decimal Amount,
string Currency,
string? LightningInvoice = null,
string? BitcoinAddress = null,
string? Reason = null
);
public record RefundResponse(
string RefundId,
string InvoiceId,
string Status,
decimal Amount,
string Type
);
Partial Refunds
You can refund a portion of the original payment:
{
"invoiceId": "inv_abc123",
"amount": 10.00,
"currency": "USD",
"lightningInvoice": "lnbc100000n1p...",
"reason": "Partial refund for returned item"
}
note
The refund amount cannot exceed the original payment amount.
Refund Limits
| Type | Minimum | Maximum |
|---|---|---|
| Lightning | 1 sat | Original payment |
| On-chain | 546 sats (dust limit) | Original payment |
Error Handling
Common refund errors:
| Error | Cause | Solution |
|---|---|---|
invalid_invoice | Invalid Lightning invoice | Verify invoice format |
insufficient_balance | OpenNode balance too low | Add funds to OpenNode |
invoice_not_found | Original payment not found | Check invoice ID |
already_refunded | Payment already refunded | Check refund history |
amount_exceeded | Refund exceeds payment | Reduce refund amount |
Best Practices
Validate Customer Input
function validateRefundRequest(request) {
if (request.lightningInvoice) {
// Lightning invoices start with 'lnbc' (mainnet) or 'lntb' (testnet)
if (!request.lightningInvoice.match(/^ln(bc|tb)/)) {
throw new Error('Invalid Lightning invoice format');
}
}
if (request.bitcoinAddress) {
// Basic Bitcoin address validation
if (!request.bitcoinAddress.match(/^(bc1|[13])[a-zA-Z0-9]{25,39}$/)) {
throw new Error('Invalid Bitcoin address format');
}
}
}
Store Refund History
async function trackRefund(refund) {
await db.refunds.insertOne({
refundId: refund.refundId,
invoiceId: refund.invoiceId,
orderId: await getOrderIdFromInvoice(refund.invoiceId),
amount: refund.amount,
status: refund.status,
createdAt: new Date(),
reason: refund.reason
});
}
Handle Lightning Invoice Expiration
Lightning invoices expire quickly (often 1 hour). Process refunds promptly:
async function processLightningRefund(invoiceId, lightningInvoice) {
// Get invoice details to check expiry
const invoiceDetails = parseLightningInvoice(lightningInvoice);
if (invoiceDetails.expiresAt < Date.now()) {
throw new Error('Lightning invoice has expired. Please request a new one.');
}
// Process refund immediately
return await createRefund(invoiceId, lightningInvoice);
}
Next Steps
- API Reference - Complete refund API documentation
- Webhooks - Receive refund notifications
- Error Handling - Error codes and handling