Skip to Content
API ReferenceAuthentication

Authentication

All Foir API requests require authentication. This page covers API key types, authentication methods, scopes, customer JWT authentication, and security best practices.

API Key Types

Foir uses two categories of API keys — public and secret — each available in live and test modes.

Public Keys

Public keys are designed for use in frontend applications. They are restricted to read-only scopes by default.

PrefixModeBehavior
pk_live_ProductionReturns published content only
pk_test_TestReturns latest draft content (for preview and development)

Secret Keys

Secret keys provide full read/write access to the API and must only be used server-side.

PrefixModeBehavior
sk_live_ProductionFull API access against live data
sk_test_TestFull API access for testing and development

Secret keys should never be exposed in client-side code, browser bundles, or public repositories.

Authentication Methods

Include the API key in the x-api-key header:

curl -X POST https://api.foir.io/graphql \ -H "Content-Type: application/json" \ -H "x-api-key: pk_live_abc123..." \ -d '{"query": "{ recordByKey(modelKey: \"page\", naturalKey: \"homepage\") { id data } }"}'

API Key Scopes

Each API key can be configured with specific scopes to limit what it can access. Scopes are assigned when creating or editing a key in the dashboard.

ScopeDescription
records:readRead records and models
records:writeCreate and update records
records:deleteDelete records
records:publishPublish and unpublish record versions
drafts:readRead unpublished draft content via preview: true (SECRET keys only)
files:readRead files and media metadata
files:writeUpload and update files
files:deleteDelete files
search:readFull-text and semantic search
search:writeManage search indexes and embeddings
schemas:readRead project schemas (for code generation)
operations:executeExecute custom operations
analytics:readRead analytics data
notifications:readRead customer notifications
schedules:readRead schedules
extensions:manageManage extension configurations

Public keys (pk_*) are automatically restricted to a safe subset of read-only scopes. You cannot grant write scopes to a public key.

Customer JWT Authentication

For customer-facing features (authenticated user areas, customer-owned records, personalized content), combine an API key with a customer JWT.

After a customer logs in via the Customer Auth mutations, they receive an access token. Include both the API key and the customer token in subsequent requests:

curl -X POST https://api.foir.io/graphql \ -H "Content-Type: application/json" \ -H "x-api-key: pk_live_abc123..." \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \ -d '{"query": "{ currentUser { id email } }"}'
const response = await fetch("https://api.foir.io/graphql", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.NEXT_PUBLIC_FOIR_API_KEY, "Authorization": `Bearer ${customerAccessToken}`, }, body: JSON.stringify({ query, variables }), });

The API key identifies the project and the JWT identifies the customer. Requests with a valid customer token can access customer-specific queries like currentUser and records scoped to that customer.

Preview Mode Access

Pass preview: true on any read query to receive the latest (working) version of a record instead of the published one. Preview reads are gated behind the drafts:read scope, which can only be granted to SECRET (sk_) keys — public (pk_) keys are rejected at key-creation time, since they’re designed to be embeddable in browser bundles and would leak unpublished content.

query PreviewPage { page(naturalKey: "home", preview: true) { _id _hasDraft blocks { __typename } } }

To set this up:

  1. Create a SECRET (sk_) key in the dashboard with records:read and drafts:read scopes.
  2. Store the key in a server-only environment variable (Hydrogen Oxygen private env, Next.js server runtime config, etc.) — never expose it to the browser.
  3. Pass preview: true from your server-rendered route when the storefront’s preview flag is on.

If a request passes preview: true without drafts:read, the API responds with an authorization error rather than silently returning published content — so a misconfigured preview deploy fails loudly instead of quietly serving stale data.

Detecting drafts without loading them

Records resolved with a key that holds drafts:read carry a _hasDraft system field that reports whether unpublished changes exist. The field is omitted entirely for pk_ keys, so its mere presence in a response is itself information that callers without drafts:read cannot observe.

Cache safety

Responses that honour preview: true come back with Cache-Control: private, no-store, must-revalidate, preventing CDNs and shared proxies from caching draft content even if a misconfigured upstream tries to.

Security Best Practices

Use Environment Variables

Never hardcode API keys in source code. Store them in environment variables:

// Node.js / Next.js const apiKey = process.env.FOIR_API_KEY; // Vite const apiKey = import.meta.env.VITE_FOIR_API_KEY;

Keep Secret Keys Server-Side

Secret keys (sk_*) must only be used in server-side code — API routes, server components, build scripts, or backend services. Never include them in client-side bundles.

// Next.js App Router -- Server Component (safe) async function getPageData(slug) { const response = await fetch("https://api.foir.io/graphql", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": process.env.FOIR_SECRET_KEY, // Server-only env var }, body: JSON.stringify({ query: `query GetPage($slug: String!) { recordByKey(modelKey: "page", naturalKey: $slug) { id resolved(locale: "en-US") { content } } }`, variables: { slug }, }), }); return response.json(); }

Client-Side Key Safety

If you need to use API keys in client-side code:

  1. Use public keys only (pk_live_ or pk_test_)
  2. Enable domain restrictions in the API key settings
  3. Configure rate limiting per key
  4. Monitor usage in the admin dashboard

Key Rotation

Rotate API keys periodically to reduce risk:

  1. Create a new key with the same scopes
  2. Update your application to use the new key
  3. Verify the new key works in production
  4. Disable the old key
  5. Delete the old key after a grace period

Troubleshooting

UNAUTHENTICATED Error

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

Common causes:

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

FORBIDDEN Error

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

Common causes:

  • API key lacks the required scope for the operation
  • Domain restriction mismatch (key is restricted to specific domains)
  • Public key attempting a write operation
  • Customer JWT expired or invalid for the requested resource

Invalid Token Error

{ "errors": [{ "message": "Invalid or expired token", "extensions": { "code": "UNAUTHENTICATED" } }] }

Common causes:

  • Customer access token has expired (use the refresh token to obtain a new one)
  • Token was issued for a different project
  • Token has been revoked via logout
Last updated on