Scopes
What a key is allowed to do, and how to give it as little as possible.
A key carries scopes as resource: [actions]. They are checked on every request,
and a missing one is a 403 naming exactly what was needed:
{
"error": {
"code": "insufficient_scope",
"message": "This API key lacks the session:create scope",
"required": "session:create"
}
}The vocabulary
Every scope below gates at least one endpoint. Which one a given endpoint needs
is on its reference page, and in the spec as x-required-scope.
| Resource | Actions | What it covers |
|---|---|---|
form | read, write, publish | Reading forms, editing documents, publishing versions |
response | read, write, export | Creating and reading responses, and requesting exports |
session | create, write, read | Conversations |
webhook | read, write | Delivery endpoints |
file | read, write | Uploads and downloads |
analytics | read | Aggregates |
ai | generate | Generating or editing a document with a model |
payment | read, write | Connected payment gateway accounts (Cashfree, Razorpay, Stripe) |
ai:generate is the only scope that costs money to use. A generation spends your
plan's ai_generations allowance and the tokens it takes, so it is a separate
resource rather than part of form:write: a key that writes documents your own code
composed should not also be able to start spending your model budget. It is not in
the agent preset for the same reason — ask for it deliberately.
Two more names exist in the vocabulary that GET /api/keys/scopes reports, and
no endpoint requires either: response:read_partial and response:delete.
Granting them changes nothing. They are there because the dashboard's own roles
are written in the same vocabulary — a viewer who may read completed responses
but not unfinished ones is a real distinction inside the product, and it has no
API surface yet.
Reading unfinished responses is gated by your plan, not by a scope:
GET /v1/forms/{id}/responses?status=partial answers 402 on a plan without
partial responses, whatever the key holds.
Defaults
A new secret key gets form:read, response:read and all of session — enough
for most integrations, and nothing destructive. Ask for more explicitly.
Publishable keys have a ceiling, not a default: form:read, session:* and
file:write, and no amount of asking will widen it. That is what makes them safe
to ship in a page.
Because the ceiling is a hard one, an endpoint guarded by anything outside it is
secret-key-only in fact, so each reference page lists only the key types that
can really reach it. Most of the /v1 surface needs sk_; what a publishable
key can reach is sessions, blocks, a published form's config, and uploading a
file. The reference page for an endpoint is the authority on which key types it
accepts.
What no key can do
Some things are unreachable by any key, whatever its scopes say: minting keys, changing your plan, reading the audit log, managing team members. Those need a signed-in person with the right role.
This is deny-by-default — a capability is unreachable until it is deliberately mapped — so a new dashboard feature is never accidentally exposed to every existing key.
Pinning a key to specific forms
A key can be restricted to a list of forms. Anything outside that list returns 404, not 403, so a restricted key cannot be used to discover which form ids exist.
Useful when you are handing a key to a contractor, a client, or a single deployment that only has business with one form.
Least privilege in practice
- A script that exports responses nightly:
response:read,response:export. - A signup page: a publishable key, pinned to the signup form.
- A form-authoring pipeline:
form:read,form:write,form:publish— and nothing that can read anyone's answers.