Errors & Rate Limits

REST error status codes, the 400 validation issues shape, 422 operation errors, and the 120-requests-per-minute rate limit.


Errors from the Blue REST API come back as a JSON body with an error field and a matching HTTP status code. The status tells you what kind of failure it was; the error value carries a human-readable message, and validation failures add an issues array. Always branch on the status code first.

Error status codes

StatusMeaningWhen
400Bad RequestThe input is invalid - a missing required field, a wrong type, an out-of-range value. Includes an issues array.
401UnauthorizedThe Bearer token is missing, malformed, expired, or doesn’t match a real token.
403ForbiddenThe token is valid but the user has no access to the company in x-bloo-company-id.
422UnprocessableThe request was well-formed but the operation failed its own validation (an operation error).
429Too Many RequestsYou exceeded the rate limit. Retry after a short wait.
500Server ErrorSomething went wrong on Blue’s side. Safe to retry.

Two cases are worth calling out: a missing x-bloo-company-id header is the most common cause of a 400 on an otherwise well-formed request, and requests from sanctioned regions are rejected up front with a 403 and { "error": "Service not available in your region" } before any operation runs.

Every error body has the same outer shape:

{
  "error": "A human-readable message describing what went wrong."
}

Validation errors (400)

A 400 means the input didn’t pass schema validation. The body carries the standard error message plus an issues array - one entry per problem - produced by the request validator. Each issue names the offending path, the failure code, and a message:

{
  "error": "Invalid input",
  "issues": [
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "undefined",
      "path": ["title"],
      "message": "Required"
    },
    {
      "code": "invalid_type",
      "expected": "boolean",
      "received": "string",
      "path": ["done"],
      "message": "Expected boolean, received string"
    }
  ]
}

path is an array because it can point into nested input - for example ["customFields", 0, "value"] for the value of the first custom field. Read issues to surface field-level errors to the user; the top-level error is a summary.

Operation errors (422)

A 422 is different from a 400: the request was structurally valid - it passed input validation - but the operation itself couldn’t complete, so it returned its own validation message. This is the status for business-rule failures, like trying to move a record into a list that doesn’t accept it, or an operation-specific constraint:

{
  "error": "Target list is full and cannot accept more records."
}

Treat 400 as “fix your request shape” and 422 as “the request was understood but rejected for a reason specific to this operation.”

Rate limits (429)

Each user is limited to 120 requests per minute. The limit is keyed to the user, not the token, so multiple tokens belonging to the same user share one budget. Exceeding it returns a 429 with a JSON body:

{
  "error": "Rate limit exceeded. Maximum 120 requests per minute."
}

When you hit a 429, back off briefly and retry. To stay under the limit, batch where the API allows it, page with the largest limit (see Pagination) to make fewer calls, and avoid tight polling loops - poll on an interval rather than as fast as the client can fire.

Build in retries

A robust client retries on 429 (after a short wait) and 500 (transient server errors), but never on 400/401/403/422 - those won’t succeed on retry without changing the request or the token. An exponential backoff on the retryable statuses keeps an integration resilient without hammering the API.