Developer dashboard

Rate Limits#

API rate limiting policies and best practices.


Rate Limit Overview#

To ensure API availability and prevent abuse, AuxCORE enforces rate limits at the API gateway. Every request to /api/v1/* passes through a general limiter, and some public endpoints have an additional per-IP limit.

General limiter#

Caller Limit Window Counted per
Signed-in user (JWT) 400 1 minute Tenant + user
Public API (/api/v1/public/*) — any key or none 1,000 15 minutes Client IP
AuxCORE SDK API (/api/v1/auxvault/*) — server-side API key 1,000 15 minutes Tenant
Admin API with an API key, plan MVP (default) 1,000 15 minutes Tenant
Plan Starter 2,000 15 minutes Tenant
Plan Growth 3,000 15 minutes Tenant
Plan Custom 1,500 15 minutes Tenant
Plan Enterprise / Full 5,000 15 minutes Tenant

The Public API is limited before the API key is resolved, so it is always counted per client IP at the MVP limit, whatever the plan. On the Admin API, API-key traffic is counted against the tenant, so every integration on the tenant shares one bucket; the plan's limit applies when the request carries X-Tenant-ID, otherwise the MVP limit. On /api/v1/auxvault/*, gateway auth resolves the tenant from the key before limiting; requests share a tenant bucket at the MVP limit because this namespace skips the tenant-plan lookup. Sandbox applies the same limits as Production.

Per-IP public endpoint limits#

These apply in addition to the general limiter and are counted per client IP:

Endpoints Limit Window
POST /public/accesstoken, /refreshToken, /client-token, /validate-client-token, GET /public/field-config 30 15 minutes
POST /public/checkout-session and all /public/checkout-sessions/:sessionId/*, /public/transactions/return/:token, /public/transactions/status/:token 30 15 minutes
POST /public/otp/generate, POST /public/otp/resend 10 1 hour
POST /public/kyc/verify 60 1 hour
GET /public/totalTransactions/multi/:ids 300 15 minutes

Endpoints in the same row group share one counter — for example the token endpoints and the checkout endpoints all draw on the same 30-per-15-minutes budget for an IP. A WooCommerce store creates every checkout session from its own server, so a busy store can reach this limit from its server IP.

Sign-in limits#

POST /api/v1/public/accesstoken and POST /api/v1/auth/login additionally limit failed attempts: 10 per account and 100 per IP in 15 minutes. Successful sign-ins do not count. POST /api/v1/auth/refresh allows 120 requests per 15 minutes per IP. These sign-in limits are switched off on development environments.


Rate Limit Headers#

Rate-limited responses carry the standard RateLimit-* headers:

http
RateLimit-Limit: 1000
RateLimit-Remaining: 995
RateLimit-Reset: 812
Header Description
RateLimit-Limit Maximum requests allowed in the window
RateLimit-Remaining Requests remaining in the current window
RateLimit-Reset Seconds until the window resets
Retry-After On a 429 only — seconds to wait before retrying

X-RateLimit-* headers are not sent.


Rate Limit Exceeded#

When you exceed a limit the API returns 429 Too Many Requests. The body depends on which limiter fired.

Per-IP public endpoint limits and sign-in limits return JSON:

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

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests from this IP. Try again later."
  }
}

The general limiter returns a plain-text body:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 41

Too many requests, please try again later

Always branch on the 429 status code and Retry-After, not on the body.


Best Practices#

✅ Honor Retry-After#

javascript
async function makeRequestWithRetry(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fn();
    if (response.status !== 429 || i === maxRetries - 1) return response;

    const retryAfter = Number(response.headers.get('retry-after'));
    const delay = Number.isFinite(retryAfter)
      ? retryAfter * 1000
      : Math.min(1000 * Math.pow(2, i), 10000);
    await sleep(delay);
  }
}

✅ Check Headers Before You Hit the Limit#

javascript
function waitTimeMs(response) {
  const remaining = parseInt(response.headers.get('ratelimit-remaining'), 10);
  const resetSeconds = parseInt(response.headers.get('ratelimit-reset'), 10);

  if (remaining === 0) {
    return resetSeconds * 1000;
  }
  return 0;
}

✅ Use List Endpoints#

Instead of fetching transactions one by one:

javascript
// ❌ BAD: 100 API calls
for (const txn of transactions) {
  await fetch(`${BASE_URL}/transaction/${txn.id}`, { headers });
}

// ✅ GOOD: 1 API call
await fetch(`${BASE_URL}/transaction-list`, { headers });

✅ Cache Responses#

javascript
const cache = new Map();

async function getTransaction(id) {
  if (cache.has(id)) {
    return cache.get(id);
  }
  
  const res = await fetch(`${BASE_URL}/transaction/${id}`, { headers });
  const txn = await res.json();
  cache.set(id, txn);
  
  // Cache for 5 minutes
  setTimeout(() => cache.delete(id), 5 * 60 * 1000);
  
  return txn;
}

✅ Use Webhooks Instead of Polling#

javascript
// ❌ BAD: Poll every second (3600 requests/hour)
setInterval(async () => {
  const res = await fetch(`${BASE_URL}/transaction/${txnId}`, { headers });
  // ...
}, 1000);

// ✅ GOOD: Use webhooks (0 polling requests)
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const event = JSON.parse(req.body); // verify X-Webhook-Signature first
  if (event.type === 'transaction.approved') {
    processTransaction(event.data);
  }
  res.sendStatus(200);
});

Monitoring Rate Limits#

There is no rate-limit status endpoint. Read the RateLimit-* headers on your normal responses.


Increasing Rate Limits#

Need higher limits? Options:

  1. Upgrade your plan — the general limit follows the tenant's subscription tier
  2. Contact support — custom limits for high-volume merchants
  3. Optimize your integration — reduce unnecessary calls

Contact: support@auxcore.net


Next Steps#


Need help? Contact support@auxcore.net