API errors and rate limits
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?
Section titled “What does an API error look like?”Every error body is one object named error, with a code and a message:
{ "error": { "code": "NOT_FOUND", "message": "Contact not found" } }codeis a stable code in capital letters. Build your program on it.messageis an English explanation for developers. Its wording may change, so never parse it.issuesandissueCountappear only on a 400 for an invalid query parameter:issueslists the first 20 problems found, each naming the parameter inpath, andissueCountsays how many there were in all.
{ "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?
Section titled “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 |
| 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?
Section titled “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?
Section titled “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/1.1 429 Too Many RequestsRetry-After: 60X-RateLimit-Limit: 60X-RateLimit-Remaining: 0X-RateLimit-Reset: 17Content-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:
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)); }}import osimport timeimport 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?
Section titled “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?
Section titled “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
limitto 100 on lists, so each request reads as much as it can. See cursor pagination. - For customers, use
sinceto ask for new ones only; for bookings, usefromandtoto ask for the days you care about. - If you poll the API every minute to find what is new, use 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.