Skip to main content

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

FieldTypeRequiredDescription
invoiceIdstringYesOriginal payment invoice ID
amountdecimalYesRefund amount
currencystringYesCurrency code (USD, EUR, etc.)
lightningInvoicestringOne ofCustomer's Lightning invoice
bitcoinAddressstringOne ofCustomer's Bitcoin address
reasonstringNoReason 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

FieldTypeDescription
refundIdstringUnique refund ID
invoiceIdstringOriginal payment invoice
statusstringRefund status
amountdecimalRefund amount
currencystringCurrency code
amountSatsintegerAmount in satoshis
typestringlightning or onchain
createdAtdatetimeRefund creation time

Refund Statuses

StatusDescription
pendingRefund created, processing
completedRefund successfully sent
failedRefund 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

TypeMinimumMaximum
Lightning1 satOriginal payment
On-chain546 sats (dust limit)Original payment

Error Handling

Common refund errors:

ErrorCauseSolution
invalid_invoiceInvalid Lightning invoiceVerify invoice format
insufficient_balanceOpenNode balance too lowAdd funds to OpenNode
invoice_not_foundOriginal payment not foundCheck invoice ID
already_refundedPayment already refundedCheck refund history
amount_exceededRefund exceeds paymentReduce 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