Rate Limits
The Not AI Public REST API applies a sliding-window rate limit at the edge: no more than the per-tier request count in any rolling 60-second period. Limits are scoped to the plan tier of the integration the presented key resolves to. The free tier is included, and the tier only changes the rate of consumption, not the right to consume. A 429 can occur on every authenticated route; per-endpoint response lists do not repeat it.
Per-key isolation requires the Authorization: Bearer form. The edge counter reads the Authorization header value to separate callers, so x-api-key requests share a per-tier edge bucket and can throttle each other under load. Use Authorization: Bearer for production traffic; reserve x-api-key for quick scripts and shell pipelines where the precedence is acceptable.
Per-tier limits
| Plan | Per-minute | Max page size | Available history window |
|---|---|---|---|
| Free | 60 | 50 | 7 days (default) |
| Pro | 600 | 100 | 30 days (default) |
| Enterprise | 6,000 | 100 | Up to 7 years |
The dashboard’s plan picker also offers a starter billing plan. At the API edge, starter buckets into the free tier: the x-isnotai-tier header echoes free and the Free row above applies (60 per minute, page cap 50). The one place starter differs from free in its own right is the webhook subscription cap, which is enforced per billing plan.
A request is rejected when the per-minute window for its tier is exhausted (“per-minute” means a rolling 60-second window, not a calendar minute). Per-day quotas are not currently enforced at the edge.
The max page size caps the ?pageSize= query parameter on every list endpoint. Above the tier cap, the server clamps to the cap and reflects the effective value back in pagination.limit. The ?pageSize= you asked for is never larger than what the server is willing to serve. If you need bigger pages, upgrade the integration tier.
Requests that target a startDate outside the available history window return an empty page rather than an error.
Tier echo header
Every response to a successfully authenticated request carries an x-isnotai-tier header echoing the resolved plan tier (free, pro, or enterprise), regardless of status code. It is stamped from the integration record, so you can confirm in one request which row of the table above applies to a given key, including on 4xx validation failures and 5xx errors that surface after auth.
Responses returned before the integration resolves do not carry the header. In practice that is the 401 Unauthorized envelope when authentication fails, the edge-emitted 429 (the request never reaches the API), and the rare 500 INTERNAL_ERROR returned if the authentication service itself errors before resolving the integration.
The header echoes one of free, pro, or enterprise. Integrations on the starter billing plan echo free (starter buckets into the free tier at the edge).
x-isnotai-tier: pro
No live remaining-quota counters are emitted. Pace your traffic against the table above, not against per-response telemetry.
When you hit the limit
A request that exhausts the per-minute window returns 429 Too Many Requests with the standard error envelope:
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded for this API key."
}
}
A Retry-After header is included when the edge can compute one. Its value is an integer number of seconds, never an HTTP-date; parse it as digits. Wait at least that long before retrying. If the header is absent, back off with an exponential delay seeded at one second.
Because the 429 is emitted at the edge before the request reaches the API, the response does not carry the x-isnotai-tier echo header.
import os
import random
import time
import requests
BASE_URL = "https://api.isnotai.com"
HEADERS = {"Authorization": f"Bearer {os.environ['ISNOTAI_API_KEY']}"}
def get_with_throttle(path, params=None, max_attempts=8):
delay = 1.0
for attempt in range(max_attempts):
response = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=10)
if response.status_code != 429:
return response
retry_after = response.headers.get("Retry-After")
if retry_after and retry_after.isdigit():
time.sleep(int(retry_after))
else:
time.sleep(delay + random.random())
delay = min(delay * 2, 60.0)
response.raise_for_status()
return response
Staying under the limit
- Pace your traffic against the per-tier table above; the API does not surface live remaining-quota counters.
- For backfills, paginate at a steady cadence rather than fanning out. Two parallel walkers on the same
Authorization: Bearerkey share the same per-minute budget. - Cache responses on your side when the same data will be reread soon.
/v1/stats,/v1/courses, and/v1/integrationare particularly cache-friendly. - If you need a higher ceiling, change the integration’s plan in the dashboard at
dash.isnotai.com. The new tier applies on the next request; there is no in-flight switchover.