Rate Limits
The API enforces rate limits at the application layer so a single misbehaving client cannot starve others. Limits are grouped into tiers, applied as middleware on the relevant routes.
Tier summary
| Tier | Limit | Keyed by | Applies to |
|---|---|---|---|
| Auth | 10 requests / 15 minutes | IP | Login, register, password reset, desktop and device login |
| Password change | 3 requests / hour | IP | PUT /api/v1/auth/password, MFA disable, MFA backup-code regenerate |
| MFA verify | 10 requests / 15 minutes | IP | POST /api/v1/auth/mfa/verify |
| Token refresh | 100 requests / 15 minutes | IP | POST /api/v1/auth/refresh |
| Desktop sync | 60 requests / minute | Device token prefix | /api/v1/desktop/sync/* |
| Default | 200 requests / minute | Principal (token prefix) | Every other /api/v1/* endpoint |
The default tier is sized to absorb realistic signal-write bursts (up to about 80 signals when a user pushes to both paper and live accounts) on top of concurrent reads. There is no signal-specific tier.
Authentication endpoints
Authentication endpoints have strict limits to protect against brute-force and credential-stuffing attacks.
| Endpoint | Limit | Window |
|---|---|---|
POST /api/v1/auth/register | 10 requests | per 15 minutes |
POST /api/v1/auth/login | 10 requests | per 15 minutes |
POST /api/v1/auth/forgot-password | 10 requests | per 15 minutes |
POST /api/v1/auth/reset-password | 10 requests | per 15 minutes |
POST /api/v1/auth/desktop-login | 10 requests | per 15 minutes |
POST /api/v1/auth/device-login | 10 requests | per 15 minutes |
PUT /api/v1/auth/password (change password) | 3 requests | per hour |
POST /api/v1/auth/mfa/verify | 10 requests | per 15 minutes |
POST /api/v1/auth/mfa/disable | 3 requests | per hour |
POST /api/v1/auth/mfa/backup-codes/regenerate | 3 requests | per hour |
POST /api/v1/auth/refresh | 100 requests | per 15 minutes |
Connected app tokens (desktop sync)
Requests authenticated with a device or connected app token (dt_ prefix) on the desktop sync routes are limited to 60 requests per minute, keyed by the token prefix. Each issued token gets its own bucket, so two desktop installs on the same user account do not share the budget.
Other authenticated endpoints
Every endpoint under /api/v1 is covered by the default tier of 200 requests per minute per principal, regardless of whether the caller authenticates with a JWT, a device token, or an API key. The principal key is derived from the credential the caller presents, so each session, device, and API key has its own bucket.
Rate limit headers
All rate-limited endpoints return standard headers on every response:
| 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 |
When the limit is exceeded, the API also sets Retry-After to the number of seconds before the window resets.
Rate limit exceeded
When you exceed a rate limit the API returns 429 Too Many Requests with a structured body:
{
"error": "rate_limited",
"message": "Too many requests. Limit is 200 requests per minute.",
"code": "RATE_LIMIT_EXCEEDED",
"retryAfter": 60
}
The error field is a machine-readable identifier (rate_limited). Use message for any text shown to a user, and retryAfter (in seconds) to compute when to retry.
Best practices
Implement exponential backoff
When receiving a 429 response, wait before retrying. Prefer the Retry-After header when present.
async function makeRequestWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After")) || 0;
const delay = (retryAfter || Math.pow(2, attempt)) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
return response;
}
throw new Error("Max retries exceeded");
}
Cache authentication tokens
Reuse access tokens until they expire rather than re-authenticating on every request. Use POST /api/v1/auth/refresh to obtain a new access token without re-entering credentials.
Batch requests where possible
For connected apps that need to sync data periodically, fetch all required data in one pass rather than making many small requests. For example, fetch trades and accounts together in your sync loop rather than making separate requests for each.