Skip to main content

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.

Plan requirement

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 vs OpenNode

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, or processing status
  • For Lightning refunds: the customer provides a BOLT11 invoice for the refund amount
  • Sufficient balance in your payment provider account

Endpoints

MethodEndpointDescription
POST/api/refundsCreate a refund
GET/api/refunds/{refundId}Get refund status
GET/api/refundsList refunds
GET/api/refunds/invoice/{invoiceId}Refunds for one invoice
POST/api/refunds/{refundId}/syncForce-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

FieldTypeRequiredDescription
invoiceIdstringYesThe Lightning Enable invoice ID to refund
refundAddressstringYesWhere to send the refund: a Bitcoin address (on-chain) or a BOLT11 Lightning invoice (26–500 chars)
refundTypestringNo"chain" (on-chain Bitcoin, default) or "ln" (Lightning). Must match the kind of refundAddress you provided
amountSatoshisintegerConditionalAmount 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
emailstringNoEmail for refund status notifications (max 200 chars)
reasonstringNoReason for the refund (max 500 chars)
Lightning refunds need an invoice from the customer

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"
}'
When can you omit 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
}
FieldDescription
refundIdLightning Enable refund ID — use for status lookups
providerRefundIdThe provider's refund/withdrawal ID (openNodeRefundId carries the same value for backward compatibility)
statuspending, processing, confirmed, or failed
amountSatoshis / feeSatoshisRefund amount and network fee, in satoshis
refundTypechain or ln
transactionHashBitcoin transaction hash — on-chain refunds only, once broadcast

Errors

Errors return { "error": "<message>" }:

StatusWhen
400Any 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
401Missing/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

StatusDescription
pendingRefund created, not yet processed by the provider
processingProvider is processing the payout
confirmedRefund settled (on-chain: broadcast; Lightning: paid)
failedRefund failed — retry with a fresh request or contact support

List Refunds

GET /api/refunds?status=confirmed&skip=0&take=50
ParameterDefaultDescription
statusOptional filter: pending, processing, confirmed, failed
skip0Records to skip (pagination)
take50Records 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

  1. Default to on-chain unless the customer supplies a Lightning invoice — refundType defaults to "chain". Always pass amountSatoshis unless you're an OpenNode merchant doing an on-chain full refund (the only case where it's optional).
  2. Track by polling. Poll GET /api/refunds/{refundId} (or hit /sync) until the status is confirmed or failed — refunds do not emit merchant webhooks.
  3. Amounts are satoshis. amountSatoshis is the only amount field; there is no fiat amount/currency on the refund API.
  4. Send X-Idempotency-Key on create so a network retry can't double-refund.

Next Steps