> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keebai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits and errors

> Usage limits per project, error response format, and best practices for building reliable integrations against the Keebai public API.

## Rate limits

Limits apply **per project** (not per token, not per user, not per IP) and are shared across **every endpoint** — the quota is one pool, so calls to `/v1/messages` and `/v1/crm/tickets` draw from the same budget.

| Window      | Limit              | Purpose                           |
| ----------- | ------------------ | --------------------------------- |
| Per minute  | 60 requests        | Burst protection                  |
| Per hour    | 200 requests       | Keeps a burst from eating the day |
| **Per day** | **1,000 requests** | **Hard cap**                      |

The three windows stack, and the tightest one wins. The daily cap is the one that matters: at a steady pace it is roughly **40 requests per hour**, which is what the hourly window is sized around. The minute window lets a batch job move at 60 a minute for a short stretch; the hourly window is what stops that stretch from spending the whole day in a few minutes.

<Warning>
  **1,000 per day is a hard ceiling per project, not a soft target.** Once you hit it, every request returns `429` until the window rolls over. Size your polling intervals and batch jobs against it before you go to production.

  **Minting a second token does not double your quota.** All tokens issued against the same project share one budget. To isolate a noisy job from your transactional path, put them in **separate projects**.
</Warning>

If you cross a limit:

```http theme={null}
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
```

```json theme={null}
{
  "error": {
    "code": "THROTTLED",
    "message": "Rate limit exceeded: 1000 requests per day per project. Quota resets in 7h 12m."
  }
}
```

The message names the window you hit and when it frees up, so you can tell a one-minute burst apart from a spent hourly allowance or a spent daily budget.

<Note>
  **The [MCP server](/dev/mcp/overview) is outside these limits.** Tool calls on `POST /v1/mcp` are not counted against the per-minute or per-day quota. That is convenient and also a footgun: an agent stuck in a loop generates real upstream traffic with nothing stopping it, so cap it on your side.
</Note>

### Read the headers instead of waiting for the 429

Every response carries the state of each window, so you can self-throttle before you get blocked:

| Header                      | Meaning                                         |
| --------------------------- | ----------------------------------------------- |
| `X-RateLimit-Limit-day`     | Your daily cap.                                 |
| `X-RateLimit-Remaining-day` | Requests left in the current window.            |
| `X-RateLimit-Reset-day`     | Seconds until the window rolls over.            |
| `Retry-After-day`           | Only on `429`. Seconds to wait before retrying. |

The same trio exists for `-minute` and `-hour`. A healthy integration watches `X-RateLimit-Remaining-day` and slows itself down as it approaches zero, rather than sprinting into a wall.

Every window is **rolling**, not aligned to a clock boundary: each one opens on your first request and closes a minute, an hour or 24 hours later.

<Tip>
  Need more throughput (e.g. bulk-syncing a CRM with hundreds of thousands of contacts)? Email [support@keebai.com](mailto:support@keebai.com) with your expected volume and we'll raise the cap on your project.
</Tip>

### What does not count

Requests to `/health` are exempt. Broadcasts count as **one** request each, not one per recipient — [`POST /v1/messages/bulk`](/dev/endpoints/messages-bulk) with 5,000 recipients costs a single unit, which makes it far cheaper than looping over [`POST /v1/messages`](/dev/endpoints/messages-send).

## How to handle 429 properly

Retrying a minute-window `429` and retrying a day-window `429` are different problems. Exponential backoff over a few seconds clears the first and does nothing for the second — if your daily budget is gone, no amount of waiting inside the request path will bring it back.

Read `Retry-After-<window>` and decide:

```ts theme={null}
const DAY = 60 * 60; // anything longer than an hour is not worth blocking on

async function callKeebai(path: string, attempt = 0): Promise<Response> {
  const response = await fetch(`https://api.keebai.com/v1${path}`, {
    headers: { Authorization: `Bearer ${process.env.KEEBAI_API_TOKEN}` },
  });

  if (response.status !== 429 || attempt >= 5) return response;

  const retryAfter = Number(
    response.headers.get("Retry-After-day") ??
      response.headers.get("Retry-After-minute") ??
      0,
  );

  // Daily quota spent: stop, alert, and resume after the window rolls over.
  if (retryAfter > DAY) {
    throw new DailyQuotaExhausted(retryAfter);
  }

  const delay = Math.min(retryAfter * 1000, 2 ** attempt * 1000) + Math.random() * 500;
  await new Promise((r) => setTimeout(r, delay));
  return callKeebai(path, attempt + 1);
}
```

Don't tight-loop retries — you'll burn the rest of the quota on requests that were never going to succeed.

### Staying under 1,000 a day

<CardGroup cols={2}>
  <Card title="Prefer webhooks over polling" icon="bell">
    A poll every minute is 1,440 requests a day — over budget before you send anything. [Subscribe to events](/dev/webhooks/overview) and let Keebai push instead.
  </Card>

  <Card title="Batch your sends" icon="layer-group">
    One [bulk broadcast](/dev/endpoints/messages-bulk) costs one request regardless of recipient count. A loop over 500 contacts costs 500.
  </Card>

  <Card title="Cache what rarely changes" icon="database">
    Channels, templates, branches, and services change on a human timescale. Fetch them once at boot, not once per message.
  </Card>

  <Card title="One project per workload" icon="key">
    The quota is per project, so tokens are not how you isolate. Give a noisy batch job its own project and it cannot starve your transactional path.
  </Card>
</CardGroup>

## Error format

Every error is wrapped in the same envelope, with a stable machine-readable `code` and, on some errors, a `details` object:

```json theme={null}
{
  "error": {
    "code": "<STABLE_SLUG>",
    "message": "<human description>"
  }
}
```

Branch on `code`, never on `message` — the wording can change, the slug will not.

| Status                      | Typical `code`             | When                                                                                                                       |
| --------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`           | `Bad Request`              | Invalid body, malformed query params, or a property the endpoint does not accept.                                          |
| `401 Unauthorized`          | `Unauthorized`             | Token invalid, missing, revoked, or expired. See [Authentication](/dev/authentication).                                    |
| `403 Forbidden`             | `INSUFFICIENT_SCOPE`       | Token is valid but lacks the required scope. Includes `details.required` and `details.missing`. See [Scopes](/dev/scopes). |
| `404 Not Found`             | `Not Found`                | Resource or route doesn't exist.                                                                                           |
| `429 Too Many Requests`     | `THROTTLED`                | You hit a rate limit. See above.                                                                                           |
| `500 Internal Server Error` | `Internal Server Error`    | Server-side bug. If it persists, [report it](mailto:support@keebai.com).                                                   |
| `502 Bad Gateway`           | `<SERVICE>_UPSTREAM_ERROR` | A downstream service was unreachable. Retry with backoff.                                                                  |
| `503 Service Unavailable`   | `Service Unavailable`      | Maintenance or temporary degradation. Retry with backoff.                                                                  |

## Best practices

<CardGroup cols={2}>
  <Card title="Idempotency" icon="repeat">
    Design retries to be safe. `GET` and `DELETE` are idempotent out of the box; for `POST`/`PATCH`, dedupe on your side with stable identifiers.
  </Card>

  <Card title="Sensible timeouts" icon="clock">
    Set a client timeout between 10 and 30 seconds. Lower kills legitimate requests; higher locks your thread on transient slowness.
  </Card>

  <Card title="Log everything useful" icon="file-lines">
    Log status, request id (when the API returns one in headers), and duration. Future-you will thank you.
  </Card>

  <Card title="Circuit breaker" icon="bolt-slash">
    On repeated 5xx, pause requests for a few seconds. Your system stays up for everything else that doesn't depend on Keebai.
  </Card>
</CardGroup>

## In production

* **Monitor** latency, error rate, and status codes. An alert on error rate `>5%` catches issues before users do.
* **Rotate tokens** every 90 days, incident or not. Mint, deploy, validate, revoke the old.
* **Audit *Last used*** in the tokens table periodically. Tokens unused for months are revoke candidates.
* **Suspect us?** Check **[status.keebai.com](https://status.keebai.com)** first — it reports availability per component and any open incident, and it runs outside our cluster so it stays up when the API does not. If nothing is reported there, email [support@keebai.com](mailto:support@keebai.com).
