# API errors and rate limits

> The Mujib API's rate limits per key, the shape and code of every error, the X-RateLimit headers, what to do on a 429, and which errors are worth a retry.

The Mujib API's rate limits are 60 requests a minute per key, and every error comes back the same way: an HTTP status and a JSON body with a stable `code` and a `message` that explains it. This page lists every code, what to do about each, and how to stay inside the limits.

## What does an API error look like?

Every error body is one object named `error`, with a `code` and a `message`:

```json
{ "error": { "code": "NOT_FOUND", "message": "Contact not found" } }
```

- **`code`** is a stable code in capital letters. Build your program on it.
- **`message`** is an English explanation for developers. Its wording may change, so never parse it.
- **`issues`** and **`issueCount`** appear only on a 400 for an invalid query parameter: `issues` lists the first 20 problems found, each naming the parameter in `path`, and `issueCount` says how many there were in all.

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "issues": [
      { "code": "too_big", "maximum": 100, "type": "number", "inclusive": true, "exact": false, "message": "Number must be less than or equal to 100", "path": ["limit"] }
    ],
    "issueCount": 1
  }
}
```

## What does each error code mean?

These are all the codes the API returns:

| HTTP | `code` | When | What to do |
| --- | --- | --- | --- |
| 400 | `VALIDATION_ERROR` | An unknown parameter, a `limit` out of range, an invalid date, `from` after `to`, an unknown `status`, or a cursor that cannot be read | Fix the request; do not resend it as is |
| 401 | `UNAUTHORIZED` | No key, or a key that is invalid, revoked or expired, or a business account that is not active | See [API keys](https://docs.mujib.ai/en/developers/authentication/) |
| 403 | `FORBIDDEN` | The key is valid but lacks `contacts.read` or `bookings.read` | Create a key with the scope you need |
| 404 | `NOT_FOUND` | An id that is not in your business, a malformed id, or a path that does not exist | Check the id and the path |
| 429 | `RATE_LIMITED` | You went over a rate limit | Wait, then retry |
| 500 | `INTERNAL` | An unexpected server error | Retry a little later |

An id that belongs to another business returns the very same 404 as an id that does not exist, so a response never reveals anything about anyone else's data.

## What are the Mujib API rate limits?

Each key may make 60 requests in every minute, and the count starts over at the start of the next minute. Every request with a valid key counts, including one that ends in a 400, 403 or 404.

Every response to a valid key carries three headers about that key's budget:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests the key may make per minute |
| `X-RateLimit-Remaining` | What the key has left in the current minute |
| `X-RateLimit-Reset` | Seconds until the key's minute starts over |

Above the per-key limit sits a wider limit per network address, shared by every request from the same address, whatever the key. When you reach it, the headers of that 429 describe that limit, not your key's. On a 401, do not read the headers as your key's budget.

## What do I do on a 429?

Wait the number of seconds in the `Retry-After` header, then send the same request again. This is what the response looks like when a key has used up its minute:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 17
Content-Type: application/json; charset=utf-8

{"error":{"code":"RATE_LIMITED","message":"At most 60 requests a minute per key."}}
```

`X-RateLimit-Reset` says when the new minute really starts, and it is usually shorter than `Retry-After`. Waiting for either is enough. A function that waits and retries:

```js
async function getWithRetry(url, tries = 3) {
  for (let i = 0; ; i++) {
    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.MUJIB_API_KEY}` } });
    if (res.status !== 429 || i === tries - 1) return res;
    const wait = Number(res.headers.get('x-ratelimit-reset') ?? res.headers.get('retry-after') ?? '60');
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
}
```

```python
import os
import time
import requests

def get_with_retry(url, params=None, tries=3):
    for i in range(tries):
        res = requests.get(url, params=params, timeout=30,
                           headers={"Authorization": f"Bearer {os.environ['MUJIB_API_KEY']}"})
        if res.status_code != 429 or i == tries - 1:
            return res
        wait = int(res.headers.get("X-RateLimit-Reset") or res.headers.get("Retry-After") or 60)
        time.sleep(wait)
```

## Which errors are worth a retry?

Retry on 429 and 500 only; in every other case, fix the request.

- **429:** wait as above, then retry.
- **500:** retry after a few seconds, and double the wait if it happens again.
- **400, 401, 403 and 404:** sending the same request again returns the same error and spends your budget. Fix the parameter, the key or the id first.

## How do I stay inside the rate limits?

Ask for bigger pages, ask only for what changed, and let Mujib tell you about events.

- Set `limit` to 100 on lists, so each request reads as much as it can. See [cursor pagination](https://docs.mujib.ai/en/developers/pagination/).
- For customers, use `since` to ask for new ones only; for bookings, use `from` and `to` to ask for the days you care about.
- If you poll the API every minute to find what is new, use [webhooks](https://docs.mujib.ai/en/developers/webhooks/) instead.
- Give each system its own key, since each key has its own budget. Remember that requests from one server share the per-address limit.

## Frequently asked questions

### Do requests that fail count toward the limit?

Yes. Every request with a valid key counts, even one that ends in a 400, 403 or 404. A request whose key is refused with a 401 does not count against the key.

### Does each key have its own limit?

Yes. Each key gets 60 requests a minute, and one key never uses another key's budget. All requests from the same network address also share a wider limit.

### Should my code read the error message?

No. Build on the code, which is stable. The message is an English explanation for developers and may change.

### When does the count start over?

At the start of every minute. The X-RateLimit-Reset header says how many seconds are left until then.

---

Source: https://docs.mujib.ai/en/developers/errors-limits/ · Last updated: 2026-11-04
