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:
{
"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 |
403 | Authenticated but not allowed | Permission, scope, or org-isolation failure. See 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 |
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. |
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.
Sample 403 payload
{
"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.
{
"errors": [{ "message": "Concurrent modification detected — etag mismatch." }]
}
The fix is always the same:
- Re-read the entity to get the latest
_etag and the latest field values.
- Re-apply your changes on top of the new state.
- 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:
{
"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:
- Implement exponential backoff in your client. Start at 1 second, double up to about 30 seconds, and add jitter.
- Batch related queries. One query selecting many fragments beats several queries that each select one fragment.
- 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.
Standard recovery pattern:
- Retry with exponential backoff. Use 1, 2, 4, and 8 seconds.
- After three consecutive failures across about 30 seconds, surface the error to the user or on-call.
- 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:
{
"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
- Check the HTTP status.
- If it’s a 200, parse
errors[] from the response body.
- Match the message against the tables on this page.
- If the message is unfamiliar, its wording usually points at the offending field, permission, or resource. Search for it in Authentication and the tables on this page.
- If you’re still stuck, reproduce the call in the API Playground, which surfaces errors more readably. Paste the request and error into a support ticket.
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.