Skip to main content

Error Handling

Learn how to handle errors from the Lightning Enable API.

Error Response Format

All errors follow a consistent format:

{
"error": "Error Type",
"message": "Human-readable description",
"code": "MACHINE_READABLE_CODE",
"details": { }
}
FieldTypeDescription
errorstringHTTP status text
messagestringHuman-readable error message
codestringMachine-readable error code
detailsobjectAdditional error context (optional)

HTTP Status Codes

Success Codes

CodeMeaning
200 OKRequest succeeded
201 CreatedResource created
204 No ContentRequest succeeded, no response body

Client Error Codes

CodeMeaning
400 Bad RequestInvalid request parameters
401 UnauthorizedMissing or invalid API key
402 Payment RequiredL402 payment needed
403 ForbiddenAccess denied (inactive, wrong plan, etc.)
404 Not FoundResource doesn't exist
409 ConflictResource conflict (duplicate, etc.)
422 Unprocessable EntityValid syntax but semantic error
429 Too Many RequestsRate limit exceeded

Server Error Codes

CodeMeaning
500 Internal Server ErrorServer error
502 Bad GatewayUpstream service error
503 Service UnavailableTemporary maintenance
504 Gateway TimeoutUpstream timeout

Error Codes Reference

Authentication Errors

MISSING_API_KEY

{
"error": "Unauthorized",
"message": "API key is required. Include X-API-Key header.",
"code": "MISSING_API_KEY"
}

Solution: Add X-API-Key header to your request.

INVALID_API_KEY

{
"error": "Unauthorized",
"message": "Invalid API key",
"code": "INVALID_API_KEY"
}

Solution: Verify your API key is correct and active.

MERCHANT_INACTIVE

{
"error": "Forbidden",
"message": "Merchant account is inactive",
"code": "MERCHANT_INACTIVE"
}

Solution: Check your subscription status or contact support.

Subscription Errors

SUBSCRIPTION_REQUIRED

{
"error": "Forbidden",
"message": "Active subscription required",
"code": "SUBSCRIPTION_REQUIRED"
}

Solution: Subscribe to a plan at lightningenable.com.

SUBSCRIPTION_EXPIRED

{
"error": "Forbidden",
"message": "Subscription has expired",
"code": "SUBSCRIPTION_EXPIRED"
}

Solution: Renew your subscription.

FEATURE_NOT_AVAILABLE

{
"error": "Forbidden",
"message": "Refunds not available on your plan",
"code": "FEATURE_NOT_AVAILABLE",
"details": {
"feature": "refunds",
"currentPlan": "pilot",
"requiredPlan": "production"
}
}

Solution: Upgrade to a plan with this feature.

Payment Errors

INVALID_AMOUNT

{
"error": "Bad Request",
"message": "Amount must be greater than 0",
"code": "INVALID_AMOUNT"
}

Solution: Provide a positive payment amount.

INVALID_CURRENCY

{
"error": "Bad Request",
"message": "Unsupported currency: XYZ",
"code": "INVALID_CURRENCY",
"details": {
"supported": ["USD", "EUR", "GBP", "BTC", "sats"]
}
}

Solution: Use a supported currency code.

MISSING_ORDER_ID

{
"error": "Bad Request",
"message": "Order ID is required",
"code": "MISSING_ORDER_ID"
}

Solution: Include orderId in your request.

DUPLICATE_ORDER_ID

{
"error": "Conflict",
"message": "Order ID already exists",
"code": "DUPLICATE_ORDER_ID",
"details": {
"orderId": "ORDER-123",
"existingInvoiceId": "inv_abc123"
}
}

Solution: Use unique order IDs for each payment.

PAYMENT_NOT_FOUND

{
"error": "Not Found",
"message": "Payment not found",
"code": "PAYMENT_NOT_FOUND"
}

Solution: Verify the invoice ID is correct.

Refund Errors

INVOICE_NOT_PAID

{
"error": "Bad Request",
"message": "Cannot refund unpaid invoice",
"code": "INVOICE_NOT_PAID"
}

Solution: Only refund invoices with paid status.

REFUND_EXCEEDS_PAYMENT

{
"error": "Bad Request",
"message": "Refund amount exceeds original payment",
"code": "REFUND_EXCEEDS_PAYMENT",
"details": {
"originalAmount": 49.99,
"requestedRefund": 75.00,
"previousRefunds": 0
}
}

Solution: Refund amount must not exceed payment amount.

INVALID_LIGHTNING_INVOICE

{
"error": "Bad Request",
"message": "Invalid Lightning invoice",
"code": "INVALID_LIGHTNING_INVOICE"
}

Solution: Provide a valid BOLT11 Lightning invoice.

INSUFFICIENT_BALANCE

{
"error": "Payment Failed",
"message": "Insufficient balance in OpenNode account",
"code": "INSUFFICIENT_BALANCE"
}

Solution: Add funds to your OpenNode account.

L402 Errors

L402_PAYMENT_REQUIRED

{
"error": "Payment Required",
"message": "Pay the Lightning invoice to access this resource",
"code": "L402_PAYMENT_REQUIRED",
"l402": {
"macaroon": "...",
"invoice": "...",
"amount_sats": 10
}
}

Solution: Pay the provided Lightning invoice.

INVALID_L402_CREDENTIAL

{
"error": "Unauthorized",
"message": "Invalid L402 credential",
"code": "INVALID_L402_CREDENTIAL",
"details": "Preimage does not match payment hash"
}

Solution: Verify macaroon and preimage are correct.

L402_TOKEN_EXPIRED

{
"error": "Forbidden",
"message": "L402 token has expired",
"code": "L402_TOKEN_EXPIRED"
}

Solution: Get a new token by paying again.

Rate Limiting Errors

RATE_LIMIT_EXCEEDED

{
"error": "Too Many Requests",
"message": "Rate limit exceeded",
"code": "RATE_LIMIT_EXCEEDED",
"details": {
"limit": 100,
"window": "1 minute",
"retryAfter": 45
}
}

Solution: Wait before retrying. See Retry-After header.

OpenNode Errors

OPENNODE_ERROR

{
"error": "Bad Gateway",
"message": "OpenNode API error",
"code": "OPENNODE_ERROR",
"details": {
"openNodeError": "Invalid API key"
}
}

Solution: Check your OpenNode API key configuration.

OPENNODE_TIMEOUT

{
"error": "Gateway Timeout",
"message": "OpenNode request timed out",
"code": "OPENNODE_TIMEOUT"
}

Solution: Retry the request.

Handling Errors

JavaScript

async function createPayment(orderId, amount, currency) {
try {
const response = await fetch('https://api.lightningenable.com/api/payments', {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ orderId, amount, currency })
});

if (!response.ok) {
const error = await response.json();
throw new LightningError(error);
}

return response.json();
} catch (error) {
handleError(error);
}
}

class LightningError extends Error {
constructor(data) {
super(data.message);
this.code = data.code;
this.details = data.details;
}
}

function handleError(error) {
switch (error.code) {
case 'INVALID_API_KEY':
console.error('Check your API key configuration');
break;

case 'RATE_LIMIT_EXCEEDED':
const retryAfter = error.details?.retryAfter || 60;
console.log(`Rate limited. Retry after ${retryAfter}s`);
break;

case 'OPENNODE_ERROR':
console.error('OpenNode issue:', error.details?.openNodeError);
break;

default:
console.error('Error:', error.message);
}
}

C#

public class LightningEnableClient
{
public async Task<PaymentResponse> CreatePaymentAsync(PaymentRequest request)
{
var response = await _httpClient.PostAsJsonAsync("/api/payments", request);

if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
throw new LightningException(error);
}

return await response.Content.ReadFromJsonAsync<PaymentResponse>();
}
}

public class LightningException : Exception
{
public string Code { get; }
public Dictionary<string, object>? Details { get; }

public LightningException(ErrorResponse error)
: base(error.Message)
{
Code = error.Code;
Details = error.Details;
}
}

// Usage
try
{
var payment = await client.CreatePaymentAsync(request);
}
catch (LightningException ex) when (ex.Code == "RATE_LIMIT_EXCEEDED")
{
var retryAfter = ex.Details?["retryAfter"] as int? ?? 60;
await Task.Delay(TimeSpan.FromSeconds(retryAfter));
// Retry
}
catch (LightningException ex)
{
_logger.LogError("Lightning API error: {Code} - {Message}", ex.Code, ex.Message);
}

Python

class LightningError(Exception):
def __init__(self, response):
self.code = response.get('code')
self.message = response.get('message')
self.details = response.get('details', {})
super().__init__(self.message)

def create_payment(order_id, amount, currency):
response = requests.post(
'https://api.lightningenable.com/api/payments',
headers={'X-API-Key': API_KEY, 'Content-Type': 'application/json'},
json={'orderId': order_id, 'amount': amount, 'currency': currency}
)

if not response.ok:
raise LightningError(response.json())

return response.json()

# Usage
try:
payment = create_payment('ORDER-123', 49.99, 'USD')
except LightningError as e:
if e.code == 'RATE_LIMIT_EXCEEDED':
retry_after = e.details.get('retryAfter', 60)
time.sleep(retry_after)
# Retry
elif e.code == 'INVALID_API_KEY':
print('Check your API key')
else:
print(f'Error: {e.message}')

Retry Strategy

Retryable Errors

ErrorRetry?Strategy
429 Rate LimitedYesWait for Retry-After
500 Server ErrorYesExponential backoff
502 Bad GatewayYesExponential backoff
503 Service UnavailableYesWait, then retry
504 Gateway TimeoutYesRetry immediately
400 Bad RequestNoFix request
401 UnauthorizedNoFix credentials
404 Not FoundNoCheck resource ID

Exponential Backoff

async function withRetry(fn, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (!isRetryable(error) || attempt === maxRetries) {
throw error;
}

const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

function isRetryable(error) {
const retryableCodes = [
'RATE_LIMIT_EXCEEDED',
'OPENNODE_TIMEOUT',
'OPENNODE_ERROR'
];
return retryableCodes.includes(error.code) ||
error.status >= 500;
}

Best Practices

Always Check Status Codes

// Bad - assumes success
const data = await response.json();

// Good - check status first
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
const data = await response.json();

Log Errors with Context

function handleError(error, context) {
console.error({
code: error.code,
message: error.message,
details: error.details,
context: {
orderId: context.orderId,
timestamp: new Date().toISOString()
}
});
}

User-Friendly Messages

function getUserMessage(error) {
const messages = {
'INVALID_API_KEY': 'Authentication failed. Please try again.',
'PAYMENT_NOT_FOUND': 'Payment not found. It may have expired.',
'RATE_LIMIT_EXCEEDED': 'Too many requests. Please wait a moment.',
'OPENNODE_ERROR': 'Payment service temporarily unavailable.'
};

return messages[error.code] || 'An error occurred. Please try again.';
}

Next Steps