Rate Limiting
Lightning Enable applies rate limits to ensure fair usage and system stability.
Rate Limits
Default Limits
| Policy | Limit | Window | Applied To |
|---|---|---|---|
| Global | 100 requests | 1 minute | All requests (per API key when authenticated, per IP when anonymous) |
| Read | 200 requests | 1 minute | GET operations on payments, refunds, merchant settings |
| Payment Create | 10 requests | 1 minute | POST /api/payments, POST /api/refunds, /api/checkout/* |
| Write | 20 requests | 1 minute | Merchant self-service writes (e.g., POST /api/merchant/regenerate-key) |
| Checkout Create | 5 requests | 1 minute | Stripe checkout session creation |
| Admin | 30 requests | 1 minute | Internal admin endpoints |
| Webhook | 100 requests | 1 minute (sliding window) | /api/webhooks/* |
| Magic Link | 3 requests | 15 minutes | Magic link email requests (per IP) |
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 webhooksPOST /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 field | Type | Description |
|---|---|---|
error | string | Always "Too many requests" for the general rate limiter |
message | string | Human-readable description |
retryAfter | number | Seconds to wait before retrying |
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 limiter | Auth-failure throttle | |
|---|---|---|
| Trigger | Too many requests | Too many failed authentications (missing/invalid API key) |
| Scope | Per 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 signal | retryAfter in JSON body | Retry-After response header (and the message text) |
| Correct response | Back off and retry | Fix 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
| Endpoint | Policy | Limit |
|---|---|---|
POST /api/payments | payment-create | 10/min |
GET /api/payments/{id} | read | 200/min |
GET /api/payments/order/{orderId} | read | 200/min |
POST /api/payments/{id}/sync | read | 200/min |
Refund Endpoints
| Endpoint | Policy | Limit |
|---|---|---|
POST /api/refunds | payment-create | 10/min |
GET /api/refunds | read | 200/min |
GET /api/refunds/{id} | read | 200/min |
Merchant Self-Service Endpoints
| Endpoint | Policy | Limit |
|---|---|---|
GET /api/merchant/me | read | 200/min |
PUT /api/merchant/opennode-key | read | 200/min |
PUT /api/merchant/webhook-url | read | 200/min |
POST /api/merchant/regenerate-key | write | 20/min |
Webhook Endpoints
| Endpoint | Policy | Limit |
|---|---|---|
POST /api/webhooks/opennode | webhook (sliding window) | 100/min |
POST /api/webhooks/strike | webhook (sliding window) | 100/min |
L402 Endpoints
| Endpoint | Policy | Limit |
|---|---|---|
GET /api/l402/pricing | read | 200/min |
GET /api/l402/status | read | 200/min |
/l402/proxy/* | global | 100/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
- Errors - Error handling
- Authentication - API key setup
- Webhooks - Avoid polling with webhooks