Authentication
Every request carries an API key. Keys are created in the app under API keys → Create key, by owners and admins of the organisation. A key belongs to exactly one workspace, and its secret is shown once, when it is created; afterwards the app shows only its first characters (the prefix).
Authorization: Bearer ff_live_3f9c...
X-API-Key: ff_live_3f9c... is accepted as well; when a request sends both, Authorization counts. A key is ff_live_ or ff_test_ followed by 43 letters and digits.
When a key is refused
| Answer | Why |
|---|---|
401 unauthorized | No key was sent (the answer carries WWW-Authenticate: Bearer), the key is not in the ff_live_…/ff_test_… format, or it is unknown or revoked. detail says which. |
403 forbidden | The key lacks the scope the endpoint needs (This API key lacks the "template:write" scope). |
403 forbidden | The request comes from a web page (it has an Origin header). Keys are for servers; see From a browser. |
403 forbidden | The plan does not include the feature; the problem names it in feature (post_processing, office, einvoice, channels …). See Plan limits. |
Test and live keys
The prefix tells you the environment. You pick it when you create the key (Test or Live).
ff_live_… | ff_test_… | |
|---|---|---|
| Use it for | Production | Development, CI, staging, the dev kit |
| Units | Counted against the plan (units) | Not counted. Test e-invoices are limited to a number per day instead |
| When the Free plan's units are used up | Renders stop with 402 quota_exceeded | Renders go on |
| Outputs kept | The plan's retention, or the workspace's shorter one (retention) | At most 24 hours |
| Test mark on the file | Never | On the Free plan on every test file; on every plan on a test e-invoice |
| Deduplication | Yes | No, every request renders |
version: "latest" (the draft) | Refused with 400 invalid_request | Allowed |
| Word and PowerPoint templates | From the Starter plan on | On the Free plan the PDF only (403 test_output_unavailable for docx and pptx) |
| Rate limit | The plan's requests per second | The same bucket as live keys, plus 30 requests per minute on the Free plan |
| Webhook events | Delivered to every endpoint that has live in its environments | Delivered to every endpoint that has test in its environments (both are the default) |
Everything else is identical: the same endpoints, the same templates, the same output. Switching to production means swapping the key.
Scopes
A key can do only what its scopes allow. Pick them when you create the key; a ✓ marks the ones the dialog ticks by default.
| Scope | Allows | Default |
|---|---|---|
render:create | POST /renders, POST /renders/batch, the PDF tools POST /pdf/merge, /pdf/protect, /pdf/watermark and /pdf/convert, the compatibility endpoints | ✓ |
render:read | GET /renders, GET /renders/{id}, GET /renders/{id}/result, GET /jobs/{id}, POST /pdf/info | ✓ |
render:input | GET /renders/{id}/input, the stored request of a render (formfeed renders pull); every read is audited. See Reproduce a failed render | |
template:read | Reading templates, versions, files, schemas and channels, POST /templates/{id}/validate, reading the brand kit and shared partials (formfeed templates pull) | ✓ |
template:write | Creating, updating, publishing and archiving templates, moving release channels, writing shared partials (formfeed templates push) | |
webhook:manage | Managing webhook endpoints (needed by the triggers of the no-code integrations), resending events to them, and live events in formfeed listen --live | |
webhook:listen | formfeed listen sessions and resending events to them. See Develop webhooks locally | |
account:read | GET /account, GET /usage (formfeed whoami) | ✓ |
file:read | GET /files, GET /files/{id} (the workspace file library) | |
file:write | POST /files (upload to the library) | |
file:delete | DELETE /renders/{id}/outputs, DELETE /files/{id} | |
workspace:delete | DELETE /workspaces/{id} for any workspace of the key's organisation, its own included (formfeed workspaces delete). Never part of a default or of a CLI login key |
A write scope comes with its read scope: ticking render:create in the dialog ticks render:read too, template:write ticks template:read and file:write ticks file:read, because writing answers with the object and a key should be able to read back what it wrote.
Give each integration its own key with the smallest set of scopes it needs: a backend that only renders needs render:create and render:read; the key in your CI publish step needs template:write.
Keeping keys safe
- Keep keys on the server. The API refuses a key sent from a web page (From a browser), and a key in a mobile app or a browser bundle is public anyway: anyone who finds it can render on your account.
- Store keys in your secret manager or environment (
FORMFEED_API_KEY), never in the repository. - Name keys after where they live (
billing-service-prod,github-actions). The list under API keys shows when each key was Last used, so an unused key is easy to spot and revoke. - Rotate without downtime: create the new key, deploy it, check that the old key's last used time stops moving, then revoke the old key (Revoke key). A revoked key stops working within a minute, because the API keeps a key's details for up to 60 seconds.
The app and AI agents
The app and AI agents connected through the MCP server with a Formfeed sign-in do not use API keys. They send the member's session token as Authorization: Bearer …, and the app names the workspace in X-Formfeed-Workspace. Such a token can do what the member's role allows. An agent's token is narrowed further, to render:create, render:read, template:read and account:read: an agent cannot publish, delete, manage webhooks or render a draft (latest), whatever the member's role. If the member allows it for that agent, and their role may edit templates, the agent may also save templates as drafts (POST /templates and POST /templates/{id}/versions without publish; see Template drafts). If the member uses two-factor authentication, an agent works only after the member has confirmed it in Formfeed. You do not need any of this for your own integrations: use an API key.
CLI login
formfeed login uses a device flow instead of a pasted key: the CLI shows a code, you approve it in the browser at app.formfeed.dev/device and pick a workspace, and the CLI receives a live key (ff_live_…) named Formfeed CLI on <machine>, with the scopes of your role in the organisation. Renders made with it are live renders and count against the plan's units; use formfeed login --api-key ff_test_… with a test key when you only try things out.
| Role | Scopes of the CLI key |
|---|---|
| Viewer | render:read, template:read, account:read, file:read |
| Editor | the viewer's, plus render:create, template:write, file:write, render:input, webhook:listen |
| Admin and owner | the editor's, plus webhook:manage and file:delete |
The key never gets workspace:delete. It appears under API keys like any other and can be revoked there. formfeed logout only removes the key from your machine; it stays valid until you revoke it in the app. In CI, set FORMFEED_API_KEY instead. See CLI installation.