---
title: Rate limits and retries
description: What the API answers when it throttles a client, how to retry with Retry-After and back-off, and where the monthly API call limits of each plan are stated.
canonical: https://developers.coevera.com/concepts/rate-limits-and-retries/
section: concepts
verification: schema
lastVerified: 2026-10-16
publisher: Coevera
---

# Rate limits and retries

**Answer:** A throttled request answers HTTP 429 with code 182 and, when the limit computes one, a Retry-After header in seconds: wait that long, then retry. Without the header, back off exponentially. No per-minute limit is published. Monthly API call limits depend on your plan; see plans and limits. Never retry a 401.

An integration that sends many requests will at some point be asked to slow down. Build the
retry in from the start: it costs a few lines, and it is the difference between a slow sync and a
failed one.

## How throttling looks

A throttled request is refused with HTTP 429, code 182 `ERROR_TOO_MANY_REQUESTS`. In REST the
body is the usual [error envelope](/concepts/errors/#the-error-envelope). In GraphQL it is not an
`errors` list but a short body with `status`, `message` and `errorcode`, the same shape as a
GraphQL `401`. When the limit computes a wait, a `Retry-After` header gives it in whole seconds.

No per-minute number is published.

Failed sign-ins are throttled on their own. After repeated failed sign-ins the credentials are
locked for a while: every request answers 429, even with the right password, and without
`Retry-After`. Stop at the first HTTP 401 and check the credentials
([authentication](/get-started/authentication/)): retrying a 401 only adds failed sign-ins.

## Retry after a 429

Every request in this helper goes through one function. On HTTP 429 it waits for `Retry-After`,
or 5 seconds when the header is missing, and tries again, up to three attempts; any other error
is raised with its code:

**REST · cURL**

```bash
# call METHOD PATH [BODY]: one REST request; prints the body. On HTTP 429 it waits as long as
# Retry-After says (5 seconds when the header is missing) and tries again, three attempts in
# all. On any other error it prints "HTTP <status>, code <code> <name>: <message>" and the
# body, sets ERROR_CODE to the code and returns 1.
call() {
  local attempt headers response status delay
  local args=(-X "$1" "$COEVERA_BASE$2" -u "$COEVERA_API_KEY:$COEVERA_API_SECRET")
  if [ $# -ge 3 ]; then args+=(-H "Content-Type: application/json" -d "$3"); fi
  headers=$(mktemp)
  for attempt in 1 2 3; do
    response=$(curl -sS -D "$headers" -w '\n%{http_code}' "${args[@]}")
    status=${response##*$'\n'}
    response=${response%$'\n'*}
    if [ "$status" = 429 ] && [ "$attempt" -lt 3 ]; then
      delay=$(sed -n 's/^[Rr]etry-[Aa]fter: *\([0-9][0-9]*\).*/\1/p' "$headers")
      sleep "${delay:-5}"
      continue
    fi
    rm -f "$headers"
    if [ "$status" -ge 400 ]; then
      # The message is for people; branch on the numeric code.
      jq -r --arg status "$status" \
        '"HTTP \($status), code \(.code) \(.name): \(.message | sub("^\\s+"; "") | sub("\\s+$"; ""))"' <<<"$response"
      echo "$response"
      ERROR_CODE=$(jq -r '.code' <<<"$response")
      return 1
    fi
    echo "$response"
    return 0
  done
}
```

**REST · TypeScript**

```ts
// An error the API answered: the HTTP status, and the code, name and message of its body.
class ApiError extends Error {
  constructor(
    readonly status: number,
    readonly code: number,
    readonly errorName: string,
    message: string,
    readonly body: unknown,
  ) {
    super(`HTTP ${status}, code ${code} ${errorName}: ${message}`);
  }
}

const sleep = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

// call: one REST request. On HTTP 429 it waits as long as Retry-After says (5 seconds when the
// header is missing) and tries again, three attempts in all. Any other error is an ApiError.
async function call(path: string, init: { method?: string; body?: unknown } = {}) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`${base}${path}`, {
      method: init.method ?? "GET",
      headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
      body: init.body === undefined ? undefined : JSON.stringify(init.body),
    });
    if (res.status === 429 && attempt < 3) {
      await sleep(Number(res.headers.get("Retry-After")) || 5);
      continue;
    }
    const body = await res.json();
    // The message is for people; branch on the numeric code.
    if (!res.ok) throw new ApiError(res.status, body.code, body.name, String(body.message).trim(), body);
    return body;
  }
}
```

**REST · Python**

```python
class ApiError(Exception):
    """An error the API answered: the HTTP status, and the code, name and message of its body."""

    def __init__(self, status, body):
        self.status, self.body = status, body
        self.code, self.name = body.get("code"), body.get("name")
        message = str(body.get("message")).strip()
        super().__init__(f"HTTP {status}, code {self.code} {self.name}: {message}")


def call(method, path, **kwargs):
    """One REST request. On HTTP 429 it waits as long as Retry-After says (5 seconds when the
    header is missing) and tries again, three attempts in all. Any other error is an ApiError."""
    for attempt in range(1, 4):
        response = session.request(method, f"{BASE}{path}", timeout=30, **kwargs)
        if response.status_code == 429 and attempt < 3:
            time.sleep(float(response.headers.get("Retry-After") or 5))
            continue
        body = response.json()
        # The message is for people; branch on the numeric code.
        if not response.ok:
            raise ApiError(response.status_code, body)
        return body
```

**GraphQL · cURL**

```bash
# call REQUEST: POST one GraphQL request (a JSON body); prints the data. On HTTP 429 it waits as
# long as Retry-After says (5 seconds when the header is missing) and tries again, three
# attempts in all. On any error it prints "HTTP <status>, code <code> <name>: <message>" and
# the error, sets ERROR_CODE to the api_error code (null when there is none) and returns 1.
call() {
  local attempt headers response status delay
  headers=$(mktemp)
  for attempt in 1 2 3; do
    response=$(curl -sS -D "$headers" -w '\n%{http_code}' "$COEVERA_GRAPHQL" \
      -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
      -H "Content-Type: application/json" \
      -d "$1")
    status=${response##*$'\n'}
    response=${response%$'\n'*}
    if [ "$status" = 429 ] && [ "$attempt" -lt 3 ]; then
      delay=$(sed -n 's/^[Rr]etry-[Aa]fter: *\([0-9][0-9]*\).*/\1/p' "$headers")
      sleep "${delay:-5}"
      continue
    fi
    rm -f "$headers"
    if [ "$status" != 200 ] || jq -e '.errors' <<<"$response" >/dev/null; then
      # The message is for people; branch on the numeric code.
      jq -r --arg status "$status" '.errors[0]
        | "HTTP \($status), code \(.api_error.code) \(.api_error.name // ""): \(.message | sub("^\\s+"; "") | sub("\\s+$"; ""))"' <<<"$response"
      jq '.errors[0]' <<<"$response"
      ERROR_CODE=$(jq -r '.errors[0].api_error.code' <<<"$response")
      return 1
    fi
    jq '.data' <<<"$response"
    return 0
  done
}
```

**GraphQL · TypeScript**

```ts
// An error the API answered: the HTTP status, and the code and name of api_error when the
// error has one (a document the schema refuses has none).
class ApiError extends Error {
  constructor(
    readonly status: number,
    readonly code: number | undefined,
    readonly errorName: string | undefined,
    message: string,
    readonly error: unknown,
  ) {
    super(`HTTP ${status}, code ${code ?? "none"} ${errorName ?? ""}: ${message}`);
  }
}

const sleep = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

// call: one GraphQL request. On HTTP 429 it waits as long as Retry-After says (5 seconds when
// the header is missing) and tries again, three attempts in all. Any error is an ApiError.
async function call(query: string, variables: object = {}) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(endpoint, {
      method: "POST",
      headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
      body: JSON.stringify({ query, variables }),
    });
    if (res.status === 429 && attempt < 3) {
      await sleep(Number(res.headers.get("Retry-After")) || 5);
      continue;
    }
    const body = await res.json();
    const error = body.errors?.[0];
    // The message is for people; branch on the numeric code.
    if (!res.ok || error) {
      throw new ApiError(res.status, error?.api_error?.code, error?.api_error?.name, String(error?.message).trim(), error);
    }
    return body.data;
  }
}
```

**GraphQL · Python**

```python
class ApiError(Exception):
    """An error the API answered: the HTTP status, and the code and name of api_error when the
    error has one (a document the schema refuses has none)."""

    def __init__(self, status, code, name, message, error):
        super().__init__(f"HTTP {status}, code {code} {name}: {message}")
        self.status, self.code, self.name, self.error = status, code, name, error


def call(query, variables=None):
    """One GraphQL request. On HTTP 429 it waits as long as Retry-After says (5 seconds when the
    header is missing) and tries again, three attempts in all. Any error is an ApiError."""
    for attempt in range(1, 4):
        response = session.post(ENDPOINT, json={"query": query, "variables": variables or {}}, timeout=30)
        if response.status_code == 429 and attempt < 3:
            time.sleep(float(response.headers.get("Retry-After") or 5))
            continue
        body = response.json()
        error = (body.get("errors") or [None])[0]
        # The message is for people; branch on the numeric code.
        if not response.ok or error:
            api_error = (error or {}).get("api_error") or {}
            message = str((error or {}).get("message")).strip()
            raise ApiError(response.status_code, api_error.get("code"), api_error.get("name"), message, error)
        return body["data"]
```

In your own client:

- Retry HTTP 429 after `Retry-After`. When the header is missing, back off exponentially, such as
  1, 2, 4 and 8 seconds, with a cap.
- Retry HTTP 500 and network errors with back-off too, but only reads, or writes you can check:
  a write that failed with 500 may have been applied. Read the record back before you send it
  again.
- Never retry other 4xx answers unchanged. A 400 or 422 fails the same way the next time; fix the
  request.
- Spread large jobs out instead of sending them in parallel, and use
  [batches](/concepts/batch-operations/) of up to 100 records to make fewer requests.

## GraphQL operation size

One operation on the public GraphQL endpoint holds at most 200 mutations and at most 500
aliased fields; a fragment's aliases count at every place it is spread. A larger operation is
refused before any of it runs, with HTTP 400 and only `errors[].message`, such as
`Too many mutations in one operation: 201 (limit 200). Split them into several requests.` Split
the work into several requests, or write many records with one `bulkUpdate…` mutation
([batch operations](/concepts/batch-operations/)).

## Plan limits

The number of API calls a space may make each month depends on its Coevera plan: see
[plans and limits](/get-started/plans-and-limits/#api-calls-per-month), which links the pricing
page. The MCP server has limits of its own: see [MCP limits and errors](/ai/limits-and-errors/).

## Limits

| Limit               | Value                             | Details                                                                              |
| ------------------- | --------------------------------- | ------------------------------------------------------------------------------------ |
| Requests per minute | Not published                     | A throttled request answers HTTP 429, with `Retry-After` when the limit computes one |
| API calls per month | Per plan                          | See [plans and limits](/get-started/plans-and-limits/#api-calls-per-month)           |
| Failed sign-ins     | Throttled                         | Stop at the first 401; a locked sign-in answers 429 without `Retry-After`            |
| GraphQL operation   | 200 mutations, 500 aliased fields | A larger operation answers HTTP 400                                                  |
