Skip to main content

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. detail names 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. detail says 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:write is needed).
  • Managing webhooks without webhook:manage, for example when Zapier or n8n registers a trigger.
  • Reading usage without account:read, deleting outputs without file:delete, reading a render's stored request without render:input, or deleting a workspace without workspace:delete.

Plan limits (feature):

featureRefusedAllowed 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
channelsRendering 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
canaryA channel with a canary.Pro
byo_storageConnecting a bucket for bring your own storage.Pro
templatesA 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
webhooksA 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
einvoiceAn 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}/storage and 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: detail names 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, templates or webhooks: upgrade the plan. Test keys on a paid plan can try a feature for free. For channels, you can also keep rendering and moving the published channel, which every plan has; for templates, archive a template you no longer use; for webhooks, 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 until resets_at when 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.