Skip to main content

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

TierLimitKeyed byApplies to
Auth10 requests / 15 minutesIPLogin, register, password reset, desktop and device login
Password change3 requests / hourIPPUT /api/v1/auth/password, MFA disable, MFA backup-code regenerate
MFA verify10 requests / 15 minutesIPPOST /api/v1/auth/mfa/verify
Token refresh100 requests / 15 minutesIPPOST /api/v1/auth/refresh
Desktop sync60 requests / minuteDevice token prefix/api/v1/desktop/sync/*
Default200 requests / minutePrincipal (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.

EndpointLimitWindow
POST /api/v1/auth/register10 requestsper 15 minutes
POST /api/v1/auth/login10 requestsper 15 minutes
POST /api/v1/auth/forgot-password10 requestsper 15 minutes
POST /api/v1/auth/reset-password10 requestsper 15 minutes
POST /api/v1/auth/desktop-login10 requestsper 15 minutes
POST /api/v1/auth/device-login10 requestsper 15 minutes
PUT /api/v1/auth/password (change password)3 requestsper hour
POST /api/v1/auth/mfa/verify10 requestsper 15 minutes
POST /api/v1/auth/mfa/disable3 requestsper hour
POST /api/v1/auth/mfa/backup-codes/regenerate3 requestsper hour
POST /api/v1/auth/refresh100 requestsper 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:

HeaderDescription
RateLimit-LimitMaximum requests allowed in the window
RateLimit-RemainingRequests remaining in the current window
RateLimit-ResetSeconds 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.