> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstresonance.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Error codes

> HTTP status codes and GraphQL error payloads ION returns: what each one means, what causes it, and how to fix it.

## Overview

When an ION API request fails, two things tell you what went wrong. The HTTP status code gives you a coarse category and the `errors[]` payload in the response body gives you the specific message. That message often points directly at the fix.

A typical error response looks like this:

```json theme={null}
{
  "errors": [
    {
      "message": "User does not have permission to update this resource."
    }
  ]
}
```

GraphQL operations always return HTTP 200 even when the operation itself fails. The failure shows up in `errors[]`. Authentication and transport failures are the exception. The auth layer can return a 4xx or 5xx status code before GraphQL runs.

## HTTP status reference

| Status | Meaning                       | When you'll see it                                                                                                |
| ------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `200`  | Success or GraphQL error      | Always for `/graphql`. Check `errors[]`                                                                           |
| `400`  | Malformed request             | Invalid JSON body, missing required field on the wire                                                             |
| `401`  | Unauthenticated               | Missing, expired, or invalid token. See [the 401 table](#401-unauthorized)                                        |
| `403`  | Authenticated but not allowed | Permission, scope, or org-isolation failure. See [403 Forbidden](#403-forbidden)                                  |
| `404`  | Resource not found            | Wrong ID, soft-deleted entity, or not visible to your org                                                         |
| `409`  | Concurrency conflict          | The `_etag` mismatch. Your update raced another writer. See [409 Concurrency conflict](#409-concurrency-conflict) |
| `422`  | Validation error              | Field value violates a domain rule, such as quantity ≤ 0                                                          |
| `429`  | Rate-limited                  | Back off and retry                                                                                                |
| `5xx`  | Server error                  | ION-side failure. Safe to retry with exponential backoff                                                          |

## 401 Unauthorized

ION rejects the request at the authentication layer. Here are the common messages and their fixes:

| Message                                                      | Cause                                                                 | Fix                                                                                                        |
| ------------------------------------------------------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `Please provide proper credentials`                          | No `Authorization` header on the request                              | Add `Authorization: Bearer <token>`.                                                                       |
| `Unable to parse authentication token`                       | Malformed token, such as one that is truncated or in the wrong format | Verify the full token is being sent. A common cause is an env var that lost a trailing character.          |
| `Unable to find appropriate RSA key`                         | The token was signed by a key ION doesn't recognize                   | The token came from the wrong auth provider. Check that you're hitting the right `client_id` and audience. |
| `Token is expired`                                           | The access token's `exp` claim has passed                             | Refresh the token for OAuth, or re-issue it for an API key.                                                |
| `Unable to validate authentication token`                    | Generic verification failure                                          | Re-issue the token. If it persists, check that the token audience and issuer match your environment.       |
| `Token organization does not match user's organization`      | The token was issued for one org, but the user belongs to another     | Re-authenticate the user against their actual organization.                                                |
| `User <email> is deactivated`                                | The user has been deactivated in ION                                  | Reactivate the user or use a different account.                                                            |
| `Sorry, this organization is blocked from accessing ION`     | The org is in a blocked state                                         | Contact your CSM.                                                                                          |
| `Login domain '<domain>' is not valid for this organization` | The user's email domain isn't allowed on the org                      | Add the domain to org settings or use an allowed email.                                                    |

## 403 Forbidden

The request authenticated successfully, but the principal isn't authorized for the operation. The distinct causes each have a different fix.

| Cause                           | What happened                                                                                                                                      | How to fix                                                                               |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **Missing role permission**     | The user or service account's role doesn't include the action being performed. For example, the role lacks the "create purchase order" permission. | An admin grants the role the missing permission, or assigns the user a role that has it. |
| **Resource not visible to org** | The entity ID exists in another tenant's data. ION treats it as nonexistent for this caller.                                                       | Verify the ID belongs to your org. Cross-org references are never allowed.               |
| **Read-only organization**      | The org is in read-only mode, such as a billing freeze or compliance hold, and any mutation is rejected.                                           | Contact your CSM to release the read-only flag.                                          |
| **Read-only schema**            | A specific schema, your tenant, is flagged read-only on the ION side.                                                                              | Contact support.                                                                         |
| **Deactivated user**            | The user account is deactivated. Auth still passes if the token is valid, but every operation returns a 403.                                       | Reactivate the user in admin settings.                                                   |
| **Approval gate not satisfied** | The operation requires an approval that hasn't been granted. For example, publishing a procedure that requires QE sign-off.                        | Complete the approval flow first.                                                        |
| **OAuth scope insufficient**    | The token was issued with limited scopes that don't cover the requested operation.                                                                 | Re-authorize the OAuth app with broader scopes.                                          |
| **Feature flag disabled**       | The operation is gated behind a feature flag not enabled for your org.                                                                             | Contact your CSM to enable the feature for your tier.                                    |
| **Cross-org token**             | A token issued for org A is trying to access org B's data.                                                                                         | Re-authenticate against the correct org.                                                 |

<Tip>
  If a 403 says "permission", check the user's role. If it says "not found" or
  refers to an ID, check the entity's tenant. If it says "read-only", check the
  org's billing and compliance state.
</Tip>

### Sample 403 payload

```json theme={null}
{
  "errors": [
    {
      "message": "User does not have permission to perform UPDATE on Purchase Order."
    }
  ]
}
```

The action verb (`UPDATE`) and the resource type (`Purchase Order`) are the two pieces an admin needs to find the missing role permission.

## 404 Not Found

ION returns a 404, and a GraphQL `null` for the field, when the entity doesn't exist or doesn't belong to your tenant. The two cases are intentionally indistinguishable to prevent enumeration. A 404 doesn't leak that an ID exists in another org.

Common causes:

* An ID typo.
* The entity was soft-deleted.
* A cross-tenant ID. See 403 above. Sometimes this surfaces as a 404 instead, depending on the resolver.

If the entity should exist, query a filtered list such as `parts(filters: { id: { in: [42] } }) { edges { node { id } } }` instead of `part(id: "42")`. The filtered query returns an empty `edges` array. That confirms a tenant or scope issue.

## 409 Concurrency conflict

ION uses optimistic concurrency control via the `_etag` field. Every mutable entity returns an `_etag` on read. Mutations require you to pass back the `_etag` you received. If another writer modified the entity in the meantime, the etag won't match and ION returns a 409.

```json theme={null}
{
  "errors": [{ "message": "Concurrent modification detected — etag mismatch." }]
}
```

The fix is always the same:

1. Re-read the entity to get the latest `_etag` and the latest field values.
2. Re-apply your changes on top of the new state.
3. Retry the mutation with the new `_etag`.

This is intentional. Re-reading surfaces the other writer's changes before you overwrite them. Always re-read and re-apply, even when you intend to discard the other writer's changes.

## 422 Validation error

The request was structurally valid and authorized, but a field value violated a domain rule:

* A quantity is zero or negative.
* A required field is missing.
* A date is out of range.
* A string exceeds the maximum length.
* An enum value is not allowed.

The error message names the field and the constraint:

```json theme={null}
{
  "errors": [{ "message": "PartInventory quantity must be greater than 0." }]
}
```

These errors are deterministic. Fix the input and retry.

## 429 Rate limit

ION applies fair-use rate limiting on the `/graphql` endpoint. Production traffic patterns rarely hit it. If you do, the response is HTTP 429.

Address it in this order:

1. Implement exponential backoff in your client. Start at 1 second, double up to about 30 seconds, and add jitter.
2. Batch related queries. One query selecting many fragments beats several queries that each select one fragment.
3. Cache locally. For data that doesn't change often, such as parts and procedures, don't re-fetch every time.

## 5xx Server errors

A 5xx response indicates a failure on ION's side. These responses are safe to retry with exponential backoff.

```http theme={null}
HTTP/1.1 502 Bad Gateway
```

Standard recovery pattern:

1. Retry with exponential backoff. Use 1, 2, 4, and 8 seconds.
2. After three consecutive failures across about 30 seconds, surface the error to the user or on-call.
3. Check ION's status page if available, or contact support if the error persists.

## GraphQL errors\[] shape

Inside the GraphQL response body, an HTTP 200, the `errors` array is the source of truth:

```json theme={null}
{
  "data": null,
  "errors": [
    {
      "message": "User does not have permission to perform UPDATE on Purchase Order.",
      "path": ["updatePurchaseOrder"],
      "locations": [{ "line": 2, "column": 3 }]
    }
  ]
}
```

| Field       | Meaning                                                           |
| ----------- | ----------------------------------------------------------------- |
| `message`   | Human-readable error.                                             |
| `path`      | Which field in your query failed.                                 |
| `locations` | Position in the query string for IDE and playground integrations. |

A partial `data` payload with a non-empty `errors[]` means the returned fields are valid and one or more other fields failed. Most clients merge these. Yours should too.

## Troubleshooting flow

1. Check the HTTP status.
2. If it's a 200, parse `errors[]` from the response body.
3. Match the message against the tables on this page.
4. If the message is unfamiliar, its wording usually points at the offending field, permission, or resource. Search for it in [Authentication](/api-reference/authentication/overview) and the tables on this page.
5. If you're still stuck, reproduce the call in the [API Playground](/api-reference/playground), which surfaces errors more readably. Paste the request and error into a support ticket.

<Note>
  Most 403s are fixed by granting the user's role the missing permission. Check
  permissions before you change code. For a 409, show the conflict to a human so
  they can re-merge.
</Note>

## Related

* [Authentication](/api-reference/authentication/overview)
* [API playground](/api-reference/playground)
