For the complete documentation index, see llms.txt. This page is also available as Markdown.
ReferenceBeta

Error handling

Understand Navixy GraphQL API errors based on RFC 9457 format

When an operation fails, the API returns an error response with details to help you understand what went wrong and how to fix it. All errors follow the RFC 9457 Problem Details format, providing both human-readable messages and machine-readable codes.

Error response structure

Errors are returned in the standard GraphQL format with additional details in the extensions field:

{
  "errors": [{
    "message": "Device not found: 019a6a3f-...",
    "locations": [{ "line": 2, "column": 3 }],
    "path": ["bdr", "deviceUpdate"],
    "extensions": {
      // RFC 9457 standard fields
      "type": "https://api.navixy.com/errors/not-found",
      "title": "Resource Not Found",
      "status": 404,
      "detail": "Device not found: 019a6a3f-...",
      "instance": "/graphql",
      
      // Navixy-specific fields
      "code": "NOT_FOUND",
      "field": "input.id",
      "traceId": "0af7651916cd43dd8448eb211c80319c",
      "timestamp": "2025-01-15T14:30:00Z"
    }
  }],
  "data": { "bdr": { "deviceUpdate": null } }
}

Standard fields

Every error includes these RFC 9457 standard fields:

Field
Type
Description

type

URI

A stable identifier for the error type, in URL form.

title

String

Summary of the problem type. Stays the same for all errors of this type.

status

Integer

HTTP status code (e.g., 400, 404, 409).

detail

String

Human-readable explanation specific to this occurrence of the problem.

instance

URI

The request path that caused the error. Useful for logs or support tickets.

Field
Description

code

Machine-readable error code for use in your application logic.

traceId

Distributed tracing ID (32 hex characters). Equals the trace ID of the traceparent header when the request sends one; otherwise the API generates it. Present on every error, including errors on requests without a token.

timestamp

When the error occurred (ISO 8601 format).

field

For validation errors: the field that failed validation (e.g., input.title).

entityType

Present when a custom field references an entity that no longer exists: the type of that entity (for example Device). Also present on a refusal relayed from Navixy Console: what the request was about. Also present on IN_USE: the type of the record that the delete named, for example ASSET_TYPE.

entityId

Present only with entityType: the ID of that entity.

dependency

Present when the error comes from a system that the API relays, such as Navixy Console (console): on SERVICE_UNAVAILABLE, and on a refusal that system answered with. See Refusals relayed from Navixy Console.

dependencyCode

Present with dependency when that system answered with its own error code, for example PRODUCT_NOT_COVERED.

expectedVersion

Present only when CONFLICT comes from a version check.

currentVersion

Present only when CONFLICT comes from a version check.

allowedValues

For enum validation errors: the list of valid options.

violations

For IoT Logic flow validation errors: every rule that the flow breaks, with the input fields to fix. See Flow validation errors.

productCode

Present on PRODUCT_DISABLED: the product that your workspace doesn't have, for example tracking.

blockers

Present on IN_USE: one entry for each kind of record and each field of it that still points at the record you tried to delete, as { entityType, path, ids }. path is that field, for example type or customFields.<code>, and ids lists up to 20 of them that your role can read. An empty list means that such records exist but you can't read them. See In use (409).

Error codes

Every error has a code in extensions.code. The table lists every code that the API returns. A new version of the API can add codes, so handle a code that isn't in the table as a generic failure.

Code
Status
Type URI
When it occurs

UNAUTHORIZED

401

.../errors/unauthorized

The access token is invalid, expired, or from an untrusted issuer. For example, a request with an expired token, or a tracking stream whose token expires while it's open. The response has no data.

PERMISSION_DENIED

403

.../errors/forbidden

You don't have permission to perform this action. For example, your role doesn't allow the operation, the request has no token, or Business Data Repository or Billing isn't enabled for your workspace. See What your roles and products allow.

NOT_FOUND

404

.../errors/not-found

A mutation names a record that doesn't exist, was deleted, or belongs to another workspace. For example, assetGroupItemsAdd with an unknown asset ID. A query by ID returns null instead.

VALIDATION_ERROR

400

.../errors/validation

Input data failed validation. For example, first above 100, an ID of the wrong type, or a query sent over the WebSocket. field names the input when the error is about one value.

CONFLICT

409

.../errors/conflict

The write conflicted with another request. With expectedVersion and currentVersion present, fetch the entity again and retry with its current version, see Optimistic locking. Without them, retry once after a short pause, then stop and report the failure with its traceId. Console and IAM also return this code when they refuse a request in the current state of the record, for example IAM when you revoke the last Owner role. See Refusals relayed from Navixy Console.

DUPLICATE

409

.../errors/duplicate

A value that must be unique already exists. For example, a device identifier that another device has, or the code of an existing type.

IN_USE

409

.../errors/in-use

A delete was refused because other records still point at the record. blockers names them. See In use (409).

RATE_LIMITED

429

.../errors/rate-limited

Too many requests, or too many open tracking subscriptions. For example, more than 100 requests in 60 seconds. See Limits.

QUERY_TOO_COMPLEX

400

.../errors/query-too-complex

The query selects more fields than the complexity limit allows. See Query complexity.

QUERY_TOO_DEEP

400

.../errors/query-too-deep

The query nests deeper than the depth limit allows. See Query depth.

PRODUCT_DISABLED

403

.../errors/product-disabled

Your workspace doesn't have the product the field belongs to. productCode names it. Tracking and IoT Logic return this code.

SERVICE_UNAVAILABLE

503

.../errors/service-unavailable

A service that the API calls is unavailable, for example while Tracking starts. dependency names it when it's known, for example console, iam, tracking, or tracking-timeline. Retry after a short delay.

INTERNAL_ERROR

500

.../errors/internal

An unexpected server failure. Report it with its traceId.

An error from the GraphQL validation of the request itself, such as an unknown field or an argument of the wrong type, has the code VALIDATION_ERROR too.

Refusals relayed from Navixy Console

The console operations call Navixy Console, and Console decides what a caller may see and do. When Console refuses a request, the API answers with the matching error code and marks the error with dependency: "console". When Console's answer carries its own error code, dependencyCode holds it.

Console answers

Code

Examples of dependencyCode

400

VALIDATION_ERROR

PRODUCT_UNKNOWN, PRODUCT_NOT_TOGGLABLE

401, 403

PERMISSION_DENIED

404

NOT_FOUND

409

CONFLICT

PRODUCT_NOT_COVERED, PRODUCT_MANAGED_BY_PLAN, PRODUCT_NOT_ENABLED, WORKSPACE_TERMINATING

Anything else, or no answer

SERVICE_UNAVAILABLE

A CONFLICT from Console is Console refusing the request in its current state, not a concurrent modification. It carries no expectedVersion, and retrying the request unchanged fails the same way. To learn in advance whether an action would be refused, read allowedActions on the product: see Products.

HTTP status codes

Navixy GraphQL API follows the GraphQL-over-HTTP specification, which means HTTP status codes reflect transport-level results, not domain-level errors.

HTTP Status
Meaning

200

Request processed successfully. Check the errors array for any domain-level failures.

400

Invalid HTTP or GraphQL request (parse or validation error).

401

Authentication required or invalid.

429

Rate limit exceeded.

5xx

Infrastructure or system error.

All business logic errors (such as NOT_FOUND, CONFLICT, or VALIDATION_ERROR) return HTTP 200 with the semantic status code in extensions.status. Use extensions.code for programmatic error handling, not the HTTP status.

Three cases change the HTTP status. A token that the API can't accept returns 401. A request over the request rate returns 429. A request that the API can't run at all returns 400 when your Accept header is application/graphql-response+json, and 200 when it's application/json. This covers parse and validation errors, QUERY_TOO_COMPLEX, and QUERY_TOO_DEEP.

Partial failure

One request can partly succeed. The response then has both data and errors. The field that failed is null in data, and the path of its error names that field. Every other field returns its value as usual. So check errors in every response, even when data isn't empty.

A failure can make more than one field null. When a field that can't be null fails, the API sets its parent to null. If the parent can't be null either, the API sets the next parent to null, up to the first field that can be null. The product fields at the root, such as bdr and tracking, can't be null. The lists and connections inside them, workspace, and me can't be null either. When one of these fields fails, for example with PERMISSION_DENIED, the whole data object is null, and the request loses every other field and alias. IoT Logic mutations return payloads that can't be null, so a failed IoT Logic mutation also makes data null.

Operations that work on several items at once handle a failed item in one of three ways:

Behavior
Operations
What you see

The whole call is refused, nothing changes

assetGroupItemsAdd, and the assetIds of assetGroupCreate and assetGroupUpdate, when an asset doesn't exist or belongs to another workspace. flowCreate and flowUpdate when the flow breaks a rule

One error. NOT_FOUND lists the bad asset IDs in detail. A flow error lists the broken rules it found in violations

Items that need no change are skipped without an error

assetGroupItemsAdd for assets already in the group. assetGroupItemsRemove for assets that aren't in it

No error. Read the group's currentAssets to see the result

Each item gets its own result

nodes(ids:) and the tracking deviceStates query

null in the place of each ID that doesn't exist or that your role can't read

A delete that is refused with IN_USE changes nothing: see Deleting records.

Several mutations in one request run one after another, each in its own aliased bdr block. Each block succeeds or fails on its own, so a failed block doesn't undo the blocks before it. See Batch mutations.

Common error scenarios

Unauthorized (401)

Returned when the request carries a token that is malformed, expired, or wasn't issued by the identity service that the API trusts. The check runs before the query, so the response has no data field and the HTTP status is 401, unlike the 200 that business logic errors return. A request with no token at all is not a 401: it reaches the query, and the product field (bdr) answers PERMISSION_DENIED inside an HTTP 200 response, see below. See Authentication for how to obtain a token.

How to handle: Obtain a new token and retry the request once. Don't retry with the same token: a rejected token stays rejected, so a retry loop only adds latency. Refresh on a schedule instead, shortly before the token expires, so a working integration never sees this error. If the token is fresh and the error persists, check that it was issued by the identity service that the API trusts.

Validation error (400)

Returned when input data fails validation: a required field is missing, a value is out of range, or a string exceeds the maximum length.

How to handle: Check the field to identify which input failed, and detail for the specific requirement.

Permission denied (403)

The API returns this error in these cases:

  • The request has no Authorization header. The product field (bdr) returns this error inside an HTTP 200 response, with detail Access Denied, and nothing under it runs.

  • The API could not tell which workspace the request acts in. The detail names the reason, for example the caller belongs to more than one workspace and the token names none or the caller has no active workspace membership. See Authentication § How the API picks your workspace for the rule the API applies.

  • You're authenticated, but your roles don't allow the operation. See What your roles and products allow.

  • Your workspace doesn't have Business Data Repository or Billing, and the request uses it.

How to handle: Check that the request carries a token. If it does, read detail: a workspace reason means that you need a token issued for one workspace, see Authentication. Otherwise the operation isn't allowed for your account. An administrator of your organization assigns roles in Navixy Console or through the IAM operations, so ask them for a role that allows the operation.

Entity not found (404)

Returned by a mutation when the entity you name doesn't exist in your workspace or has been deleted. A by-id query never returns this error: it answers null instead, see below.

How to handle: Check that the ID is correct. If you're using an ID from a previous response, the entity may have been deleted. A by-id query such as device(id:) returns null without an error when the ID belongs to another workspace or to nothing at all, so treat null and NOT_FOUND the same way: the entity is not available to your token. Billing mutations report a missing entity as VALIDATION_ERROR instead: see Billing errors.

Version conflict (409)

Returned when the entity was modified by another request since you last fetched it. See Optimistic locking for details.

How to handle: Fetch the entity again to get the current version, merge your changes if needed, and retry with the new version in your update input.

A CONFLICT without expectedVersion and currentVersion is not a version mismatch. It comes from a concurrent write that was rolled back, a device reassignment that raced another change, or a data-integrity conflict, and the code alone doesn't say which. Retry once after a short pause. If the error persists, stop and report the failure along with its traceId.

Duplicate (409)

Returned when you try to create or update an entity with a value that must be unique but already exists, such as a device identifier, user email, or catalog item code.

How to handle: The detail names the uniqueness rule that was violated, and field points at the input that holds the conflicting value. Either use a different value or query for the existing entity if you need to update it instead.

In use (409)

Returned when you delete a record that other records still point at, for example an asset type that an asset has, or a device that a required custom field holds. Nothing is deleted. Deleting records lists every reference that refuses a delete.

How to handle: Read blockers. Each entry names a kind of record, the field that points at the record you tried to delete, and up to 20 such records that your role can read. Change or remove those references through the records' own mutations, then delete again. How to clear the blockers of a delete gives the change for each path. An entry with an empty ids list means that your role can't read the records that block the delete.

Best practices

  1. Always check for errors. Inspect the errors array in every response, even when the HTTP status is 200 (GraphQL can return partial data with errors).

  2. Use code for logic, detail for display. Base your error handling on code, not on detail. New codes can appear, and a failure can move to a different code, so handle an unknown code as a generic failure. The detail field is human-readable and suitable for showing to users.

  3. Log the traceId. When users report issues, the traceId helps the support team quickly locate the relevant logs. Include it in your bug reports.

  4. Handle conflicts gracefully. In collaborative applications, version conflicts are expected. Implement retry logic or prompt users to review changes.

See also

  • Authentication: Obtain, refresh, and send the access token that every request needs

  • Optimistic locking: Prevent concurrent updates from overwriting each other with version

  • Limits: Request rate, subscription, pagination, and input size limits, and the errors they return

  • GraphQL basics: Learn GraphQL fundamentals, from queries and mutations to the type system

Last updated

Was this helpful?