Scopes & irreversibility
How to read the required scope and irreversibility of each endpoint from the OpenAPI spec — the x-required-scope and x-irreversible extensions — and the catalog of irreversible operations.
Every public endpoint declares two pieces of safety metadata in the OpenAPI
spec: the exact scope it enforces and whether the operation can be undone.
Clients (the CLI, agents, your own tooling) read them to fail
fast — block a call when the key lacks the scope, confirm before an irreversible
action — instead of discovering the problem from a 403 or an unrecoverable
mutation.
Reading it from the spec
Each operation in the OpenAPI spec carries two custom extensions:
{
"operationId": "public-api.v1.invoices.delete",
"x-required-scope": "invoices:delete",
"x-irreversible": true
}x-required-scope— the singleresource:actionscope the API key must hold to call the operation. Present on every operation exceptGET /v1/me, which needs no scope: any valid credential can introspect itself.x-irreversible—trueonly on operations that cannot be undone. Absent (treated asfalse) on everything else.
These are x-* vendor extensions, so a generic OpenAPI viewer may not render
them — but any client that parses the spec (like the CLI) reads them directly.
Generate a client from the spec and you inherit both.
Scopes
Scopes are resource:action (e.g. invoices:read, contacts:delete) — the
same closed catalog the REST API and the API keys use. A request whose key
lacks the scope returns 403 with code: insufficient_scope. Request only the
scopes your integration needs.
One operation enforces a second scope on top of x-required-scope when a
field widens what it reads: POST /v1/companies/{company}/purchase-invoices with
source_purchase_invoice_id also needs purchase_invoices:read, because it
copies the lines of another document. See
Measured purchase lines.
Time-tracking operations carry their own scopes — employees:*,
time_entries:*, work_schedules:*, absences:*, presence:read,
holidays:read and payroll_exports:read — all gated behind the
control_horario module. See Time tracking.
Automations do the same with automations:read, automations:write,
automations:delete and automation_runs:read, gated behind the automations
module. See Automations.
The account model adds its own scopes. account:read, account:write and
account:owner cover the account itself (its keys, its usage and its ownership),
members:*, invitations:* and nif_transfers:* its team and its tax ID
transfers (they need the user_management module), and companies:* its tax ID
portfolio. See
Authentication › Account administration.
Two scopes are privileged: accounts:read and accounts:write. They let an
account provision, administer and charge managed accounts for its clients, and
they work differently from every other scope: the super-scope * does not cover
them, Factuarea grants the privilege account by account and checks it on every
request (403 privileged_scope_required
when it fails), and OAuth consent never grants them, so use an API key. See
Privileged scopes and
Managed accounts.
Transport control documents (DeCA) use transport_documents:read,
transport_documents:write (drafts and email delivery), transport_documents:delete
(drafts only) and transport_documents:transition (issue, modify, close, reopen,
cancel and supersede), gated behind the transport_documents module, which in turn
requires delivery_notes. The PDF of each version enforces pdfs:read; the proof of issuance
(GET /v1/companies/{company}/transport-documents/{transport_document}/issuance-proof) is not a PDF of the document but the
export of its issuance evidence, so it enforces transport_documents:read. See
Transport control documents.
Projects and tasks use projects:read|write|delete, tasks:read|write|delete,
users:read and notifications:read|write. projects:* and tasks:* are gated
behind the tasks module when the credential is issued: a key with those
scopes for a company without the module is rejected with
scope_not_allowed_by_plan, and without the scope each request gets
insufficient_scope. users:read and notifications:* are cross-cutting and
belong to no module. The two delete scopes are API-key-only: an OAuth consent
never grants them. Invoicing the hours of a project
(POST /v1/companies/{company}/projects/{project}/time-invoices) requires invoices:write, not a
project scope, because it creates an invoice. See
Projects and tasks.
The full catalog — every scope, what it grants, and the sensitive ones — is in
Scopes & permissions. A scope is not always derivable
from the resource name: some endpoints enforce a different scope than you'd
guess (PDF downloads enforce pdfs:read, payment methods enforce
invoices:read, state transitions enforce a :transition scope, VeriFactu
actions enforce verifactu:*). Always read x-required-scope rather than
inferring it.
An API key may hold the super-scope *, which satisfies any
x-required-scope except the privileged accounts:read and
accounts:write. * does not cover accounts:*: a key that holds only *
answers 403 insufficient_scope on those operations, however broad it looks.
List accounts:* explicitly, on an account that holds the privilege. See the
super-scope.
Irreversible operations
An operation marked x-irreversible: true has no undo: it deletes data,
emits a fiscal record, transitions a document to a terminal state, rotates or
revokes a secret, or changes who owns, reaches or pays for an account. The
CLI asks for a typed confirmation before
running one; your own tooling should guard them the same way.
These are the categories that carry x-irreversible: true:
| Category | Examples | Scope |
|---|---|---|
| Deletes (single) | contacts.delete, invoices.delete, products.delete, purchase_invoices.delete, webhook_endpoints.delete… | <resource>:delete |
| Catalog subresource deletes | products.presentations.delete, products.variants.delete, products.supplier-offers.delete, price-lists.delete, price-lists.items.delete, price-lists.items.purge_retired | products:write / price_lists:write |
| Bulk deletes | invoices.bulk_delete, contacts.bulk_delete, products.bulk_delete… | <resource>:delete |
| Task and project deletes | tasks.delete, tasks.bulk_delete, tasks.comments.delete, tasks.attachments.delete, tasks.time_entries.delete, task_labels.delete, projects.delete, projects.columns.delete, projects.custom_fields.delete — the nine irreversible operations of the module | tasks:delete / projects:delete |
| Fiscal emission / numbering | invoices.send, invoices.mark_sent, invoices.assign_real_number, invoices.corrective, invoices.substitute_simplified | invoices:send / invoices:write |
| Void / annul | invoices.void, invoices.annul | invoices:void |
| Terminal conversions | quotes.convert, proformas.convert, delivery_notes.convert | <resource>:transition |
| Cancel / sign | delivery_notes.cancel, delivery_notes.sign, recurring_invoices.cancel, recurring_invoices.generate | <resource>:transition / :write |
| VeriFactu (AEAT) | invoices.verifactu_create, verifactu.records.subsanar, verifactu.settings.update, verifactu.certificates.revoke | verifactu:write |
| Secret / certificate | webhook_endpoints.rotate_secret, verifactu.certificates.revoke | webhooks:write / verifactu:write |
| Tax ID transfer | account.nif_transfers.create, account.nif_transfers.accept, account.nif_transfers.revoke | nif_transfers:write |
| Account ownership and team | account.owner.transfer, account.members.delete | account:owner / members:write |
| Account keys and webhooks | account.api_keys.revoke, account.api_keys.rotate_secret, account.webhooks.delete, account.webhooks.rotate_secret | account:write / webhooks:delete / webhooks:write |
| Managed accounts (privileged) | managed_accounts.create, managed_accounts.claim_tokens.create, managed_accounts.management.end, managed_accounts.plan.update, managed_accounts.imports.create | accounts:write |
| GDPR forget | delivery_notes.signature_audits.forget | delivery_notes:gdpr_forget |
| Transport control documents | transport_documents.issue, transport_documents.modify, transport_documents.cancel, transport_documents.supersede, transport_documents.send | transport_documents:transition / transport_documents:write |
| FacturaE (B2G) submit | invoices.face_submissions.submit, face_submissions.cancel | facturae:write |
| Time-record seal | monthly_time_record_closes.seal | time_entries:write |
| Replay (re-execution) | automations.runs.replay, automations.runs.steps.replay, integrations.events.replay | automations:write / integration_events:write |
Removing a relation, an external link, an entity link or a label from a task is
not irreversible — each can be created again — and needs only tasks:write.
This list is the human-readable summary. The machine-readable source of
truth is x-irreversible in the spec — a client that reads it stays correct
even as the catalog grows.
Putting it together
A safe client does two checks before a mutation:
- Scope-check — does the key hold
x-required-scope? If not, stop locally (no wasted round trip). The CLI exits4; see scope-check. An operation without the extension (GET /v1/me) needs no check, and foraccounts:*the scope is necessary but not sufficient: the account must also hold the privilege, whichGET /v1/mereports inaccount.is_providerandaccount.privileged_scopes. - Irreversibility confirm — is
x-irreversibletrue? If so, confirm before calling. The CLI requires--confirm <id>; see irreversible operations.
The official SDKs and the CLI do both for you. If you generate your own client from the spec, wire these two checks yourself from the extensions.
Store permissions
Store management uses stores:read and stores:write; provider-specific scopes are woocommerce_store:read, woocommerce_store:write, shopify_store:read and shopify_store:write. Connection-test operations require the provider write scope. These scopes are assigned to API keys and are outside OAuth consent. See ecommerce stores for the operation matrix.