Skip to content
StraventaDocs
Guides

Rate Limits

Rate limit headers, 429 response format, and exponential backoff implementation

Rate Limits

505pay enforces per-tenant, per-endpoint rate limits to ensure platform stability. Limits are communicated via standard response headers on every API call.

Rate limit headers

Every API response includes these headers:

HeaderTypeDescription
X-RateLimit-LimitintegerMaximum requests allowed in the current window
X-RateLimit-RemainingintegerRequests remaining in the current window
X-RateLimit-ResetUnix timestampWhen the current window resets (UTC epoch seconds)
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1746921600

429 response

When you exceed the rate limit, the API returns 429 Too Many Requests:

{
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded. Retry after 2026-05-11T08:00:00Z.",
  "request_id": "01j..."
}

The response also includes the standard rate limit headers so you know exactly when to retry.

Do not poll the API in a tight loop. Repeated 429 responses may temporarily lower your rate limit bucket.

Default limits

Endpoint categoryLimit
Token issuance (/token)60 req/min per IP
Payment creation120 req/min per tenant
Payment reads600 req/min per tenant
Webhook management60 req/min per tenant
Admin operations300 req/min per tenant
All other endpoints300 req/min per tenant

Contact support to request higher limits for production integrations with high throughput requirements.

Exponential backoff with jitter

The recommended backoff strategy is exponential with full jitter to prevent thundering herd:

interface RateLimitError {
  error: 'rate_limit_exceeded';
  message: string;
  request_id: string;
}

async function fetchWithBackoff(
  url: string,
  options: RequestInit,
  maxAttempts = 5,
): Promise<Response> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const res = await fetch(url, options);

    if (res.status !== 429) return res;

    if (attempt === maxAttempts - 1) throw new Error('Rate limit exceeded after max retries');

    // Prefer the server-provided reset time if available
    const resetAt = res.headers.get('X-RateLimit-Reset');
    let waitMs: number;

    if (resetAt) {
      // Wait until the reset window, plus a small jitter
      const resetMs = Number(resetAt) * 1000;
      const jitter = Math.random() * 1000;
      waitMs = Math.max(resetMs - Date.now() + jitter, 0);
    } else {
      // Fallback: exponential backoff with full jitter
      const base = Math.min(2 ** attempt * 1000, 60_000);
      waitMs = Math.random() * base;
    }

    console.warn(
      `Rate limited (attempt ${attempt + 1}/${maxAttempts}), ` +
      `retrying in ${Math.round(waitMs / 1000)}s`
    );
    await new Promise(r => setTimeout(r, waitMs));
  }

  throw new Error('Unreachable');
}

// Usage
const res = await fetchWithBackoff(
  'https://sandbox.505pay.link/v1/payments',
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
  },
);
import time
import random
import requests
from requests.exceptions import HTTPError

def fetch_with_backoff(
    url: str,
    method: str = 'GET',
    max_attempts: int = 5,
    **kwargs,
) -> requests.Response:
    for attempt in range(max_attempts):
        response = requests.request(method, url, **kwargs)

        if response.status_code != 429:
            response.raise_for_status()
            return response

        if attempt == max_attempts - 1:
            raise HTTPError(f'Rate limit exceeded after {max_attempts} attempts', response=response)

        # Prefer the server-provided reset time
        reset_at = response.headers.get('X-RateLimit-Reset')
        if reset_at:
            wait = max(int(reset_at) - time.time(), 0) + random.uniform(0, 1)
        else:
            # Exponential backoff with full jitter
            base = min(2 ** attempt, 60)
            wait = random.uniform(0, base)

        print(f'Rate limited (attempt {attempt + 1}/{max_attempts}), retrying in {wait:.1f}s')
        time.sleep(wait)

    raise RuntimeError('Unreachable')

# Usage
response = fetch_with_backoff(
    'https://sandbox.505pay.link/v1/payments',
    headers={'Authorization': f'Bearer {access_token}'},
)
payments = response.json()

Best practices

  • Read X-RateLimit-Remaining proactively. Slow down before you hit zero rather than reacting to 429s.
  • Use X-RateLimit-Reset as your retry target. It is more accurate than a fixed backoff duration.
  • Add jitter to any backoff. Multiple clients retrying simultaneously at the exact reset time causes a new burst.
  • Batch reads where possible. Listing 100 payments in one request consumes 1 rate-limit slot instead of 100.
  • Cache aggressively. Payment status, configuration, and reference data change infrequently — cache them with a short TTL instead of re-fetching on every render.

Was this page helpful?

On this page