Refunds API
Create and track refunds for completed payments. Refunds are sent on-chain (Bitcoin address) or over Lightning (BOLT11 invoice) — you choose per refund with the refundType field.
Refunds are included on both Agentic Commerce plans (Individual and Business). If your plan doesn't include refunds you'll get a 403 with an explanatory message.
How Refunds Work
Lightning payments are instant and irreversible, so a refund is a new outgoing payment from your payment provider account back to the customer:
Original Payment: Customer → Payment Provider → Your Account
Refund: Your Account → Payment Provider → Customer
Strike has no native refund API — Lightning Enable implements Strike refunds as outgoing payments using Strike's payment-quotes flow. OpenNode processes refunds as withdrawals from your OpenNode balance. The API contract below is identical either way.
Requirements:
- The original payment must be in
paid,underpaid, orprocessingstatus - For Lightning refunds: the customer provides a BOLT11 invoice for the refund amount
- Sufficient balance in your payment provider account
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/refunds | Create a refund |
| GET | /api/refunds/{refundId} | Get refund status |
| GET | /api/refunds | List refunds |
| GET | /api/refunds/invoice/{invoiceId} | Refunds for one invoice |
| POST | /api/refunds/{refundId}/sync | Force-sync status from the provider |
All endpoints require your merchant API key in the X-API-Key header.
Create Refund
POST /api/refunds
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
invoiceId | string | Yes | The Lightning Enable invoice ID to refund |
refundAddress | string | Yes | Where to send the refund: a Bitcoin address (on-chain) or a BOLT11 Lightning invoice (26–500 chars) |
refundType | string | No | "chain" (on-chain Bitcoin, default) or "ln" (Lightning). Must match the kind of refundAddress you provided |
amountSatoshis | integer | Conditional | Amount to refund in satoshis. Required for all Lightning ("ln") refunds and for every refund on Strike (any type). Optional only for OpenNode on-chain refunds, where omitting it means a full refund |
email | string | No | Email for refund status notifications (max 200 chars) |
reason | string | No | Reason for the refund (max 500 chars) |
For refundType: "ln", the customer generates a Lightning invoice for the refund amount in their wallet and gives it to you — that BOLT11 string is the refundAddress. For on-chain refunds any Bitcoin address works.
Example — on-chain 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": 62500,
"reason": "Customer request"
}'
amountSatoshis?Only OpenNode merchants making on-chain refunds may omit it (that means "refund the full amount"). Strike merchants — the platform default — must pass amountSatoshis on every refund (Strike refunds are outgoing payments via payment-quotes, which need an explicit amount), and Lightning refunds require it on every provider. Missing it in those cases returns 400 with "AmountSatoshis is required for …".
Example — partial Lightning 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": "lnbc50u1p3xnhl2pp5...",
"refundType": "ln",
"amountSatoshis": 5000,
"reason": "Partial return"
}'
Response — 201 Created
{
"refundId": "17",
"providerRefundId": "wd_9f8e7d6c",
"openNodeRefundId": "wd_9f8e7d6c",
"invoiceId": "1042",
"status": "pending",
"amountSatoshis": 62500,
"feeSatoshis": 154,
"refundAddress": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"refundType": "chain",
"createdAt": "2026-07-03T14:00:00Z",
"transactionHash": null
}
| Field | Description |
|---|---|
refundId | Lightning Enable refund ID — use for status lookups |
providerRefundId | The provider's refund/withdrawal ID (openNodeRefundId carries the same value for backward compatibility) |
status | pending, processing, confirmed, or failed |
amountSatoshis / feeSatoshis | Refund amount and network fee, in satoshis |
refundType | chain or ln |
transactionHash | Bitcoin transaction hash — on-chain refunds only, once broadcast |
Errors
Errors return { "error": "<message>" }:
| Status | When |
|---|---|
| 400 | Any create failure: validation (missing refundAddress, missing amountSatoshis where required, bad refundType), invoice not found for your merchant, invoice not refundable, amount exceeds payment, ... — the error message says what to fix. Create Refund never returns 404 — not-found surfaces as 400 |
| 401 | Missing/invalid API key |
(404 is returned only by the GET, list-by-invoice, and sync endpoints below when the refund/invoice doesn't exist.)
Create Refund supports idempotency — send an X-Idempotency-Key header to make retries safe, the same as payment creation.
Get Refund Status
GET /api/refunds/{refundId}
Returns a refund object or 404 { "error": "..." }:
{
"refundId": "17",
"providerRefundId": "wd_9f8e7d6c",
"openNodeRefundId": "wd_9f8e7d6c",
"invoiceId": "1042",
"status": "confirmed",
"amountSatoshis": 62500,
"feeSatoshis": 154,
"refundAddress": "bc1qxy2...",
"refundType": "chain",
"transactionHash": "3a1b2c...",
"createdAt": "2026-07-03T14:00:00Z",
"processedAt": "2026-07-03T14:22:10Z",
"reason": "Customer request"
}
Refund Statuses
| Status | Description |
|---|---|
pending | Refund created, not yet processed by the provider |
processing | Provider is processing the payout |
confirmed | Refund settled (on-chain: broadcast; Lightning: paid) |
failed | Refund failed — retry with a fresh request or contact support |
List Refunds
GET /api/refunds?status=confirmed&skip=0&take=50
| Parameter | Default | Description |
|---|---|---|
status | — | Optional filter: pending, processing, confirmed, failed |
skip | 0 | Records to skip (pagination) |
take | 50 | Records to return (max 100) |
The response is a bare JSON array of refund objects — there is no wrapper object and no total count. Page by increasing skip until you receive fewer than take results:
[
{ "refundId": "17", "invoiceId": "1042", "status": "confirmed", "...": "..." },
{ "refundId": "16", "invoiceId": "1038", "status": "failed", "...": "..." }
]
Refunds for an Invoice
GET /api/refunds/invoice/{invoiceId}
Returns a bare array of all refunds for that invoice (a payment can have multiple partial refunds), or 404 if the invoice doesn't exist.
Sync Refund Status
POST /api/refunds/{refundId}/sync
Forces an immediate status refresh from the payment provider and returns the updated refund object. Refund status changes are not delivered via merchant webhooks — poll this endpoint (or GET /api/refunds/{refundId}) to track progress.
Code Examples
Node.js
async function createRefund(invoiceId, refundAddress, opts = {}) {
const response = await fetch('https://api.lightningenable.com/api/refunds', {
method: 'POST',
headers: {
'X-API-Key': process.env.LIGHTNING_ENABLE_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
invoiceId,
refundAddress,
refundType: opts.refundType ?? 'chain',
amountSatoshis: opts.amountSatoshis, // omit for full refund
reason: opts.reason
})
});
const body = await response.json();
if (!response.ok) {
throw new Error(`Refund failed (${response.status}): ${body.error}`);
}
return body; // { refundId, status: 'pending', ... }
}
C#
var request = new
{
invoiceId = "1042",
refundAddress = "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
refundType = "chain",
reason = "Customer request"
};
var response = await httpClient.PostAsJsonAsync(
"https://api.lightningenable.com/api/refunds", request);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadFromJsonAsync<JsonElement>();
throw new InvalidOperationException(
$"Refund failed ({(int)response.StatusCode}): {error.GetProperty("error").GetString()}");
}
var refund = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"Refund {refund.GetProperty("refundId")} is {refund.GetProperty("status")}");
Best Practices
- Default to on-chain unless the customer supplies a Lightning invoice —
refundTypedefaults to"chain". Always passamountSatoshisunless you're an OpenNode merchant doing an on-chain full refund (the only case where it's optional). - Track by polling. Poll
GET /api/refunds/{refundId}(or hit/sync) until the status isconfirmedorfailed— refunds do not emit merchant webhooks. - Amounts are satoshis.
amountSatoshisis the only amount field; there is no fiat amount/currency on the refund API. - Send
X-Idempotency-Keyon create so a network retry can't double-refund.
Next Steps
- Payments API — create and track payments
- Webhooks — payment notifications
- Errors — error handling