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:
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.
Navixy-specific fields
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.
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.
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.
When a request includes a root list (a list directly inside a product field such as bdr), workspace, or an IoT Logic mutation, send that field in a separate request. If it fails, it makes data null and removes the results of every other field.
Operations that work on several items at once handle a failed item in one of three ways:
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
Authorizationheader. The product field (bdr) returns this error inside an HTTP 200 response, withdetailAccess Denied, and nothing under it runs.The API could not tell which workspace the request acts in. The
detailnames the reason, for examplethe caller belongs to more than one workspace and the token names noneorthe 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
Always check for errors. Inspect the
errorsarray in every response, even when the HTTP status is 200 (GraphQL can return partial data with errors).Use
codefor logic,detailfor display. Base your error handling oncode, not ondetail. New codes can appear, and a failure can move to a different code, so handle an unknown code as a generic failure. Thedetailfield is human-readable and suitable for showing to users.Log the
traceId. When users report issues, thetraceIdhelps the support team quickly locate the relevant logs. Include it in your bug reports.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
versionLimits: 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?