API Keys
API keys let your applications connect to Foir’s public API. Foir provides two types of keys — public and secret — distinguished by their prefix, giving you fine-grained control over access and permissions.
Overview
Every request to the Foir API requires an API key. Keys are scoped to a project and carry specific permissions that determine what operations the caller can perform.
Key Types
The key’s prefix tells you its type — there is nothing else to choose. Keys look like pk_<hex> and sk_<hex>.
| Type | Prefix | Use For | Capabilities |
|---|---|---|---|
| Public | pk_ | Frontend, client-side code | Read-only, published content only |
| Secret | sk_ | Backend, server-side code | Full read/write; can hold additional scopes |
Public keys are safe to include in browser code, mobile apps, and any client-side application. They are read-only and can read only published content — they cannot be granted drafts:read or any secret-only scope (the API rejects such scopes on a pk_ key at creation time).
Secret keys must never be exposed to browsers or client-side code. They can create, update, delete, and publish content, and can be granted additional scopes (including drafts:read). The API rejects a sk_ key on any request that carries a browser Origin / Sec-Fetch-Site.
Tenant keys (pk_t_ / sk_t_) are multi-project variants: one key reaches an explicit allow-list of projects within the same tenant, and each request names the target project with an X-Project-Id header. Use them when a single deploy serves several projects (for example a storefront fleet). They behave like their pk_ / sk_ siblings otherwise. See Tenant Keys.
Reading draft content
There is no separate “test” key for previewing unpublished work. To read draft / unpublished content, use a secret key that holds the drafts:read scope and pass preview: true on the query (you can alternatively select the draft schema/design-tokens channel with the X-Foir-Schema-Channel: draft request header). Without drafts:read, preview: true is rejected with a permission error. See Preview Mode Access.
In the Admin
Creating a Key
- Open API keys in the sidebar (under Settings)
- Click Create API Key
- Fill in the form:
- Name — something descriptive (e.g., “Production Frontend”, “Staging Backend”)
- Key Type — PUBLIC (
pk_) or SECRET (sk_) - Permissions — select the scopes this key needs
- Click Create
- Copy the key immediately — it is only shown once
Scopes and Permissions
Public Key Scopes
| Scope | Grants Access To |
|---|---|
records:read | Fetch published records |
files:read | Fetch a file by ID |
files:list | List files (own uploads + shared library media) |
search:read | Semantic search (searchRecords, per-model search<Model>s) |
search:semantic:read | Read embedding coverage / similar-record queries |
schemas:read | Read project models / schemas |
notifications:read | Read customer notifications + preferences |
schedules:read | Read schedules |
customers:read | List / read customers and invitations |
operations:read | Read operation executions |
Secret Key Scopes
All public scopes, plus:
| Scope | Grants Access To |
|---|---|
records:write | Create and update records |
records:publish | Publish records |
records:delete | Delete records |
drafts:read | Read draft content via preview: true (SECRET keys only) |
files:write | Upload files |
files:delete | Delete files |
files:list.all | List every customer’s files, unfiltered (SECRET keys only) |
operations:execute | Execute all operations (execute<Op>, cancel executions) — see Specific operations only to grant individual operations instead |
search:semantic:write | Generate / write / delete embeddings |
notifications:write | Send notifications, register device tokens |
customers:write | Set passwords, manage customer invitations |
secrets:read.project | Read project-scoped secret plaintext via getSecret (SECRET keys only) |
secrets:put.project | Write project-scoped secrets via putSecret (SECRET keys only) |
secrets:delete.project | Soft-delete project-scoped secrets via deleteSecret (SECRET keys only) |
The drafts:read, files:list.all, and secrets:*.project scopes are restricted to secret keys at key-creation time — public keys (pk_*) are designed to be embeddable in browser bundles and would leak unpublished or privileged content. See Preview Mode Access.
Per-model record scopes (records:read:<model>, records:write:<model>, etc.) are reserved for scoped Ed25519 tokens and are rejected on API keys at creation time — keys must use the unscoped records:read / records:write / records:delete / records:publish forms. To bound an API key to specific models, use the model allow-list instead.
Restricting a Key’s Reach
Scopes control what kind of access a key has (read, write, execute). Two further selectors in the key form control which resources that access reaches. Both follow the same pattern: All (the default — no restriction) or Specific, with a picker. Use them to mint least-privilege keys for a frontend: the key exposes exactly the fields that application uses and nothing else.
Specific operations only
When a key holds operations:execute, an Operation Access selector appears:
- All operations — the key can execute every operation in the project (today’s default, and the behavior of every key minted before this selector existed).
- Specific operations only — pick the operations this key may execute. Under the hood the key’s
operations:executescope is replaced with oneoperations:execute:<operation-key>entry per selected operation.
An operation left off the list isn’t just denied — its execute<Op> mutation disappears from the schema the key sees, including introspection, so codegen against the key’s schema only ever sees what the key can call.
Two details to know:
- The selector covers mutation-shaped operations (the
execute<Op>surface). Query- and field-shaped operations are read surface and aren’t restricted per key. cancelOperationExecutionrequires the unrestrictedoperations:executescope — a key limited to specific operations can execute them but not cancel executions.
Specific models only
When a key holds any records scope, a Model Access selector appears (this is about API key reach — not to be confused with a model’s access config, which declares record ownership):
- All models — the key reaches every model its scopes allow.
- Specific models only — pick the models. The key’s record reads/writes and per-model search are bounded to that list: typed fields for other models disappear from the key’s schema and are denied if called.
A model-restricted key deliberately loses the generic record fields — record, records, recordVersions, searchRecords, and batchRecordOperations — because those can reach any model. Use the typed per-model fields (page, createPage, searchPages, …) instead; a restricted frontend shouldn’t need the generic escape hatches.
One caveat: the semantic-search scopes (search:semantic:read / search:semantic:write) are not model-bounded. If a restricted key must not surface other models’ content at all, don’t grant it semantic search.
Rotating a Key
If a key is compromised or needs to be changed:
- Open API keys in the sidebar (under Settings)
- Find the key and click Rotate
- Copy the new key
- Update your application
- The old key stops working immediately
Revoking a Key
To permanently disable a key:
- Open API keys in the sidebar (under Settings)
- Find the key and click Revoke
Revoked keys cannot be restored. Create a new key if needed.
Using Keys in Requests
Include the key in the x-api-key header:
curl -X POST https://api.foir.dev/graphql \
-H "Content-Type: application/json" \
-H "x-api-key: pk_..." \
-d '{"query": "{ pages(first: 10) { edges { node { _id } } } }"}'In JavaScript:
const response = await fetch('https://api.foir.dev/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.FOIR_API_KEY,
},
body: JSON.stringify({
query: `{ pages(first: 10) { edges { node { _id } } } }`
}),
});Environment Variables
Store keys in environment variables, never in code:
# .env.local (do not commit this file)
FOIR_PUBLIC_KEY=pk_...
FOIR_SECRET_KEY=sk_...The Foir CLI also uses the FOIR_API_KEY environment variable for headless authentication:
export FOIR_API_KEY=sk_...
foir records list page --jsonVia the CLI
# List all API keys for the current project
foir api-keys list
# Create a new API key (scopes are comma-separated)
foir api-keys create \
--name "Production Frontend" \
--scopes records:read,files:read,search:read
# Restrict execution to specific operations with per-operation scopes
foir api-keys create \
--name "Checkout Frontend" \
--scopes records:read,operations:execute:checkout,operations:execute:enrich_url
# Rotate a key (generates a new key, invalidates the old one)
foir api-keys rotate <id>
# Revoke a key (permanently disables it)
foir api-keys revoke <id>Rate Limits
Each API key has its own per-hour request budget. When exceeded, the API returns 429 Too Many Requests and the response carries a Retry-After header telling you how many seconds to wait.
- Limits are configured per key in the dashboard; the default for new keys is 10,000 requests per hour.
- The window is a sliding 60-minute window — bursts are flattened across the hour rather than refilling on a fixed cadence.
- Every successful response includes
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Resetheaders so you can pace your client. - Enterprise plans support higher per-key limits — contact support to raise yours.
See Errors & Rate Limits for the full header reference and a recommended client retry/backoff pattern.
Best Practices
Do:
- Use public keys for all client-side code (browsers, mobile apps).
- Store secret keys in environment variables on your server.
- Grant only the minimum scopes each key needs — and use the operation and model selectors to bound a frontend key to exactly the fields it uses.
- Use descriptive names so you can identify which application uses each key.
- Rotate keys on a regular schedule and immediately if compromised.
- Use a secret key with
drafts:read(pluspreview: true) when a server-rendered preview environment needs unpublished content.
Do not:
- Commit keys to version control.
- Use secret keys in browser or client-side code.
- Use one key everywhere — create separate keys per application and environment.
- Grant more permissions than necessary.