insufficient_scope
HTTP 403 type: authorization_error param: null{ "error": { "type": "authorization_error", "code": "insufficient_scope", "message": "This key holds scopes [invoices:read] and cannot write invoices. Required scope: invoices:write.", "param": null, "upstream": null, "doc_url": "https://docs.fiskhub.com/errors/insufficient_scope" }}What happened
The key authenticated successfully but does not carry the scope this operation requires. The message names the scopes it holds and the one it needs.
Nothing was fiscalized.
Should you retry?
Not with the same key. Scopes are fixed when a key is issued; they are not negotiated per request.
The fix
Use a key that holds the required scope, or issue one.
| Operation | Scope |
|---|---|
POST /v1/invoices, POST /v1/invoices/{id}/retry | invoices:write |
GET /v1/invoices, GET /v1/invoices/{id} | invoices:read |
POST / PATCH on companies, devices | fleet:write |
GET on companies, devices | fleet:read |
POST /v1/certificates | certificates:write |
GET /v1/certificates | certificates:read |
A write scope does not imply the matching read scope; grant both when a key needs both.
Scoping keys well
The point of scopes is that a key can only do what its holder needs, so a leak costs less.
- A device fiscalizing sales needs
invoices:writeand nothing else. It never reads the fleet and never touches certificates. A key that leaks off a machine in a public place should not be able to enumerate the estate. - A reconciliation job needs
invoices:readonly. It reads yesterday’s invoices and files a report; it must not be able to create one. - A provisioning script needs
fleet:write, typically alongsidecertificates:write, and runs from somewhere you control.
Resist the single all-scopes key shared by everything. It works until it is on a machine you no longer physically control, and then the blast radius is the whole account.