Developers
Set your API contextPick a region and space ID once. Every sample fills in your base URL.
Where do I find these?

In Coevera: open your space's API application and choose Show Access. It shows the space ID and the base URL, whose host starts with your region.

From the API: if you know the space ID, the discovery endpoint GET /api/discovery/spaces/{space_id} returns its region and base URLs, without credentials.

Only the region and space ID are kept, in this browser. Never paste a key or password here.

More in the quickstart

ConceptsErrors

Errors

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.

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:

400 Bad RequestRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "code": 117,
  "name": "ERROR_PARAMETER_INVALID",
  "message": "Limit must not be greater than 100",
  "http_status": 400,
  "success": false
}
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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
      }
    }
  }
}
Show all 30 linesShow less
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 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:

422 Unprocessable EntityRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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
}
Show all 29 linesShow less
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "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
  }
}
Show all 46 linesShow less

In a batch sent with the default rollback, entity_index is null: the error does not say which item failed (batch operations).

HTTP statuses

HTTP When Typical codes
200 Success, including a batch with failed items sent with rollback-method=1 See 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 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 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. 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:

# 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
}
// 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;
  }
}
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
# 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
}
// 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;
  }
}
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.

# 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
// 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));
}
# 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))
# 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
// 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));
}
# 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 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 ERROR_NOT_ENOUGH_PERMISSIONS 400 The application lacks a permission
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 ERROR_PARAMETER_UNKNOWN 400 A parameter the endpoint does not take
119 ERROR_PARAMETER_MISSING 422 A required value is missing
120 ERROR_RELATIONS_DOESNT_EXIST 422 A related record does not exist, inside field_errors
143 ERROR_NO_ENTITY 404 No record with that ID
182 ERROR_TOO_MANY_REQUESTS 429 Too many requests; see Retry-After
1000 ERROR_UNKNOWN 500 Unexpected server error
2002 ERROR_CONFLICTING_DATA 400 The data conflicts, for example an id in the body that differs from the one in the URL
2006 ERROR_UNKNOWN_TYPE 404 No entity with that name in the path
40000 ERROR_ENTITY_VALIDATION 422 A record failed validation; see entity_errors and field_errors
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