# 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

- [Error handling →](./errors.md)
- [Webhooks guide →](./webhooks.md)
- [API reference →](../reference/endpoints.md)

---

**Need help?** Contact support@auxcore.net
