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.
Overview
When an ION API request fails, two things tell you what went wrong:
- HTTP status code — coarse category (auth, permissions, validation, server)
errors[] payload in the response body — specific message, often pointing directly at the fix
A typical error response looks like:
{
"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[], not the status code. The exception is authentication and transport failures (401, 4xx, 5xx) which return non-200 status codes from the auth layer before GraphQL ever 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/invalid token. See 401 table below |
403 | Authenticated but not allowed | Permission, scope, or org-isolation failure. See 403 deep-dive |
404 | Resource not found | Wrong ID, soft-deleted entity, or not visible to your org |
409 | Concurrency conflict | etag mismatch — your update raced another writer. See Concurrency |
422 | Validation error | Field value violates a domain rule (e.g. quantity ≤ 0) |
429 | Rate-limited | Slow down; back off and retry |
5xx | Server error | ION-side failure; safe to retry with exponential backoff |
401 Unauthorized
The request was rejected at the authentication layer. This is fully covered in Authentication → Error codes, including the message-by-message remediation table. Most common causes:
- No
Authorization header
- Expired token
- Wrong audience or issuer (token from one auth provider, ION expecting another)
- Token belongs to a different organization than the user
403 Forbidden
The request authenticated successfully but the principal isn’t authorized for the operation. This is the highest-volume support ticket category, so it’s worth understanding the distinct causes — they each have a different fix.
| Cause | What happened | How to fix |
|---|
| Missing role permission | The user/service account’s role doesn’t include the action being performed (e.g. role lacks “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’s row-level security says it doesn’t exist for this caller | Verify the ID belongs to your org. Cross-org references are never allowed |
| Read-only organization | Org is in read-only mode (billing freeze, compliance hold) and any mutation is rejected | Contact your CSM to release the read-only flag |
| Read-only schema | Specific schema (your tenant) is flagged read-only on the ION side | Contact support |
| Deactivated user | User account is deactivated; auth still passes if token is valid, but every operation 403s | Reactivate the user in admin settings |
| Approval gate not satisfied | Operation requires an approval that hasn’t been granted (e.g. 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 | 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 | Token issued for org A trying to access org B’s data | Re-authenticate against the correct org |
Rule of thumb: 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/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 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:
- ID typo
- Entity was soft-deleted (most ION entities use the
_deleted flag rather than hard-delete)
- Cross-tenant ID (see 403 above — sometimes this surfaces as 404 instead, depending on the resolver)
If the entity should exist, query a filtered list (parts(filters: { ids: [42] })) instead of part(id: 42) — the filtered query returns an empty array and confirms it’s a tenant/scope issue, not a true 404.
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 — ION wants you to see the other writer’s changes before stomping on them. If you don’t care about the other writer’s changes (rare, usually a sign of a bug), just always re-read and re-apply.
422 Validation Error
The request was structurally valid and authorized, but a field value violated a domain rule:
- Quantity ≤ 0
- Required field missing
- Date out of range
- String too long for the column
- Enum value not allowed
The error message names the field and the constraint:
{
"errors": [
{ "message": "PartInventory quantity must be greater than 0." }
]
}
These 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.
Mitigation order:
- Implement exponential backoff in your client (start at 1s, double up to ~30s, jitter)
- Batch related queries — one query selecting many fragments beats N queries each selecting one fragment
- Cache locally — for data that doesn’t change often (parts, procedures), don’t re-fetch every time
5xx Server Errors
5xx responses indicate failures on ION’s side: database timeouts, dependency failures, deployment glitches. They’re safe to retry.
Standard recovery pattern:
- Retry with exponential backoff (1s, 2s, 4s, 8s…)
- After 3 consecutive failures across ~30s, surface to the user / on-call
- Check ION’s status page if available, or contact support if persistent
GraphQL errors[] shape
Inside the GraphQL response body (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/playground integrations |
When data is partially populated and errors[] is non-empty, GraphQL is telling you the partial result is real but one or more fields had errors. Most clients merge these — yours should too.
Troubleshooting flow
- Check the HTTP status.
- If
200, parse errors[] from the response body.
- Match the message against the tables on this page.
- If unfamiliar, the message wording usually points at the offending field/permission/resource — search for it in the Authentication and this page’s tables.
- Still stuck → reproduce the call in the API Playground (which surfaces errors more readably) and paste the request + error into a support ticket.
Tips
- Log the full response body, not just the status code. The
errors[] payload is where remediation actually lives.
- Don’t retry 4xx errors blindly — they won’t fix themselves. Only 429 and 5xx are safe to retry without changing inputs.
- Treat 403 as an admin action, not a code change. The most common “fix” for 403 is a role grant, not a code update.
- Surface concurrency 409s to humans when relevant — letting an operator see “this was modified by Alice 30 seconds ago” prevents bad merges.