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:
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/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/1.1 429 Too Many Requests
Retry-After: 41
Too many requests, please try again laterAlways branch on the 429 status code and Retry-After, not on the body.
Best Practices#
✅ Honor Retry-After#
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#
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:
// ❌ 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#
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#
// ❌ 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:
- Upgrade your plan — the general limit follows the tenant's subscription tier
- Contact support — custom limits for high-volume merchants
- Optimize your integration — reduce unnecessary calls
Contact: support@auxcore.net
Next Steps#
Need help? Contact support@auxcore.net