---
title: Validation
description: Relax server-side validation on a write with the validation-level bit flags. Which bits are public, what each does, and how to add them up.
canonical: https://developers.coevera.com/concepts/validation/
section: concepts
verification: behaviour
lastVerified: 2026-10-15
publisher: Coevera
---

# Validation

**Answer:** validation-level relaxes validation on create, update and batch-modify. It takes a bit mask, member names or a comma list: 2 skips unchanged fields, 16 allows links to deleted records, 32 lets an update empty a required field, 512 allows duplicates in a unique field. Use 2 to update records that are already invalid.

Every write is validated: required fields, e-mail formats, links to other records, unique values.
`validation-level` turns some of those checks off for one request. The usual reason to use it is
a sync that writes back records which were already invalid when it read them.

## How it works

`validation-level` is a query parameter of `POST /entities/{Entity}` (create),
`PATCH /entities/{Entity}/{id}` (update) and `POST /entities/{Entity}/batch-modify`. On
`batch-delete` and `batch-restore` it is refused with HTTP 400, code 118. In GraphQL, the create,
update, bulk and `convert…` mutations take `validationLevel` as a list of names:
`validationLevel: [SKIP_UNCHANGED_FIELDS]`. Leave the argument out for full validation: an
explicit `validationLevel: null` on `bulkUpdateAccount` fails with code 1000 `ERROR_UNKNOWN`,
even for a valid update.

The value is a bit mask. Send it as a number, by name, or as a comma list of either; a list is
combined with OR, so a repeated value does no harm:

| Form    | Example                                                               | Means    |
| ------- | --------------------------------------------------------------------- | -------- |
| Number  | `validation-level=18`                                                 | 2 + 16   |
| Names   | `validation-level=SKIP_UNCHANGED_FIELDS,SKIP_RELATIONSHIP_VALIDATION` | The same |
| Numbers | `validation-level=2,16`                                               | The same |

Add the bits up to combine them, and take a number apart the same way: `416` is
256 + 128 + 32, not 4 and 16 (that is `20`).

## The bits

| Bit | Name                             | What it does                                                                                                                                                                                                                             |
| --- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0   | `FULL`                           | Every check; the default                                                                                                                                                                                                                 |
| 2   | `SKIP_UNCHANGED_FIELDS`          | A `batch-modify` update goes through on a record that already holds an invalid value it does not change, whether it re-sends that value or leaves it out. On create, it also lets a contact through without `first_name` or `last_name`. |
| 4   | `SKIP_USER_DEFINED_VALIDATIONS`  | Skips the rules an admin sets on forms                                                                                                                                                                                                   |
| 8   | `SKIP_READONLY_VALIDATION`       | Keeps a `created` or `modified` more than one day in the future, which a write otherwise ignores ([dates](/concepts/dates-and-time-zones/#date-and-date-time-fields))                                                                    |
| 16  | `SKIP_RELATIONSHIP_VALIDATION`   | A record may link to a deleted record, such as a `parent_account_id` of a deleted account                                                                                                                                                |
| 32  | `SKIP_FIELD_VALUE_VALIDATION`    | An update may set a required field to empty, such as `"name": ""` by PATCH                                                                                                                                                               |
| 64  | `SKIP_INVALID_RECALCULATIONS`    | Accepted, and part of `SKIP_ALL`                                                                                                                                                                                                         |
| 128 | `SKIP_STEP_CHECKLIST_VALIDATION` | Skips the checks of step checklists                                                                                                                                                                                                      |
| 254 | `SKIP_ALL`                       | Bits 2 to 128 together. Only this full set lets through an invalid e-mail format or a dropdown value from another field's options.                                                                                                       |
| 512 | `SKIP_UNIQUE_VALIDATION`         | Allows a duplicate in a field that must be unique. Not part of `SKIP_ALL`: add it, `766`.                                                                                                                                                |

- No bit lets a create through without its name: an account without `name` is refused with HTTP
  422, even with `SKIP_ALL`.
- A contact needs `first_name` or `last_name`, although the published spec marks only `owner_id`
  as required: without both, the create answers HTTP 422, code 119.
- Other creates need more than the published spec marks as required, and answer HTTP 422
  without it: an opportunity needs `account_relations` and a `value` with `currency_id`
  ([create an opportunity](/recipes/create-opportunity/)), a product needs `unit_symbol`, and its
  `sku` must be unique, or the create answers HTTP 400
  ([products and line items](/recipes/products-and-line-items/)), and a lead needs `owner_id` or
  `unit_id` ([lead from a web form](/recipes/lead-from-web-form/)).
- Send only the bits in this table. A value that carries either of two internal bits is refused
  with HTTP 400, code 117 `ERROR_PARAMETER_INVALID`, and nothing is written. That holds on
  create, update and `batch-modify`, when the bit is added to public bits or sent in a comma
  list, and for `-1`, which sets every bit. Other bits that name no member, such as `1` or
  `1024`, are accepted without an error.

## Update records that are already invalid

A PATCH ignores the fields it does not change, even when you send them with their current
value. A `batch-modify` update does not: it validates the whole record, fields you do not send
included. A `batch-modify` therefore fails on a record that holds an invalid value, whether the
update sends that value unchanged or leaves it out.

The sample creates such a record, an account whose `email1` is `not-an-email`. It stands in for
old data and uses `SKIP_ALL` (254) to get past the e-mail check; do not do that in your own code:

**REST · cURL**

```bash
# Stands in for a record that is already invalid, as older data often is: validation-level=254
# (SKIP_ALL, every skip bit from 2 to 128) lets the invalid e-mail address in. Do not do this
# in your own code.
account=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Accounts?validation-level=254" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{ "name": "Northwind Analytics", "owner_id": "$OWNER_ID", "email1": "not-an-email" }
JSON
)
ACCOUNT_ID=$(jq -er '.data.id' <<<"$account")
```

**REST · TypeScript**

```ts
// Stands in for a record that is already invalid, as older data often is: validation-level=254
// (SKIP_ALL, every skip bit from 2 to 128) lets the invalid e-mail address in. Do not do this
// in your own code.
const created = await call("/entities/Accounts?validation-level=254", {
  name: "Northwind Analytics",
  owner_id: ownerId,
  email1: "not-an-email",
});
if (created.status >= 300) throw new Error(`HTTP ${created.status} ${created.body.name}: ${created.body.message}`);
const accountId: string = created.body.data.id;
```

**REST · Python**

```python
# Stands in for a record that is already invalid, as older data often is: validation-level=254
# (SKIP_ALL, every skip bit from 2 to 128) lets the invalid e-mail address in. Do not do this
# in your own code.
created = session.post(
    f"{BASE}/entities/Accounts",
    params={"validation-level": 254},
    json={"name": "Northwind Analytics", "owner_id": owner_id, "email1": "not-an-email"},
    timeout=60,
)
if not created.ok:
    sys.exit(f"HTTP {created.status_code}: {created.text}")
account_id = created.json()["data"]["id"]
```

**GraphQL · cURL**

```bash
# Stands in for a record that is already invalid, as older data often is: SKIP_ALL lets the
# invalid e-mail address in. Do not do this in your own code.
ACCOUNT_ID=$(jq -n --arg owner "$OWNER_ID" '{
  query: "mutation Existing($input: CreateAccountInput!) { createAccount(input: $input, validationLevel: [SKIP_ALL]) { account { id } } }",
  variables: { input: { name: "Northwind Analytics", ownerId: $owner, email1: "not-an-email" } }
}' | gql | jq -er '.data.createAccount.account.id')
```

**GraphQL · TypeScript**

```ts
// Stands in for a record that is already invalid, as older data often is: SKIP_ALL lets the
// invalid e-mail address in. Do not do this in your own code.
const created = await gql(
  `mutation Existing($input: CreateAccountInput!) {
  createAccount(input: $input, validationLevel: [SKIP_ALL]) { account { id } }
}`,
  { input: { name: "Northwind Analytics", ownerId, email1: "not-an-email" } },
);
if (created.errors) throw new Error(`Unexpected GraphQL errors: ${JSON.stringify(created.errors)}`);
const accountId: string = created.data.createAccount.account.id;
```

**GraphQL · Python**

```python
# Stands in for a record that is already invalid, as older data often is: SKIP_ALL lets the
# invalid e-mail address in. Do not do this in your own code.
created = gql(
    """
mutation Existing($input: CreateAccountInput!) {
  createAccount(input: $input, validationLevel: [SKIP_ALL]) { account { id } }
}
""",
    {"input": {"name": "Northwind Analytics", "ownerId": owner_id, "email1": "not-an-email"}},
)
if created.get("errors"):
    sys.exit(f"Unexpected GraphQL errors: {json.dumps(created['errors'])}")
account_id = created["data"]["createAccount"]["account"]["id"]
```

A `batch-modify` that changes only `city`, and sends the unchanged `email1` with it, is refused:

**REST · cURL**

```bash
# The sync sends the whole record back with one change, the city. The unchanged, invalid
# e-mail address fails the update: HTTP 422, and nothing is written. No --fail-with-body:
# the 422 is the expected answer here.
UPDATE="[{ \"id\": \"$ACCOUNT_ID\", \"email1\": \"not-an-email\", \"city\": \"Seattle\" }]"
curl -sS -w '\nHTTP %{http_code}\n' "$COEVERA_BASE/entities/Accounts/batch-modify" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "$UPDATE"
```

**REST · TypeScript**

```ts
// The sync sends the whole record back with one change, the city. The unchanged, invalid
// e-mail address fails the update: HTTP 422, and nothing is written.
const update = [{ id: accountId, email1: "not-an-email", city: "Seattle" }];
const strict = await call("/entities/Accounts/batch-modify", update);
console.log(`Full validation: HTTP ${strict.status}`, JSON.stringify(strict.body, null, 2));
```

**REST · Python**

```python
# The sync sends the whole record back with one change, the city. The unchanged, invalid
# e-mail address fails the update: HTTP 422, and nothing is written.
update = [{"id": account_id, "email1": "not-an-email", "city": "Seattle"}]
strict = session.post(f"{BASE}/entities/Accounts/batch-modify", json=update, timeout=60)
print(f"Full validation: HTTP {strict.status_code}", json.dumps(strict.json(), indent=2))
```

**GraphQL · cURL**

```bash
# sync [LEVEL]: the update request, the whole record with one change, the city. LEVEL is a JSON
# list of validation levels; without it, full validation. Leave validationLevel out rather than
# sending null: an explicit null is answered with an unexpected error (code 1000).
sync() {
  jq -n --arg id "$ACCOUNT_ID" --argjson level "${1:-null}" '{
    query: "mutation Sync($input: [CreateOrUpdateAccountInput!]!, $validationLevel: [ValidationLevel!]) { bulkUpdateAccount(input: $input, validationLevel: $validationLevel) { result { updated errors { index code message } } } }",
    variables: ({ input: [{ id: $id, email1: "not-an-email", city: "Seattle" }] }
      + if $level then { validationLevel: $level } else {} end)
  }'
}
# The unchanged, invalid e-mail address fails the update: HTTP 200 with errors and no result,
# and nothing is written.
sync | gql
echo
```

**GraphQL · TypeScript**

```ts
// The sync sends the whole record back with one change, the city. The unchanged, invalid
// e-mail address fails the update: HTTP 200 with errors and no result, and nothing is written.
const UPDATE = `mutation Sync($input: [CreateOrUpdateAccountInput!]!, $validationLevel: [ValidationLevel!]) {
  bulkUpdateAccount(input: $input, validationLevel: $validationLevel) {
    result {
      updated
      errors { index code message }
    }
  }
}`;
const update = [{ id: accountId, email1: "not-an-email", city: "Seattle" }];
const strict = await gql(UPDATE, { input: update });
console.log("Full validation:", JSON.stringify(strict, null, 2));
```

**GraphQL · Python**

```python
# The sync sends the whole record back with one change, the city. The unchanged, invalid
# e-mail address fails the update: HTTP 200 with errors and no result, and nothing is written.
UPDATE = """
mutation Sync($input: [CreateOrUpdateAccountInput!]!, $validationLevel: [ValidationLevel!]) {
  bulkUpdateAccount(input: $input, validationLevel: $validationLevel) {
    result {
      updated
      errors { index code message }
    }
  }
}
"""
update = [{"id": account_id, "email1": "not-an-email", "city": "Seattle"}]
strict = gql(UPDATE, {"input": update})
print("Full validation:", json.dumps(strict, indent=2))
```

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

```json
{
  "code": 40000,
  "name": "ERROR_ENTITY_VALIDATION",
  "message": "\nInvalid email in field email1: \"not-an-email\"",
  "entity_id": "0a000000-0000-4000-a000-000000000001",
  "entity_name": "Account",
  "entity_errors": [
    {
      "code": 117,
      "name": "ERROR_PARAMETER_INVALID",
      "message": "Invalid email in field email1: \"not-an-email\""
    }
  ],
  "field_errors": [],
  "step_checklist_errors": [],
  "entity_index": null,
  "http_status": 422,
  "success": false
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "errors": [
    {
      "message": "\nInvalid email in field email1: \"not-an-email\"",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "bulkUpdateAccount"
      ],
      "api_error": {
        "code": 40000,
        "name": "ERROR_ENTITY_VALIDATION",
        "message": "\nInvalid email in field email1: \"not-an-email\"",
        "entity_id": "0a000000-0000-4000-a000-000000000001",
        "entity_name": "Account",
        "entity_errors": [
          {
            "code": 117,
            "name": "ERROR_PARAMETER_INVALID",
            "message": "Invalid email in field email1: \"not-an-email\""
          }
        ],
        "field_errors": [],
        "step_checklist_errors": [],
        "entity_index": null
      }
    }
  ],
  "data": {
    "bulkUpdateAccount": null
  }
}
```

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

The same update with `validation-level=2` goes through. The unchanged e-mail is not checked
again, and a changed field is still validated as usual:

**REST · cURL**

```bash
# The same update with validation-level=2 (SKIP_UNCHANGED_FIELDS): only the city is validated.
curl -sS --fail-with-body "$COEVERA_BASE/entities/Accounts/batch-modify?validation-level=2" \
  -u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "$UPDATE"
echo
```

**REST · TypeScript**

```ts
// The same update with validation-level=2 (SKIP_UNCHANGED_FIELDS): only the city is validated.
const relaxed = await call("/entities/Accounts/batch-modify?validation-level=2", update);
console.log(`validation-level=2: HTTP ${relaxed.status}`, JSON.stringify(relaxed.body, null, 2));
if (relaxed.status !== 200) throw new Error(`Expected HTTP 200, got ${relaxed.status}`);
```

**REST · Python**

```python
# The same update with validation-level=2 (SKIP_UNCHANGED_FIELDS): only the city is validated.
relaxed = session.post(
    f"{BASE}/entities/Accounts/batch-modify",
    params={"validation-level": 2},
    json=update,
    timeout=60,
)
print(f"validation-level=2: HTTP {relaxed.status_code}", json.dumps(relaxed.json(), indent=2))
if relaxed.status_code != 200:
    sys.exit(f"Expected HTTP 200, got {relaxed.status_code}")
```

**GraphQL · cURL**

```bash
# The same update with SKIP_UNCHANGED_FIELDS: only the city is validated.
relaxed=$(sync '["SKIP_UNCHANGED_FIELDS"]' | gql)
echo "$relaxed"
if jq -e '.errors' <<<"$relaxed" >/dev/null; then
  echo "Unexpected GraphQL errors" >&2
  exit 1
fi
```

**GraphQL · TypeScript**

```ts
// The same update with SKIP_UNCHANGED_FIELDS: only the city is validated.
const relaxed = await gql(UPDATE, { input: update, validationLevel: ["SKIP_UNCHANGED_FIELDS"] });
console.log("SKIP_UNCHANGED_FIELDS:", JSON.stringify(relaxed, null, 2));
if (relaxed.errors) throw new Error(`Unexpected GraphQL errors: ${JSON.stringify(relaxed.errors)}`);
```

**GraphQL · Python**

```python
# The same update with SKIP_UNCHANGED_FIELDS: only the city is validated.
relaxed = gql(UPDATE, {"input": update, "validationLevel": ["SKIP_UNCHANGED_FIELDS"]})
print("SKIP_UNCHANGED_FIELDS:", json.dumps(relaxed, indent=2))
if relaxed.get("errors"):
    sys.exit(f"Unexpected GraphQL errors: {json.dumps(relaxed['errors'])}")
```

**Recorded response · REST · 200 OK**

```json
{
  "success": true,
  "data": [
    "0a000000-0000-4000-a000-000000000001"
  ]
}
```

**Recorded response · GraphQL · 200 OK**

```json
{
  "data": {
    "bulkUpdateAccount": {
      "result": {
        "updated": [
          "0a000000-0000-4000-a000-000000000001"
        ],
        "errors": []
      }
    }
  }
}
```

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

`2` is the narrow answer for this case; `SKIP_ALL` turns off far more than you need.

## Limits

| Limit            | Details                                                                                               |
| ---------------- | ----------------------------------------------------------------------------------------------------- |
| Where it applies | Create, update and `batch-modify`; refused on `batch-delete` and `batch-restore` (HTTP 400, code 118) |
| Never skipped    | A missing name on create                                                                              |
| Refused values   | Two internal bits in any form, `-1` included: HTTP 400, code 117 `ERROR_PARAMETER_INVALID`            |
