Skip to content

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.

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

{ "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.
{
"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
}
}

These are all the codes the API returns:

HTTPcodeWhenWhat to do
400VALIDATION_ERRORAn unknown parameter, a limit out of range, an invalid date, from after to, an unknown status, or a cursor that cannot be readFix the request; do not resend it as is
401UNAUTHORIZEDNo key, or a key that is invalid, revoked or expired, or a business account that is not activeSee API keys
403FORBIDDENThe key is valid but lacks contacts.read or bookings.readCreate a key with the scope you need
404NOT_FOUNDAn id that is not in your business, a malformed id, or a path that does not existCheck the id and the path
429RATE_LIMITEDYou went over a rate limitWait, then retry
500INTERNALAn unexpected server errorRetry 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.

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:

HeaderMeaning
X-RateLimit-LimitRequests the key may make per minute
X-RateLimit-RemainingWhat the key has left in the current minute
X-RateLimit-ResetSeconds 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.

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 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:

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 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)

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.

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.
  • 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 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.