Skip to Content
API ReferenceErrors & Rate Limits

Errors and Rate Limits

This page covers the error format, common error codes, rate limiting, and query complexity limits for the Foir API.

Error Format

Foir returns errors in the standard GraphQL error format. Errors are included in the errors array alongside any partial data:

{ "errors": [ { "message": "Record not found", "path": ["recordByKey"], "extensions": { "code": "NOT_FOUND" } } ], "data": { "recordByKey": null } }

Each error object contains:

FieldTypeDescription
messageStringHuman-readable error description
path[String]GraphQL path to the field that caused the error
extensions.codeStringMachine-readable error code

Common Error Codes

UNAUTHENTICATED

The request is missing a valid API key or the key is invalid.

{ "errors": [{ "message": "Authentication required", "extensions": { "code": "UNAUTHENTICATED" } }] }

Common causes:

  • Missing x-api-key header
  • API key is invalid, expired, or deleted
  • API key has been disabled in the dashboard

Resolution: Verify your API key is correct and active. Check that the x-api-key header is included in your request.

FORBIDDEN

The API key does not have permission for the requested operation.

{ "errors": [{ "message": "Insufficient permissions", "extensions": { "code": "FORBIDDEN" } }] }

Common causes:

  • API key lacks the required scope (e.g., attempting records:write with a read-only key)
  • Public key attempting a write operation
  • Domain restriction mismatch
  • Customer JWT missing or invalid for customer-scoped resources

Resolution: Check the scopes assigned to your API key in the dashboard. Use a secret key for write operations.

NOT_FOUND

The requested resource does not exist.

{ "errors": [{ "message": "Record not found", "extensions": { "code": "NOT_FOUND" } }] }

Common causes:

  • Record ID or natural key does not exist
  • Record has been deleted
  • Model key is incorrect
  • Record is unpublished and accessed with a live key (use a test key or preview: true)

VALIDATION_ERROR

The input data failed validation.

{ "errors": [{ "message": "Validation failed: 'title' is required", "extensions": { "code": "VALIDATION_ERROR", "validationErrors": [ { "field": "title", "message": "This field is required" } ] } }] }

Common causes:

  • Required fields are missing
  • Field values do not match the expected type
  • Field values violate validation rules (e.g., max length, pattern)
  • Natural key conflicts with an existing record

RATE_LIMITED

The API key has exceeded its rate limit.

{ "errors": [{ "message": "Rate limit exceeded. Try again in 42 seconds.", "extensions": { "code": "RATE_LIMITED", "retryAfter": 42 } }] }

Resolution: Wait for the rate limit window to reset. Check the X-RateLimit-Reset header for the reset timestamp. Consider optimizing your request patterns or requesting a higher limit.

Rate Limiting

API requests are rate-limited per API key. Rate limits protect the platform from abuse and ensure fair usage across all projects.

Default Limits

The default rate limit is 10,000 requests per hour per API key. This limit is configurable per key in the dashboard, and enterprise plans support custom limits.

Rate Limit Headers

Every API response includes rate limit headers:

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed per window10000
X-RateLimit-RemainingRequests remaining in the current window9847
X-RateLimit-ResetUnix timestamp when the window resets1743523200

Handling Rate Limits

Check the headers proactively to avoid hitting limits:

async function foirQuery(query, variables) { const response = await fetch("https://api.foir.io/graphql", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.FOIR_API_KEY, }, body: JSON.stringify({ query, variables }), }); const remaining = parseInt(response.headers.get("X-RateLimit-Remaining"), 10); const resetAt = parseInt(response.headers.get("X-RateLimit-Reset"), 10); if (remaining < 100) { console.warn( `Rate limit low: ${remaining} requests remaining. Resets at ${new Date(resetAt * 1000).toISOString()}` ); } if (response.status === 429) { const retryAfter = resetAt - Math.floor(Date.now() / 1000); throw new Error(`Rate limited. Retry after ${retryAfter} seconds.`); } return response.json(); }

Best Practices

  • Batch operations where possible using batchRecordOperations instead of individual mutations
  • Cache responses on the client side to reduce redundant requests
  • Use webhooks or subscriptions for real-time updates instead of polling
  • Distribute requests across multiple API keys if needed for different services

Query Complexity Limits

To prevent expensive queries from degrading performance, Foir enforces limits on query complexity.

Limits

LimitDefaultDescription
Max depth25Maximum nesting depth of a query
Max complexity1000Maximum computed complexity score
Max aliases10Maximum number of field aliases in a single query

How Complexity Is Calculated

Each field in a query contributes to the complexity score. List fields (like records) have higher complexity because they can return many items. Nested references multiply the complexity of their children.

A simple query like this has low complexity:

query { recordByKey(modelKey: "page", naturalKey: "home") { id data } }

A deeply nested query with large lists has high complexity:

# This query may exceed complexity limits query { records(modelKey: "category", limit: 50) { items { data resolved { content # Resolved content adds complexity } } } }

Complexity Headers

Responses include headers showing the computed complexity:

HeaderDescription
X-GraphQL-Max-ComplexityComputed complexity score for the query
X-GraphQL-Max-DepthNesting depth of the query

Reducing Complexity

If your query exceeds complexity limits:

  1. Reduce list sizes — lower the limit argument on list queries
  2. Remove unnecessary fields — only request the fields you need
  3. Avoid deep nesting — flatten your query or split into multiple requests
  4. Use pagination — fetch smaller pages of results rather than large batches
  5. Skip resolution when unnecessary — omit the resolved field if you only need raw data

HTTP Status Codes

The GraphQL endpoint returns these HTTP status codes:

StatusDescription
200Success (including GraphQL-level errors in the response body)
400Malformed request (invalid JSON, missing query)
401Missing or invalid authentication
403Insufficient permissions
429Rate limit exceeded
500Internal server error

Note that GraphQL typically returns 200 even when there are field-level errors. Always check the errors array in the response body in addition to the HTTP status code.

Error Handling Example

A robust error-handling pattern for API clients:

async function foirRequest(query, variables) { const response = await fetch("https://api.foir.io/graphql", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.FOIR_API_KEY, }, body: JSON.stringify({ query, variables }), }); // Handle HTTP-level errors if (response.status === 429) { const resetAt = response.headers.get("X-RateLimit-Reset"); throw new RateLimitError(resetAt); } if (response.status === 401) { throw new AuthenticationError("Invalid API key"); } if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const result = await response.json(); // Handle GraphQL-level errors if (result.errors?.length > 0) { const error = result.errors[0]; const code = error.extensions?.code; switch (code) { case "NOT_FOUND": return null; case "VALIDATION_ERROR": throw new ValidationError(error.message, error.extensions.validationErrors); case "FORBIDDEN": throw new ForbiddenError(error.message); default: throw new Error(error.message); } } return result.data; }
Last updated on