Rate limits
To keep the Kavitro API fast and fair for everyone, we apply two kinds of limits: how many requests you can make in a given time (request rate limits), and how expensive a single query is allowed to be (query cost). This page explains both, how to see where you stand, and how to handle a request that goes over.
Request rate limits
The number of requests you can make against the GraphQL endpoint in a rolling one-minute window depends on whether the request is authenticated:
| Requests | Per minute | Counted by |
|---|---|---|
| Authenticated (with an access token) | 500 | Each access token |
| Unauthenticated | 60 | Each IP address |
Every access token has its own independent budget, so separate integrations using separate tokens never compete for the same limit. This is one more reason to create a dedicated access token for each integration rather than sharing one.
These are the limits we currently enforce, and they may be adjusted over time as we tune the API for fair use. Read the response headers (below) rather than hard-coding the numbers into your application.
Checking your usage
Every response includes headers that tell you where you stand in the current window:
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 483
X-RateLimit-Limit- your ceiling for the current window.X-RateLimit-Remaining- how many requests you have left before you're throttled.
When you exceed the limit
If you go over your limit, the request is rejected with an HTTP 429 Too Many Requests status and a Retry-After header telling you how many seconds to wait before trying again:
HTTP/1.1 429 Too Many Requests
Retry-After: 37
{
"message": "Too Many Requests."
}
This is the one case where the API does not follow the usual GraphQL convention of always returning HTTP 200 with an errors property (see error handling). Because throttling happens before your query is executed, the response is a plain HTTP 429 with a short JSON body - not the usual { "data": ..., "errors": ... } shape. Make sure your client handles it as an HTTP-level error.
Best practices
- Honor
Retry-After. When you get a429, wait the number of seconds it specifies before retrying, ideally with exponential backoff. - Watch
X-RateLimit-Remaining. Slow down on your side before you reach zero instead of waiting for the429. - Use a token per integration. Because each token has its own budget, splitting your integrations across tokens gives each one its full limit.
- Prefer webhooks over polling. If you're polling for changes, switch to webhooks to have updates pushed to you and spend far fewer requests. See polling vs webhooks.
Query cost
A single GraphQL query can ask for a lot of data at once - many fields, large lists, deep nesting - so request count alone isn't enough to keep the API responsive. Every query is also assigned a complexity score, and queries that are too expensive are rejected before they run.
You don't have to calculate this yourself, but it helps to understand what drives the score:
- Each field you request adds to the cost. The more fields, the higher the score.
- Lists multiply. For a paginated field, the cost of everything you select inside it is multiplied by the number of records you request. Asking for
first: 50is far more expensive thanfirst: 5selecting the same fields. - Nesting compounds. A list inside a list multiplies again, so deeply nested lists grow the score quickly.
For example, this query fetches 25 records and selects three fields on each:
query {
contacts(first: 25) {
edges {
node {
id
name
lastName
}
}
}
}
Its cost is driven by 25 × (the fields selected per record), plus a little for the list itself. Select more fields, raise first, or nest another list inside, and the score climbs.
Checking a query's cost
You never have to guess: the computed score is returned with every response, under extensions.complexity:
{
"data": { ... },
"extensions": {
"complexity": 320
}
}
Use extensions.complexity while you build. Run your query, look at the score, and tune the fields and page sizes until it sits comfortably low. This is the reliable way to know how expensive a query is - much better than estimating.
When a query is too expensive
If a query's complexity exceeds the allowed ceiling, it's rejected before it executes and returns a GraphQL error in the errors array, with no data. Unlike the rate-limit 429, this is a normal HTTP 200 response that follows the usual error-handling convention.
The exact ceiling can change as we tune the API, so don't hard-code a threshold. Instead, keep an eye on extensions.complexity, keep your queries lean, and handle a rejected query gracefully.
Reducing query cost
If a query is rejected, or its score is higher than you'd like:
- Request only the fields you actually use. Every field counts.
- Use smaller pages. Lower
firstand page through the results instead of pulling everything in one query. Most lists return up to 50 records per page and default to 10. See pagination. - Avoid deep nesting. Split a deeply nested query into a few smaller, focused queries.
Query depth
Separately from complexity, queries are limited to a maximum nesting depth of 11 levels. A query nested deeper than that is rejected with a GraphQL error. In practice this only affects unusually deep queries; if you hit it, flatten the query or split it into smaller ones.
Introspection
Schema introspection is disabled. To explore the available types, queries, mutations and fields, use the API Reference, which is generated directly from the current schema.