Rate limits
The API enforces three fixed-window rate limits per request. All three counters must pass for the request to proceed.
Limits
| Bucket | Limit | Window |
|---|---|---|
| Per token (project token) | 60 requests | 1 minute |
| Per token (workspace token) | 120 requests | 1 minute |
| Per workspace (all tokens combined) | 600 requests | 1 minute |
| Per workspace | 50,000 requests | 1 day (UTC midnight reset) |
The per-token limit prevents a single token from monopolising the workspace quota. The per-workspace limits protect shared infrastructure across all tokens in a workspace.
The workspace limits are shared by every token of the workspace, of both kinds. A workspace token's own limit is higher because it serves many projects, but it still draws from the same workspace minute and day buckets, so one integration's traffic counts against every other integration in the workspace. A runaway workspace token alone cannot use the whole minute bucket (120 of 600), but five can. Revoke a token that misbehaves.
Reading your limits programmatically
GET /v1/projects/{project_id}/limits reports the enforced configuration plus
today's usage. A read scope token is enough, and rate_limits.per_minute
is the limit of the calling token's kind (60 for a project token, 120 for a
workspace token):
curl -H "Authorization: Bearer zt_live_<token>" \
"https://api.ziptie.ai/v1/projects/PROJECT_ID/limits"{
"addon_active": true,
"rate_limits": {
"per_minute": 60,
"workspace_per_minute": 600,
"per_day": 50000
},
"day_used": 1842,
"day_remaining": 48158,
"reset_at": null
}Three things to know before you build on the response:
rate_limits.per_day,day_usedandday_remainingare workspace-wide, not per token. Every token in the workspace draws down the same daily counter.reset_atis alwaysnulltoday. The daily counter resets at UTC midnight; compute the reset yourself rather than reading it from this field.addon_activeis alwaystruein this response and is not an entitlement check. A workspace whose add-on is off never gets this far - the request is rejected withAPI_ADDON_INACTIVEfirst, as a 403 on/v1/or as JSON-RPC-32004at HTTP 200 over MCP. Branch on the error code instead.
Response headers
Every successful /v1/ response includes rate-limit headers reflecting the
per-token bucket. Responses from /mcp do not carry them; a rate-limited /mcp
call carries only Retry-After, alongside its JSON-RPC error body:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 37X-RateLimit-Limit- the per-token per-minute ceiling.X-RateLimit-Remaining- how many requests remain in the current window.X-RateLimit-Reset- the number of seconds until the current minute window resets (a countdown, not a Unix timestamp).
Bulk requests
POST /v1/projects/{project_id}/prompts/bulk creates up to 500 prompts in one
request and counts as one request in each bucket (per token, per workspace
per minute and per workspace per day), however many texts it carries. A
10,000-prompt load is 20 requests.
When a limit is exceeded
The rejection shape depends on which surface you called. Both carry the same
Retry-After header, whose value is the number of seconds until the current
window resets. Respect that header before retrying.
On /v1/
A 429 response with the standard error envelope:
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 37
{"error": "Rate limit exceeded", "code": "RATE_LIMITED"}On /mcp
An HTTP 200 carrying a JSON-RPC error member, because every error on the MCP surface is a JSON-RPC message rather than a transport failure:
HTTP/1.1 200 OK
Retry-After: 37
{
"jsonrpc": "2.0",
"id": "call-2",
"error": {
"code": -32003,
"message": "Rate limit exceeded",
"data": {
"code": "RATE_LIMITED",
"error": "Rate limit exceeded",
"retry_after_seconds": 37
}
}
}data.retry_after_seconds carries the same number as the header, so an MCP
client never has to reach for the transport layer. Branch on
error.code == -32003; a client that branches on the HTTP status will not see
this rejection at all.
MCP exemptions
MCP initialize and tools/list calls are exempt from rate limiting.
tools/call requests count against the same per-token and per-workspace
buckets as direct /v1/ requests, at one unit per call. The unit is
charged when the call reaches /mcp, and the /v1/ request the tool runs
internally is not charged again, so an MCP-driven integration has the same
budget as an equivalent REST integration: a 60-per-minute token supports 60
tools/call per minute. day_used on GET /v1/projects/{project_id}/limits
counts one unit per tools/call.
Edge IP-level limit
In addition to the application-level limits above, the edge layer
applies an IP-level ceiling of approximately 300 requests per minute
(5 req/s with burst 30) to /v1/ requests. The /mcp endpoint is not
covered by it. This limit is shared across all tokens originating from the
same IP address. In practice, tokens running from server environments rarely
hit this ceiling.
Handling limits in client code
A minimal retry loop:
import time
import requests
def call_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
time.sleep(retry_after)
continue
response.raise_for_status()
return response.json()
raise RuntimeError("Exceeded max retries")