Rate Limits
Per-endpoint rate limits, response headers, and how to handle 429 Too Many Requests correctly.
Kasar applies rate limits across the whole API surface — the public REST v1 endpoints, the MCP tool endpoint, and sensitive auth/OAuth/extension routes — to prevent abuse: credential brute-force, email spam, account enumeration, OAuth client spam, runaway LLM cost, unbounded meeting-recorder provisioning, and cost-asymmetric or runaway API traffic. Limits are enforced with a sliding window stored in Redis and counted per the identity key documented below.
Rate limiting is applied in addition to authentication. An authenticated token is never immune to rate limits.
Response Headers
Every rate-limited response — whether allowed or denied — carries standard headers so you can adapt your client behavior:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum number of requests allowed within the window |
X-RateLimit-Remaining | Requests still allowed in the current window |
X-RateLimit-Reset | Epoch seconds (UTC) when the window resets |
X-RateLimit-Policy | Identifier of the policy that was applied (e.g. auth:magic-link) |
Retry-After | Only on 429: seconds to wait before retrying |
429 Response Body
When a request is denied, the API returns HTTP 429 Too Many Requests:
{
"error": "Too many requests",
"policy": "auth:magic-link",
"retryAfterSeconds": 420
}Respect the Retry-After header. Implement exponential backoff for automated clients, and surface a clear message (e.g. "Please wait a few minutes before trying again") to end users.
Current Policies
Each policy below applies to a specific endpoint or family of endpoints. The identity key column describes which attribute the limit is scoped to — for example, IP + email means two different emails from the same IP share no bucket, and the same email from two different IPs share no bucket either.
Authentication
| Policy | Endpoint | Limit | Window | Identity key |
|---|---|---|---|---|
auth:magic-link | POST /api/auth/magic-link | 15 | 10 min | IP + email |
auth:signup | POST /api/auth/signup | 10 | 1 hour | IP |
auth:verify-token | GET /api/auth/verify-token | 20 | 10 min | IP + email |
auth:invite-verify | POST /api/auth/invite/verify | 20 | 10 min | IP |
auth:invite-send | Invitation creation (server action) | 20 | 1 hour | Organization id |
Why these limits?
auth:magic-link— Prevents an attacker from spamming your users' inboxes or enumerating which emails have an account.auth:signup— Prevents bulk bot-driven organization creation.auth:verify-token— Caps brute-force attempts on verification tokens. Legitimate users only consume the link once.auth:invite-verify— Caps brute-force on invitation tokens.auth:invite-send— Caps email spam from a compromised admin account. Keyed by organization rather than IP; 20 per hour comfortably covers onboarding a full team in one sitting.
auth:invite-send is enforced on a server action, not an HTTP route. Server actions have no Response object, so instead of a 429 they surface an in-band error in the action result. The other authentication policies run on HTTP routes and return a standard 429.
OAuth
| Policy | Endpoint | Limit | Window | Identity key |
|---|---|---|---|---|
oauth:register | POST /api/oauth/register | 20 | 1 hour | IP |
oauth:token | POST /api/oauth/token | 60 | 10 min | IP |
Why these limits?
oauth:register— Dynamic Client Registration (RFC 7591) is an open endpoint; the cap stops spam creation ofoauth_clientsrows per IP.oauth:token— Caps abuse of theauthorization_codegrant per IP. Codes are already single-use, PKCE-protected, and expire after 60 seconds, so this is a backstop.
AI & LLM
| Policy | Endpoint | Limit | Window | Identity key |
|---|---|---|---|---|
agent:stream | POST /api/agent/stream | 120 | 1 hour | Authenticated user id (falls back to IP) |
Why this limit? Each streaming run triggers Claude API calls whose cost scales with token usage. A capped per-user ceiling prevents a single compromised session from running up a large bill.
Chrome Extension (Meeting Recorder)
| Policy | Endpoint | Limit | Window | Identity key |
|---|---|---|---|---|
extension:session-create | POST /api/extension/sessions | 20 | 10 min | Authenticated user id |
extension:session-create-org | POST /api/extension/sessions | 100 | 10 min | Organization id |
extension:session-stop | POST /api/extension/sessions/{meetingId}/stop | 60 | 10 min | Authenticated user id |
extension:session-refresh-ws | POST /api/extension/sessions/{meetingId}/refresh-ws | 30 | 10 min | Authenticated user id |
Why these limits? Starting a recording provisions a meeting-bot pod slot and a Deepgram transcription session — real per-call cost. The per-user extension:session-create cap bounds the blast radius of a single compromised extension token; extension:session-create-org adds an org-wide ceiling so an organization with many compromised tokens can't scale recordings past its aggregate budget. extension:session-stop is looser because legitimate clients may retry stop on transient errors, and extension:session-refresh-ws covers proactive wsToken refreshes before the 30-minute token TTL expires.
POST /api/extension/sessions is gated by two policies at once: the per-user extension:session-create cap is checked first, then the org-wide extension:session-create-org cap. Either one can deny the request with a 429.
Public API & MCP Tools
Every authenticated REST v1 request and every MCP tool/resource call is rate-limited. A token is an identity, so a user's REST and MCP traffic count against the same per-identity buckets.
| Policy | Applies to | Limit | Window | Identity key |
|---|---|---|---|---|
api:ip | All REST v1 requests (pre-auth) | 1000 | 1 min | IP |
api:global | All REST v1 + MCP tool/resource calls | 600 | 1 min | Workspace user id |
api:expensive | Global search, aggregation, duplicate detection | 90 | 1 min | Workspace user id |
api:bulk | Import + export jobs | 20 | 10 min | Organization id |
api:messaging-send | Real outbound sends (crm_send_message, email/LinkedIn/WhatsApp) | 30 | 1 hour | Workspace user id |
Why these limits?
api:ip— A pre-auth ceiling. The token-verification path (JWT verify + token lookup + metadata load) is itself work, so capping per IP before auth stops floods of arbitraryBearer ksr_*tokens. Generous, because office NATs share one IP — the real throttle is per-identity.api:global— The per-identity ceiling across the entire data surface. Enforced at both chokepoints (RESTwithAuthand the MCP context builder), so a user's REST + MCP calls share one budget. 600/min (~10/s) comfortably covers an interactive AI agent or batch client while stopping a runaway loop.api:expensive— Cost-asymmetric reads fan out across objects or scan large row sets, so they get a tighter cap. Enforced inside the shared handler, so REST, MCP, and the in-app AI agent all inherit it.api:bulk— Import/export can move thousands of rows and spawn a worker; keyed per organization so the whole org shares the budget.api:messaging-send— Only the actual send consumes it (previewing a message is free), keyed per user so a compromised token can't spam outbound mail.
The api:expensive, api:bulk, and api:messaging-send caps are enforced inside the shared handler that backs each operation, so the REST endpoint, the MCP tool, and the in-app AI agent are all throttled by one consistent rule. A request can therefore consume two buckets at once: the api:global ceiling plus the matching cost-class policy.
Handling 429 in Your Integration
TypeScript example:
async function callKasar(path: string, init: RequestInit, attempt = 0): Promise<Response> {
const res = await fetch(`https://kasar.app${path}`, init)
if (res.status === 429 && attempt < 3) {
const retryAfter = Number(res.headers.get('Retry-After') ?? 60)
await new Promise((r) => setTimeout(r, retryAfter * 1000))
return callKasar(path, init, attempt + 1)
}
return res
}Python example:
import time
import requests
def call_kasar(path, attempt=0, **kwargs):
res = requests.request(url=f"https://kasar.app{path}", **kwargs)
if res.status_code == 429 and attempt < 3:
retry_after = int(res.headers.get("Retry-After", 60))
time.sleep(retry_after)
return call_kasar(path, attempt + 1, **kwargs)
return resOperational Notes
- Fail-open on infrastructure issues. If the Redis backend is unreachable, requests are allowed through rather than blocked. Rate limiting is a defense-in-depth measure and never becomes a single point of failure for authentication.
- Sliding window. The window slides with each request — not a fixed calendar bucket. A request made at
t=0ages out exactlywindowSeclater. - Policy IDs are stable. The
X-RateLimit-Policyheader value is part of the public contract. If you log it, you can rely on the identifier not changing.
Requesting Higher Limits
If your integration has a legitimate need for a higher ceiling (dedicated tenant, enterprise workflow), contact support with:
- The policy name (
X-RateLimit-Policy) - The expected peak request rate
- A short description of the integration
We review requests case-by-case.