forbidden
HTTP 403 · code: "forbidden" · type: https://docs.formfeed.dev/errors/forbidden
The key is valid, but this request is not allowed for it. There are three kinds of refusal, and the problem tells them apart:
- A missing scope.
detailnames it: This API key lacks the "template:write" scope. - A plan limit. The problem carries
feature(what the plan lacks),plan(the plan of the organisation) and, where the plan allows a number,limit. - A caller that may not do this: a call from a browser, a connected AI agent, or a member without the role.
detailsays which.
Example
A missing scope:
{
"type": "https://docs.formfeed.dev/errors/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "This API key lacks the \"template:write\" scope",
"code": "forbidden",
"request_id": "req_4b7c1e9a2f3d4c5b8a6e7f9012"
}
A plan limit:
{
"type": "https://docs.formfeed.dev/errors/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "PDF post-processing (merge, password, watermark) is not part of the free plan; it starts with Starter",
"code": "forbidden",
"feature": "post_processing",
"plan": "free",
"request_id": "req_4b7c1e9a2f3d4c5b8a6e7f9012"
}
Common causes
Missing scopes:
- Pushing or publishing templates with a key that has only the default scopes (
template:writeis needed). - Managing webhooks without
webhook:manage, for example when Zapier or n8n registers a trigger. - Reading usage without
account:read, deleting outputs withoutfile:delete, reading a render's stored request withoutrender:input, or deleting a workspace withoutworkspace:delete.
Plan limits (feature):
feature | Refused | Allowed from |
|---|---|---|
post_processing | /pdf/merge, /pdf/protect, /pdf/watermark, or a post block with merge_after, watermark or password. /pdf/info works on every plan. | Starter |
office | /pdf/convert, and live renders of Word and PowerPoint templates. On Free, test renders of these templates make the marked PDF. | Starter |
channels | Rendering or setting a named release channel on Free; on Starter and Pro, a new channel beyond the plan's number per template (with limit: 2 on Starter, 10 on Pro and Business). | Starter |
canary | A channel with a canary. | Pro |
byo_storage | Connecting a bucket for bring your own storage. | Pro |
templates | A new template, a copy or a restored one when the organisation already has the plan's number (limit: 5 on Free). Archived templates do not count. | Starter |
webhooks | A new webhook endpoint when the organisation already has the plan's number (limit: 1 on Free, 5 on Starter), disabled ones included. formfeed listen sessions do not count. | Pro |
einvoice | An e-invoice from a live render on Free; or from a test render once the day's test e-invoices are used up (Free 20, Starter 100, Pro 500, Business 2,000). The second answer carries limit, used and resets_at, the start of the next UTC day. | Starter (live) |
Callers that may not do this:
- A request from a web page (
detail: This API key may not be used from a browser origin): an API key only works server-side; see From a browser. Microsoft's Power Automate, Power Apps and Copilot Studio portals are not affected; their connector calls go through Microsoft's servers. - A connected AI agent (an MCP client signed in with OAuth) doing what only a person may do (
detail: A connected agent may not do this; sign in to Formfeed and do it there), or an agent of an account with two-factor authentication that the account never confirmed. - The routes under
/workspaces/{id}/storageand the brand kit's logos: only owners and admins signed in to the app manage them, never an API key.
How to fix it
- For a scope:
detailnames it. Create a key that has it; scopes cannot be added to an existing key. The scope table says which endpoint needs which scope. - For
post_processing,office,channels,canary,byo_storage,templatesorwebhooks: upgrade the plan. Test keys on a paid plan can try a feature for free. Forchannels, you can also keep rendering and moving thepublishedchannel, which every plan has; fortemplates, archive a template you no longer use; forwebhooks, delete an endpoint you no longer use (a no-code trigger removes its own when it is switched off). - For
einvoice: render with a test key to try it, wait untilresets_atwhen the day is used up, or render live on Starter and above, which has no daily number. To get a plain PDF from a template that declares an e-invoice, send"post": { "einvoice": false }. - For a browser call: call the API from your backend and hand the browser the result, for example the render's
download_url. - For an agent or the app-only routes: do it in the app, signed in as an owner or admin.
Retry?
No. The same request gets the same answer. Fix what the problem names, then send the request again with a new Idempotency-Key or without one: a 4xx answer is stored under its key for 24 hours, so the old key returns this problem again. See retrying.