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 payments are irreversible, so a refund is a new outgoing payment:

  • On-chain refunds (default) — the customer provides a Bitcoin address, funds are sent on-chain
  • Lightning refunds — the customer provides a BOLT11 Lightning invoice for the refund amount, and it gets paid

Refunds are processed by your payment provider and debited from your provider balance. For Strike merchants, refunds are implemented as outgoing payments via payment-quotes (Strike has no native refund API). For OpenNode merchants, refunds are processed as withdrawals through OpenNode.

Only payments in paid, underpaid, or processing status can be refunded.

Creating a Refund

The request always takes invoiceId + refundAddress. The refundType field tells Lightning Enable how to interpret the address: "chain" for a Bitcoin address (the default) or "ln" for a Lightning invoice. Amounts are specified in satoshis via amountSatoshisrequired for all Lightning refunds and for every refund on Strike (the platform default). It's optional only for OpenNode on-chain refunds, where omitting it means a full refund.

On-chain (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": "1042",
"refundAddress": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"amountSatoshis": 62500,
"reason": "Customer requested refund"
}'

Lightning (BOLT11 invoice)

Ask the customer to generate an invoice for the refund amount in their wallet, then:

curl -X POST https://api.lightningenable.com/api/refunds \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"invoiceId": "1042",
"refundAddress": "lnbc250000n1p...",
"refundType": "ln",
"amountSatoshis": 25000,
"reason": "Customer requested refund"
}'

amountSatoshis is always required for Lightning refunds — it must match the amount the customer's invoice was generated for.

Partial refund

curl -X POST https://api.lightningenable.com/api/refunds \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"invoiceId": "1042",
"refundAddress": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"amountSatoshis": 5000,
"reason": "Partial return"
}'

Partial refunds may not be supported by every provider/payment type — if unsupported, the API returns 400 with an explanatory error message.

Response

{
"refundId": "17",
"providerRefundId": "wd_9f8e7d6c",
"openNodeRefundId": "wd_9f8e7d6c",
"invoiceId": "1042",
"status": "pending",
"amountSatoshis": 62500,
"feeSatoshis": 154,
"refundAddress": "bc1qxy2...",
"refundType": "chain",
"createdAt": "2026-07-03T14:00:00Z",
"transactionHash": null
}

See the Refunds API reference for the full field-by-field schema, list endpoints, and error contract.

Tracking a Refund

Refund status changes are not delivered via merchant webhooks. Poll for status:

# Current status (pending → processing → confirmed | failed)
curl https://api.lightningenable.com/api/refunds/17 \
-H "X-API-Key: YOUR_API_KEY"

# Force an immediate re-check against your payment provider
curl -X POST https://api.lightningenable.com/api/refunds/17/sync \
-H "X-API-Key: YOUR_API_KEY"
StatusMeaning
pendingCreated, not yet processed by the provider
processingProvider is processing the payout
confirmedSettled — on-chain refunds also carry a transactionHash
failedFailed — retry with a fresh request or contact support

You can list all refunds with GET /api/refunds (optional status, skip, take query parameters — returns a bare JSON array), or fetch every refund for one payment with GET /api/refunds/invoice/{invoiceId}.

Collecting Refund Details from Customers

A minimal refund-request form needs just two inputs:

<form id="refund-form">
<label>
Where should we send your refund?
<input name="refundAddress"
placeholder="Bitcoin address or Lightning invoice" required />
</label>
<label>
Reason (optional)
<input name="reason" maxlength="500" />
</label>
<button type="submit">Request refund</button>
</form>

Detect the type server-side: BOLT11 invoices start with lnbc (mainnet) — anything else, treat as an on-chain address and let validation catch mistakes:

app.post('/request-refund', async (req, res) => {
const { refundAddress, reason, orderId } = req.body;
const order = await db.orders.findOne({ orderId });

const response = await fetch('https://api.lightningenable.com/api/refunds', {
method: 'POST',
headers: {
'X-API-Key': process.env.LIGHTNING_ENABLE_API_KEY, // server-side only
'Content-Type': 'application/json'
},
body: JSON.stringify({
invoiceId: order.lightningInvoiceId,
refundAddress,
refundType: refundAddress.toLowerCase().startsWith('lnbc') ? 'ln' : 'chain',
// Required for Lightning refunds and for all refunds on Strike —
// derive from your order record (sats), never from client input
amountSatoshis: order.amountSats,
reason
})
});

const refund = await response.json();
if (!response.ok) {
return res.status(400).json({ error: refund.error });
}

await db.orders.updateOne({ orderId }, { $set: { refundId: refund.refundId } });
res.json({ refundId: refund.refundId, status: refund.status });
});
Lightning invoices expire

BOLT11 invoices typically expire within minutes to hours. Process Lightning refunds promptly after the customer submits their invoice, and ask for a fresh one if the refund fails with an expiry error.

Best Practices

  1. Prefer on-chain for slow workflows — Bitcoin addresses don't expire; Lightning invoices do.
  2. Verify the payment first — the API rejects refunds for invoices that aren't paid/underpaid/processing, with the reason in error.
  3. Poll until terminal statusconfirmed or failed; don't assume success at creation (pending).
  4. Use X-Idempotency-Key on the create call so retries can't double-refund.
  5. Keep your provider balance funded — refunds are outgoing payments and fail if the balance can't cover amount + fee.

Next Steps