Skip to main content

Rate Limiting

Lightning Enable applies rate limits to ensure fair usage and system stability.

Rate Limits

Default Limits

PolicyLimitWindowApplied To
Global100 requests1 minuteAll requests (per API key when authenticated, per IP when anonymous)
Read200 requests1 minuteGET operations on payments, refunds, merchant settings
Payment Create10 requests1 minutePOST /api/payments, POST /api/refunds, /api/checkout/*
Write20 requests1 minuteMerchant self-service writes (e.g., POST /api/merchant/regenerate-key)
Checkout Create5 requests1 minuteStripe checkout session creation
Admin30 requests1 minuteInternal admin endpoints
Webhook100 requests1 minute (sliding window)/api/webhooks/*
Magic Link3 requests15 minutesMagic link email requests (per IP)
note

Rate limits are designed to prevent abuse while allowing normal operations. The global limiter applies per API key for authenticated requests, or per IP address for anonymous requests.

Webhook Rate Limiting

Webhook endpoints use a sliding window rate limiter instead of a fixed window. This provides smoother rate limiting by avoiding burst-at-boundary issues that can occur with fixed windows.

The sliding window is divided into 4 segments per minute, so the limit is evaluated more granularly than a simple 100-per-minute counter. This prevents scenarios where a burst of 100 requests at the end of one window and 100 at the start of the next would be allowed.

Fixed window:    |--- 100 allowed ---|--- 100 allowed ---|
^ boundary allows burst of 200

Sliding window: Smoothly tracks usage across 4 segments
No burst-at-boundary problem

Webhook rate limiting applies to:

  • POST /api/webhooks/opennode -- OpenNode payment webhooks
  • POST /api/webhooks/strike -- Strike payment webhooks

Rate Limit Exceeded

When you exceed a rate limit, the API returns 429 Too Many Requests with the wait time in the JSON body:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json

{
"error": "Too many requests",
"message": "Rate limit exceeded. Please try again later.",
"retryAfter": 60
}
Body fieldTypeDescription
errorstringAlways "Too many requests" for the general rate limiter
messagestringHuman-readable description
retryAfternumberSeconds to wait before retrying
No rate-limit headers

The API does not emit X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, or Retry-After headers on rate-limited (or successful) responses. Do not write client code that parses these headers — the only rate-limit signal is the 429 status plus the retryAfter field in the JSON body. (The one exception: the failed-authentication throttle below sets a Retry-After header on its 429s.)

Failed-Authentication Throttling

Separately from the request rate limiter, Lightning Enable throttles failed authentication attempts per IP address: more than 20 failures within a 60-second fixed window blocks further authenticated requests from that IP until the window expires.

HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json

{
"error": "Too many failed authentication attempts",
"message": "Slow down and try again in 42 seconds"
}

Key differences from the general rate limiter:

General rate limiterAuth-failure throttle
TriggerToo many requestsToo many failed authentications (missing/invalid API key)
ScopePer API key (or per IP anonymous)Per IP, regardless of which keys were tried
error string"Too many requests""Too many failed authentication attempts"
Wait signalretryAfter in JSON bodyRetry-After response header (and the message text)
Correct responseBack off and retryFix your API key — do not retry

If you hit this throttle, your integration is sending a wrong or stale API key. Retrying with the same key keeps recording failures and re-arms the block. Verify your key at Dashboard → Settings and update your configuration before retrying. See Authentication.

Handling Rate Limits

Handle 429 Errors

Read the wait time from the JSON body's retryAfter field:

async function requestWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);

if (response.status === 429) {
const body = await response.json();

if (body.error === 'Too many failed authentication attempts') {
// Auth throttle — retrying won't help until the key is fixed
throw new Error('Authentication throttled. Check your API key.');
}

const retryAfter = body.retryAfter || 60;
console.log(`Rate limited. Waiting ${retryAfter} seconds...`);
await sleep(retryAfter * 1000);
continue;
}

return response;
}

throw new Error('Max retries exceeded');
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

C# Implementation

public class RateLimitedHttpClient
{
private readonly HttpClient _client;

public RateLimitedHttpClient(HttpClient client) => _client = client;

public async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, int maxRetries = 3)
{
for (var attempt = 0; attempt < maxRetries; attempt++)
{
var response = await _client.SendAsync(request);

if (response.StatusCode != HttpStatusCode.TooManyRequests)
{
return response;
}

// The wait time is in the JSON body, not a header
var body = await response.Content
.ReadFromJsonAsync<RateLimitResponse>();

if (body?.Error == "Too many failed authentication attempts")
{
throw new InvalidOperationException(
"Authentication throttled. Check your API key.");
}

var delay = TimeSpan.FromSeconds(body?.RetryAfter ?? 60);
await Task.Delay(delay);
}

throw new InvalidOperationException("Max retries exceeded");
}

private sealed record RateLimitResponse(
string? Error, string? Message, double? RetryAfter);
}

Python Implementation

import time
import requests

class RateLimitedClient:
def __init__(self, api_key):
self.api_key = api_key

def request(self, method, url, max_retries=3, **kwargs):
headers = kwargs.pop('headers', {})
headers['X-API-Key'] = self.api_key

for _ in range(max_retries):
response = requests.request(method, url, headers=headers, **kwargs)

if response.status_code != 429:
return response

# The wait time is in the JSON body, not a header
body = response.json()

if body.get('error') == 'Too many failed authentication attempts':
raise RuntimeError('Authentication throttled. Check your API key.')

retry_after = body.get('retryAfter', 60)
time.sleep(retry_after)

raise RuntimeError('Max retries exceeded')

Best Practices

1. Implement Exponential Backoff

async function exponentialBackoff(fn, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.status !== 429 || i === maxRetries - 1) {
throw error;
}

// Prefer the server-provided retryAfter; fall back to backoff
const base = error.retryAfter
? error.retryAfter * 1000
: Math.min(1000 * Math.pow(2, i), 60000);
const jitter = Math.random() * 1000;
await sleep(base + jitter);
}
}
}

2. Use Request Queuing

class RequestQueue {
constructor(maxConcurrent = 10) {
this.queue = [];
this.running = 0;
this.maxConcurrent = maxConcurrent;
}

async add(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.process();
});
}

async process() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
return;
}

this.running++;
const { fn, resolve, reject } = this.queue.shift();

try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process();
}
}
}

// Usage
const queue = new RequestQueue(5);

const results = await Promise.all(
paymentIds.map(id =>
queue.add(() => getPayment(id))
)
);

3. Cache Responses

const cache = new Map();
const CACHE_TTL = 60000; // 1 minute

async function getCachedRate(currency) {
const cacheKey = `rate:${currency}`;
const cached = cache.get(cacheKey);

if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}

const data = await fetchRate(currency);
cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
}

4. Batch Requests

Instead of individual requests:

// Bad - 100 API calls
for (const orderId of orderIds) {
const payment = await getPayment(orderId);
}

// Good - Use webhooks or batch endpoints
// Payments are pushed via webhook, no polling needed

5. Use Webhooks

Don't poll for payment status. Use webhooks instead:

// Bad - Polling every 5 seconds
setInterval(async () => {
const status = await getPaymentStatus(invoiceId);
if (status === 'paid') {
fulfillOrder();
}
}, 5000);

// Good - Webhook notification
app.post('/webhooks/lightning', (req, res) => {
if (req.body.event === 'payment.completed') {
fulfillOrder(req.body.data.orderId);
}
res.status(200).send('OK');
});

Rate Limit by Endpoint

Payment Endpoints

EndpointPolicyLimit
POST /api/paymentspayment-create10/min
GET /api/payments/{id}read200/min
GET /api/payments/order/{orderId}read200/min
POST /api/payments/{id}/syncread200/min

Refund Endpoints

EndpointPolicyLimit
POST /api/refundspayment-create10/min
GET /api/refundsread200/min
GET /api/refunds/{id}read200/min

Merchant Self-Service Endpoints

EndpointPolicyLimit
GET /api/merchant/meread200/min
PUT /api/merchant/opennode-keyread200/min
PUT /api/merchant/webhook-urlread200/min
POST /api/merchant/regenerate-keywrite20/min

Webhook Endpoints

EndpointPolicyLimit
POST /api/webhooks/opennodewebhook (sliding window)100/min
POST /api/webhooks/strikewebhook (sliding window)100/min

L402 Endpoints

EndpointPolicyLimit
GET /api/l402/pricingread200/min
GET /api/l402/statusread200/min
/l402/proxy/*global100/min

Enterprise Options

Need higher rate limits? Contact us for enterprise plans with:

  • Custom rate limits based on your needs
  • Dedicated infrastructure
  • Priority support
  • Enhanced support

Contact: enterprise@lightningenable.com

Next Steps