← API reference

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:

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: 37

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")