---
title: Errors
description: Every REST error has the same JSON shape. Branch on the numeric code, not on the message or the HTTP status alone; a missing permission answers 400, not 403.
canonical: https://developers.coevera.com/concepts/errors/
section: concepts
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Errors

**Answer:** REST errors come back as { success: false, http_status, code, name, message }. code and name identify the error; message explains it to a person. Validation errors, HTTP 422 with code 40000, add the record and the fields that failed. A missing permission answers 400 with code 50, never 403, so branch on code rather than on the status alone.

When a request fails, the status line says roughly what went wrong and the body says exactly what.
Read the body.

## The error envelope

Asking for more than 100 records per page is refused with HTTP 400:

**Recorded response · REST · 400 Bad Request**

```json
{
  "code": 117,
  "name": "ERROR_PARAMETER_INVALID",
  "message": "Limit must not be greater than 100",
  "http_status": 400,
  "success": false
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "errors": [
    {
      "message": "On attribute id: Invalid attribute value \"not-a-uuid\"",
      "locations": [
        {
          "line": 1,
          "column": 24
        }
      ],
      "path": [
        "entities",
        "account",
        "getById"
      ],
      "api_error": {
        "code": 117,
        "name": "ERROR_PARAMETER_INVALID",
        "message": "On attribute id: Invalid attribute value \"not-a-uuid\""
      }
    }
  ],
  "data": {
    "entities": {
      "account": {
        "getById": null
      }
    }
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

| Field         | Holds                                                                                         |
| ------------- | --------------------------------------------------------------------------------------------- |
| `success`     | `false`                                                                                       |
| `http_status` | The HTTP status, repeated                                                                     |
| `code`        | The error's number, such as `117`                                                             |
| `name`        | The error's name, such as `ERROR_PARAMETER_INVALID`                                           |
| `message`     | A sentence for people. Validation messages start with a newline: trim it before you show one. |

Permission errors also carry `perm`, the permission that is missing. Grant the permission as
[permissions and scopes](/get-started/permissions-and-scopes/#what-a-missing-permission-answers)
describes.

### Validation errors

HTTP 422 with code 40000 `ERROR_ENTITY_VALIDATION` adds what failed: `entity_name`, `entity_id`
and `entity_index`, then `entity_errors` for the record as a whole and `field_errors` for single
fields (each with code 40002 and its own `errors`), and `step_checklist_errors`. Errors of related
records nest the same way. Here an account is created with an owner that does not exist:

**Recorded response · REST · 422 Unprocessable Entity**

```json
{
  "code": 40000,
  "name": "ERROR_ENTITY_VALIDATION",
  "message": "\nRelated Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
  "entity_id": "0a000000-0000-4000-a000-000000000001",
  "entity_name": "Account",
  "entity_errors": [],
  "field_errors": [
    {
      "field_id": "10000000-0000-4000-a000-000000000001",
      "field_name": "owner_id",
      "name": "ERROR_FIELD_VALIDATION",
      "code": 40002,
      "errors": [
        {
          "code": 120,
          "name": "ERROR_RELATIONS_DOESNT_EXIST",
          "message": "Related Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
          "field_id": "10000000-0000-4000-a000-000000000001",
          "field_name": "owner_id"
        }
      ]
    }
  ],
  "step_checklist_errors": [],
  "entity_index": null,
  "http_status": 422,
  "success": false
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "errors": [
    {
      "message": "\nRelated Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createAccount"
      ],
      "api_error": {
        "code": 40000,
        "name": "ERROR_ENTITY_VALIDATION",
        "message": "\nRelated Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
        "entity_id": "0a000000-0000-4000-a000-000000000001",
        "entity_name": "Account",
        "entity_errors": [],
        "field_errors": [
          {
            "field_id": "10000000-0000-4000-a000-000000000001",
            "field_name": "owner_id",
            "name": "ERROR_FIELD_VALIDATION",
            "code": 40002,
            "errors": [
              {
                "code": 120,
                "name": "ERROR_RELATIONS_DOESNT_EXIST",
                "message": "Related Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
                "field_id": "10000000-0000-4000-a000-000000000001",
                "field_name": "owner_id"
              }
            ]
          }
        ],
        "step_checklist_errors": [],
        "entity_index": null
      }
    }
  ],
  "data": {
    "createAccount": null
  }
}
```

_Recorded on 2026-10-15 against API 6.5.2 in the docs sandbox, de-identified._

In a batch sent with the default rollback, `entity_index` is `null`: the error does not say which
item failed ([batch operations](/concepts/batch-operations/#by-default-all-or-nothing)).

## HTTP statuses

| HTTP | When                                                                                                                                           | Typical codes                                       |
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| 200  | Success, including a batch with failed items sent with `rollback-method=1`                                                                     | See [batch operations](/concepts/batch-operations/) |
| 201  | A record was created                                                                                                                           |                                                     |
| 202  | A write was queued for later with the `delay` parameter                                                                                        |                                                     |
| 400  | A bad or unknown parameter, input the API cannot store, a missing permission, conflicting data                                                 | `117`, `118`, `50`, `52`, `2002`                    |
| 401  | Authentication failed: see [authentication](/get-started/authentication/)                                                                      | `ERROR_UNAUTHORIZED`                                |
| 404  | No such record, or no such entity. By default, a `batch-delete` with an unknown ID gets 404 for the whole call.                                | `143`, `2006`                                       |
| 422  | Validation failed, or a required value is missing. By default, a batch with a failed item gets 422 for the whole call, and nothing is written. | `40000`, `119`                                      |
| 429  | Too many requests: wait for `Retry-After`, see [rate limits](/concepts/rate-limits-and-retries/)                                               | `182`                                               |
| 500  | Unexpected server error                                                                                                                        | `1000`                                              |

A method a path does not have, such as `PATCH /entities/{id}` without the entity name, answers
HTTP 405 with a body of another shape, `{ "status": 405, "message": "…" }`.

> **The REST API never answers 403 or 409:** A missing permission is HTTP 400 with code 50 `ERROR_NOT_ENOUGH_PERMISSIONS`, and a revision or
> data conflict is HTTP 400 too, code 2002 `ERROR_CONFLICTING_DATA`. Check `code`, not just the
> status.

## GraphQL errors

GraphQL answers HTTP 200 for a request it could run, with the failures in `errors`. When the API
refused part of it, `errors[].api_error` carries the same body as a REST error, without
`success` and `http_status`: code 117 for an ID that is not a UUID, and code 40000 with
`field_errors` for a mutation that fails validation. Pick GraphQL in the two responses above to
see them.

A query that does not match the schema, such as a field name with a typo, answers HTTP 400 with
only `errors[].message`, and so does an operation over the
[GraphQL operation limits](/concepts/rate-limits-and-retries/#graphql-operation-size). Reading a
record that does not exist with `getById` returns `null`, not an error.

## Handle errors in code

Raise an error that carries `code`, and decide on it: retry what can be retried, report the rest.
This helper, used on every request, also waits and retries on HTTP 429:

**REST · cURL**

```bash
# call METHOD PATH [BODY]: one REST request; prints the body. On HTTP 429 it waits as long as
# Retry-After says (5 seconds when the header is missing) and tries again, three attempts in
# all. On any other error it prints "HTTP <status>, code <code> <name>: <message>" and the
# body, sets ERROR_CODE to the code and returns 1.
call() {
  local attempt headers response status delay
  local args=(-X "$1" "$COEVERA_BASE$2" -u "$COEVERA_API_KEY:$COEVERA_API_SECRET")
  if [ $# -ge 3 ]; then args+=(-H "Content-Type: application/json" -d "$3"); fi
  headers=$(mktemp)
  for attempt in 1 2 3; do
    response=$(curl -sS -D "$headers" -w '\n%{http_code}' "${args[@]}")
    status=${response##*$'\n'}
    response=${response%$'\n'*}
    if [ "$status" = 429 ] && [ "$attempt" -lt 3 ]; then
      delay=$(sed -n 's/^[Rr]etry-[Aa]fter: *\([0-9][0-9]*\).*/\1/p' "$headers")
      sleep "${delay:-5}"
      continue
    fi
    rm -f "$headers"
    if [ "$status" -ge 400 ]; then
      # The message is for people; branch on the numeric code.
      jq -r --arg status "$status" \
        '"HTTP \($status), code \(.code) \(.name): \(.message | sub("^\\s+"; "") | sub("\\s+$"; ""))"' <<<"$response"
      echo "$response"
      ERROR_CODE=$(jq -r '.code' <<<"$response")
      return 1
    fi
    echo "$response"
    return 0
  done
}
```

**REST · TypeScript**

```ts
// An error the API answered: the HTTP status, and the code, name and message of its body.
class ApiError extends Error {
  constructor(
    readonly status: number,
    readonly code: number,
    readonly errorName: string,
    message: string,
    readonly body: unknown,
  ) {
    super(`HTTP ${status}, code ${code} ${errorName}: ${message}`);
  }
}

const sleep = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

// call: one REST request. On HTTP 429 it waits as long as Retry-After says (5 seconds when the
// header is missing) and tries again, three attempts in all. Any other error is an ApiError.
async function call(path: string, init: { method?: string; body?: unknown } = {}) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`${base}${path}`, {
      method: init.method ?? "GET",
      headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
      body: init.body === undefined ? undefined : JSON.stringify(init.body),
    });
    if (res.status === 429 && attempt < 3) {
      await sleep(Number(res.headers.get("Retry-After")) || 5);
      continue;
    }
    const body = await res.json();
    // The message is for people; branch on the numeric code.
    if (!res.ok) throw new ApiError(res.status, body.code, body.name, String(body.message).trim(), body);
    return body;
  }
}
```

**REST · Python**

```python
class ApiError(Exception):
    """An error the API answered: the HTTP status, and the code, name and message of its body."""

    def __init__(self, status, body):
        self.status, self.body = status, body
        self.code, self.name = body.get("code"), body.get("name")
        message = str(body.get("message")).strip()
        super().__init__(f"HTTP {status}, code {self.code} {self.name}: {message}")


def call(method, path, **kwargs):
    """One REST request. On HTTP 429 it waits as long as Retry-After says (5 seconds when the
    header is missing) and tries again, three attempts in all. Any other error is an ApiError."""
    for attempt in range(1, 4):
        response = session.request(method, f"{BASE}{path}", timeout=30, **kwargs)
        if response.status_code == 429 and attempt < 3:
            time.sleep(float(response.headers.get("Retry-After") or 5))
            continue
        body = response.json()
        # The message is for people; branch on the numeric code.
        if not response.ok:
            raise ApiError(response.status_code, body)
        return body
```

**GraphQL · cURL**

```bash
# call REQUEST: POST one GraphQL request (a JSON body); prints the data. On HTTP 429 it waits as
# long as Retry-After says (5 seconds when the header is missing) and tries again, three
# attempts in all. On any error it prints "HTTP <status>, code <code> <name>: <message>" and
# the error, sets ERROR_CODE to the api_error code (null when there is none) and returns 1.
call() {
  local attempt headers response status delay
  headers=$(mktemp)
  for attempt in 1 2 3; do
    response=$(curl -sS -D "$headers" -w '\n%{http_code}' "$COEVERA_GRAPHQL" \
      -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
      -H "Content-Type: application/json" \
      -d "$1")
    status=${response##*$'\n'}
    response=${response%$'\n'*}
    if [ "$status" = 429 ] && [ "$attempt" -lt 3 ]; then
      delay=$(sed -n 's/^[Rr]etry-[Aa]fter: *\([0-9][0-9]*\).*/\1/p' "$headers")
      sleep "${delay:-5}"
      continue
    fi
    rm -f "$headers"
    if [ "$status" != 200 ] || jq -e '.errors' <<<"$response" >/dev/null; then
      # The message is for people; branch on the numeric code.
      jq -r --arg status "$status" '.errors[0]
        | "HTTP \($status), code \(.api_error.code) \(.api_error.name // ""): \(.message | sub("^\\s+"; "") | sub("\\s+$"; ""))"' <<<"$response"
      jq '.errors[0]' <<<"$response"
      ERROR_CODE=$(jq -r '.errors[0].api_error.code' <<<"$response")
      return 1
    fi
    jq '.data' <<<"$response"
    return 0
  done
}
```

**GraphQL · TypeScript**

```ts
// An error the API answered: the HTTP status, and the code and name of api_error when the
// error has one (a document the schema refuses has none).
class ApiError extends Error {
  constructor(
    readonly status: number,
    readonly code: number | undefined,
    readonly errorName: string | undefined,
    message: string,
    readonly error: unknown,
  ) {
    super(`HTTP ${status}, code ${code ?? "none"} ${errorName ?? ""}: ${message}`);
  }
}

const sleep = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

// call: one GraphQL request. On HTTP 429 it waits as long as Retry-After says (5 seconds when
// the header is missing) and tries again, three attempts in all. Any error is an ApiError.
async function call(query: string, variables: object = {}) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(endpoint, {
      method: "POST",
      headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
      body: JSON.stringify({ query, variables }),
    });
    if (res.status === 429 && attempt < 3) {
      await sleep(Number(res.headers.get("Retry-After")) || 5);
      continue;
    }
    const body = await res.json();
    const error = body.errors?.[0];
    // The message is for people; branch on the numeric code.
    if (!res.ok || error) {
      throw new ApiError(res.status, error?.api_error?.code, error?.api_error?.name, String(error?.message).trim(), error);
    }
    return body.data;
  }
}
```

**GraphQL · Python**

```python
class ApiError(Exception):
    """An error the API answered: the HTTP status, and the code and name of api_error when the
    error has one (a document the schema refuses has none)."""

    def __init__(self, status, code, name, message, error):
        super().__init__(f"HTTP {status}, code {code} {name}: {message}")
        self.status, self.code, self.name, self.error = status, code, name, error


def call(query, variables=None):
    """One GraphQL request. On HTTP 429 it waits as long as Retry-After says (5 seconds when the
    header is missing) and tries again, three attempts in all. Any error is an ApiError."""
    for attempt in range(1, 4):
        response = session.post(ENDPOINT, json={"query": query, "variables": variables or {}}, timeout=30)
        if response.status_code == 429 and attempt < 3:
            time.sleep(float(response.headers.get("Retry-After") or 5))
            continue
        body = response.json()
        error = (body.get("errors") or [None])[0]
        # The message is for people; branch on the numeric code.
        if not response.ok or error:
            api_error = (error or {}).get("api_error") or {}
            message = str((error or {}).get("message")).strip()
            raise ApiError(response.status_code, api_error.get("code"), api_error.get("name"), message, error)
        return body["data"]
```

Then decide on the code. A validation error is not worth retrying: report the field that failed.

**REST · cURL**

```bash
# An owner that does not exist: HTTP 422, code 40000. field_errors names the field that failed.
# Nothing is written.
ACCOUNT='{ "name": "Northwind Analytics", "owner_id": "00000000-0000-4000-a000-000000000000" }'
if ! call POST "/entities/Accounts" "$ACCOUNT" && [ "$ERROR_CODE" != 40000 ]; then
  exit 1
fi
```

**REST · TypeScript**

```ts
// An owner that does not exist: HTTP 422, code 40000. field_errors names the field that failed.
// Nothing is written.
try {
  await call("/entities/Accounts", {
    method: "POST",
    body: { name: "Northwind Analytics", owner_id: "00000000-0000-4000-a000-000000000000" },
  });
} catch (err) {
  if (!(err instanceof ApiError) || err.code !== 40000) throw err;
  console.log(err.message, JSON.stringify(err.body, null, 2));
}
```

**REST · Python**

```python
# An owner that does not exist: HTTP 422, code 40000. field_errors names the field that failed.
# Nothing is written.
try:
    call(
        "POST",
        "/entities/Accounts",
        json={"name": "Northwind Analytics", "owner_id": "00000000-0000-4000-a000-000000000000"},
    )
except ApiError as err:
    if err.code != 40000:
        raise
    print(err, json.dumps(err.body, indent=2))
```

**GraphQL · cURL**

```bash
# An owner that does not exist: HTTP 200 with errors, api_error code 40000. Its field_errors
# name the field that failed. Nothing is written.
CREATE=$(jq -n '{
  query: "mutation Create($input: CreateAccountInput!) { createAccount(input: $input) { account { id } } }",
  variables: { input: { name: "Northwind Analytics", ownerId: "00000000-0000-4000-a000-000000000000" } }
}')
if ! call "$CREATE" && [ "$ERROR_CODE" != 40000 ]; then
  exit 1
fi
```

**GraphQL · TypeScript**

```ts
// An owner that does not exist: HTTP 200 with errors, api_error code 40000. Its field_errors
// name the field that failed. Nothing is written.
try {
  await call(
    `mutation Create($input: CreateAccountInput!) {
  createAccount(input: $input) { account { id } }
}`,
    { input: { name: "Northwind Analytics", ownerId: "00000000-0000-4000-a000-000000000000" } },
  );
} catch (err) {
  if (!(err instanceof ApiError) || err.code !== 40000) throw err;
  console.log(err.message, JSON.stringify(err.error, null, 2));
}
```

**GraphQL · Python**

```python
# An owner that does not exist: HTTP 200 with errors, api_error code 40000. Its field_errors
# name the field that failed. Nothing is written.
try:
    call(
        """
mutation Create($input: CreateAccountInput!) {
  createAccount(input: $input) { account { id } }
}
""",
        {"input": {"name": "Northwind Analytics", "ownerId": "00000000-0000-4000-a000-000000000000"}},
    )
except ApiError as err:
    if err.code != 40000:
        raise
    print(err, json.dumps(err.error, indent=2))
```

## Codes you meet most

The [error-code reference](/reference/errors/) lists every code of the current release with its
name and HTTP status. These are the ones integrations meet most:

| Code                                   | Name                           | HTTP | Meaning                                                                                                         |
| -------------------------------------- | ------------------------------ | ---- | --------------------------------------------------------------------------------------------------------------- |
| [50](/reference/errors/#code-50)       | `ERROR_NOT_ENOUGH_PERMISSIONS` | 400  | The application lacks a permission                                                                              |
| [117](/reference/errors/#code-117)     | `ERROR_PARAMETER_INVALID`      | 400  | A parameter or value is invalid; also a repeated query key, a page size over 100 and input the API cannot store |
| [118](/reference/errors/#code-118)     | `ERROR_PARAMETER_UNKNOWN`      | 400  | A parameter the endpoint does not take                                                                          |
| [119](/reference/errors/#code-119)     | `ERROR_PARAMETER_MISSING`      | 422  | A required value is missing                                                                                     |
| [120](/reference/errors/#code-120)     | `ERROR_RELATIONS_DOESNT_EXIST` | 422  | A related record does not exist, inside `field_errors`                                                          |
| [143](/reference/errors/#code-143)     | `ERROR_NO_ENTITY`              | 404  | No record with that ID                                                                                          |
| [182](/reference/errors/#code-182)     | `ERROR_TOO_MANY_REQUESTS`      | 429  | Too many requests; see `Retry-After`                                                                            |
| [1000](/reference/errors/#code-1000)   | `ERROR_UNKNOWN`                | 500  | Unexpected server error                                                                                         |
| [2002](/reference/errors/#code-2002)   | `ERROR_CONFLICTING_DATA`       | 400  | The data conflicts, for example an `id` in the body that differs from the one in the URL                        |
| [2006](/reference/errors/#code-2006)   | `ERROR_UNKNOWN_TYPE`           | 404  | No entity with that name in the path                                                                            |
| [40000](/reference/errors/#code-40000) | `ERROR_ENTITY_VALIDATION`      | 422  | A record failed validation; see `entity_errors` and `field_errors`                                              |
| [40002](/reference/errors/#code-40002) | `ERROR_FIELD_VALIDATION`       | 422  | One field failed validation, inside `field_errors`                                                              |

## Limits

| Limit                      | Details                                                                                                                                                                                 |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403 and 409                | Never used; permission problems and conflicts are 400                                                                                                                                   |
| `message`                  | Written for people; match on `code`                                                                                                                                                     |
| Input the API cannot store | `NaN` or `Infinity`, a NUL character (`\u0000`), an unpaired surrogate, a value too long to index or input nested too deeply: HTTP 400, code 117, with a message that names the problem |
