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

ConceptsValidation

Validation

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.

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)
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), a product needs unit_symbol, and its sku must be unique, or the create answers HTTP 400 (products and line items), and a lead needs owner_id or unit_id (lead from a 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:

# 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")
// 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;
# 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"]
# 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')
// 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;
# 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:

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

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:

# 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
// 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}`);
# 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}")
# 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
// 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)}`);
# 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'])}")
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "success": true,
  "data": [
    "0a000000-0000-4000-a000-000000000001"
  ]
}
200 OKRecorded response · 2026-10-15 · docs sandbox, de-identified
{
  "data": {
    "bulkUpdateAccount": {
      "result": {
        "updated": [
          "0a000000-0000-4000-a000-000000000001"
        ],
        "errors": []
      }
    }
  }
}

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
Verified on 2026-10-15 against API 6.5Behaviour-verified

Every sample on this page ran against the docs sandbox and returned the response shown.