Skip to main content

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

StatusMeaningWhen you’ll see it
200Success or GraphQL errorAlways for /graphql — check errors[]
400Malformed requestInvalid JSON body, missing required field on the wire
401UnauthenticatedMissing/expired/invalid token. See 401 table below
403Authenticated but not allowedPermission, scope, or org-isolation failure. See 403 deep-dive
404Resource not foundWrong ID, soft-deleted entity, or not visible to your org
409Concurrency conflictetag mismatch — your update raced another writer. See Concurrency
422Validation errorField value violates a domain rule (e.g. quantity ≤ 0)
429Rate-limitedSlow down; back off and retry
5xxServer errorION-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.
CauseWhat happenedHow to fix
Missing role permissionThe 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 orgThe entity ID exists in another tenant’s data; ION’s row-level security says it doesn’t exist for this callerVerify the ID belongs to your org. Cross-org references are never allowed
Read-only organizationOrg is in read-only mode (billing freeze, compliance hold) and any mutation is rejectedContact your CSM to release the read-only flag
Read-only schemaSpecific schema (your tenant) is flagged read-only on the ION sideContact support
Deactivated userUser account is deactivated; auth still passes if token is valid, but every operation 403sReactivate the user in admin settings
Approval gate not satisfiedOperation 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 insufficientThe token was issued with limited scopes that don’t cover the requested operationRe-authorize the OAuth app with broader scopes
Feature flag disabledOperation is gated behind a feature flag not enabled for your orgContact your CSM to enable the feature for your tier
Cross-org tokenToken issued for org A trying to access org B’s dataRe-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:
  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 — 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:
  1. Implement exponential backoff in your client (start at 1s, double up to ~30s, jitter)
  2. Batch related queries — one query selecting many fragments beats N queries each selecting one fragment
  3. 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.
HTTP/1.1 502 Bad Gateway
Standard recovery pattern:
  1. Retry with exponential backoff (1s, 2s, 4s, 8s…)
  2. After 3 consecutive failures across ~30s, surface to the user / on-call
  3. 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 }]
    }
  ]
}
FieldMeaning
messageHuman-readable error
pathWhich field in your query failed
locationsPosition 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

  1. Check the HTTP status.
  2. If 200, parse errors[] from the response body.
  3. Match the message against the tables on this page.
  4. If unfamiliar, the message wording usually points at the offending field/permission/resource — search for it in the Authentication and this page’s tables.
  5. 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.