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.
| Prefix | Mode | Behavior |
|---|---|---|
pk_live_ | Production | Returns published content only |
pk_test_ | Test | Returns 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.
| Prefix | Mode | Behavior |
|---|---|---|
sk_live_ | Production | Full API access against live data |
sk_test_ | Test | Full API access for testing and development |
Secret keys should never be exposed in client-side code, browser bundles, or public repositories.
Authentication Methods
Header Authentication (Recommended)
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.
| Scope | Description |
|---|---|
records:read | Read records and models |
records:write | Create and update records |
records:delete | Delete records |
records:publish | Publish and unpublish record versions |
drafts:read | Read unpublished draft content via preview: true (SECRET keys only) |
files:read | Read files and media metadata |
files:write | Upload and update files |
files:delete | Delete files |
search:read | Full-text and semantic search |
search:write | Manage search indexes and embeddings |
schemas:read | Read project schemas (for code generation) |
operations:execute | Execute custom operations |
analytics:read | Read analytics data |
notifications:read | Read customer notifications |
schedules:read | Read schedules |
extensions:manage | Manage 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:
- Create a SECRET (
sk_) key in the dashboard withrecords:readanddrafts:readscopes. - 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.
- Pass
preview: truefrom 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:
- Use public keys only (
pk_live_orpk_test_) - Enable domain restrictions in the API key settings
- Configure rate limiting per key
- Monitor usage in the admin dashboard
Key Rotation
Rotate API keys periodically to reduce risk:
- Create a new key with the same scopes
- Update your application to use the new key
- Verify the new key works in production
- Disable the old key
- Delete the old key after a grace period
Troubleshooting
UNAUTHENTICATED Error
{
"errors": [{
"message": "Authentication required",
"extensions": { "code": "UNAUTHENTICATED" }
}]
}Common causes:
- Missing
x-api-keyheader - 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