GuidesScopes & Permissions

Scopes & Permissions

The 16 Forbidden Finance API scopes, how write scopes imply read scopes, the two-switch rule that gates every write, and how redaction and exclusion narrow a connection further.

Overview

A connection's permissions are three separate things, and all three have to line up before a request succeeds:

  1. Scopes — which areas of data this connection may touch, and whether it may change them.
  2. The Allow changes switch — a per-connection opt-in that arms writes at all.
  3. Privacy settings — which fields are stripped, and which accounts or categories are hidden.

Scopes and the switch decide whether an operation runs. Privacy settings decide what it can see and touch. This page covers all three.

API access is a Premium feature and is currently in limited beta. The scope list may grow while the beta is open; it will not silently change meaning.

The 16 scopes

These are the exact scope strings the authorization server advertises in scopes_supported, alongside the label the user sees on the consent screen. Read scopes are pre-checked at consent; write scopes are off by default.

ScopeConsent labelGrants
accounts:readAccountsAccount list, one account, balances
sync:readConnection sync statusConnection list, aggregate sync health
transactions:readTransactionsTransaction list, one transaction, search
transactions.annotate:writeCategorize & annotate transactionsAnnotate one transaction, bulk categorize
transactions:writeEdit transactions (add, change, delete)Create, full edit, delete — plus everything annotate covers
categories:readCategoriesCategory list
categories:writeManage custom categoriesCreate, update, delete custom categories
budgets:readBudgetsBudget list, one budget, budget progress
budgets:writeChange budgetsSwitch the active budgeting method
goals:readGoalsGoal list, one goal, progress, history, contributions
goals:writeChange goalsUpdate a goal, record a contribution
net_worth:readNet worthNet-worth summary and history
investments:readInvestmentsHoldings
liabilities:readLiabilities & debtLiabilities, debt summary, debt payoff plan
bills:readBills & recurringDetected recurring rules, upcoming bills
insights:readSpending insightsSpending by category, income vs expenses, month summary, cash-flow forecast

Two mappings surprise people:

  • Connections belong to sync:read, not accounts:read. Listing bank connections and reading aggregate sync health both need sync:read. If you are resolving a transaction all the way back to its bank, you need transactions:read, accounts:read, and sync:read.
  • The debt payoff planner needs liabilities:read, and nothing more. It computes a plan and writes nothing, so it is a read despite being a POST.

Write scopes imply their read scopes

Holding a write scope automatically carries the read scope beneath it. You never need to ask for both.

Granting thisAlso gives you
transactions.annotate:writetransactions:read
transactions:writetransactions.annotate:write, and therefore transactions:read
categories:writecategories:read
budgets:writebudgets:read
goals:writegoals:read

Implication is transitive and is resolved server-side on every request, so a connection granted transactions:write can annotate without transactions.annotate:write ever appearing in its stored scope list.

Two sizes of transaction write

Transaction writes are deliberately split, because "help me tidy up my categories" and "you may change what I spent" are not the same request.

transactions.annotate:writetransactions:write
Set category, tags, memo, locationYesYes
Bulk categorizeYesYes
Change amount, merchant, description, dateNoYes
Create a transactionNoYes
Delete a transactionNoYes
Endpoints reachedPOST /v1/transactions/{id}/annotate, POST /v1/transactions/categorizethose two, plus POST /v1/transactions, PATCH /v1/transactions/{id}, DELETE /v1/transactions/{id}

A connection holding only the narrow scope gets insufficient-scope on create, PATCH, and delete. That refusal is the point: it is a promise you can make to yourself in writing.

For anything unattended — a categorizing job, a cron script, an AI assistant left to work on its own — grant transactions.annotate:write and stop there. It covers the whole of the categorize-and-tidy use case, and it structurally cannot change an amount or remove a row. Reach for transactions:write only when you genuinely need to create or delete transactions.

categories:write is separate again, and narrow by construction: it creates, renames, re-parents, restyles, and deletes custom categories. The seeded system categories are refused with 409 conflict — the rest of the app keys on that tree, so it is not a programmatic caller's to rewrite.

The two-switch rule

A write scope on its own does nothing. Every write requires both:

  1. the relevant :write scope on the connection, and
  2. the connection's Allow changes switch turned on.

They are set at different moments and answer different questions. Scopes say what kind of change is permitted; the switch says whether this connection may change anything at all. It is off by default on a new connection, and a user can flip it off later without touching the scope list — which instantly makes the connection read-only while leaving its configuration intact for when they turn it back on.

SituationResult
No write scope, switch offinsufficient-scope (403)
No write scope, switch oninsufficient-scope (403)
Write scope granted, switch offwrites-disabled (403)
Write scope granted, switch onThe write runs

Handle writes-disabled distinctly from insufficient-scope in your client. They mean different things to the person who has to fix them: one is "ask for more permission", the other is "flip the switch you already have".

On the MCP surface the same rule shows up as visibility: a write tool appears in tools/list only when the connection holds its scope and has changes enabled. An assistant does not see a tool it would be refused. See MCP Server Reference.

Privacy narrows what a scope reaches

Scopes are coarse — a whole area of data. Privacy settings cut into that area, and they are applied on Forbidden Finance's servers, after the scope check.

Redaction removes individual fields. There are 18 of them, and the list is fixed: a connection cannot ask for a field the registry does not name.

AreaFields
TransactionsMerchant name, description, amount, tags, check number, external reference, memo, purchase location
AccountsAccount name, account number mask, balance
Bank connectionsInstitution, connection error message
InvestmentsHolding name & ticker, holding value
LiabilitiesLiability name, liability balance
GoalsGoal name & description

A redacted field is omitted from the payload — not blanked, not masked. Its id appears in the envelope's redacted_fields so you know something was removed.

Exclusion hides whole accounts or categories. An excluded row is absent from every list and from search, and asking for it by id returns the same not-found a nonexistent id returns.

What that means for writes

A write cannot change what the connection is not allowed to read. Attempt one and the whole request is refused with forbidden (403), carrying a single fixed message:

This connection's privacy settings hide a field this request would change.

It names no field. Which fields are hidden is itself part of the configuration, so a per-field answer would let a caller enumerate the privacy settings by probing writes one field at a time. The refusal is the same string every time, for every caller, in every case.

The practical consequence: read the connection's own capabilities from what it actually returns. If transaction.merchant shows up in redacted_fields, do not try to set a merchant.

Checking what you were granted

Nothing about a connection's scopes is secret from the connection itself. The most reliable check is the cheapest one: make the read you plan to build on and see whether it succeeds. A 403 with insufficient-scope means the scope is absent; a 200 whose envelope carries redacted_fields or filtered: true means the scope is present but narrowed.

For OAuth clients, the authorization server publishes the full scope vocabulary at https://api.403fin.io/.well-known/oauth-authorization-server under scopes_supported. Request the narrowest set that does the job — the consent screen shows the user exactly what you asked for, and a shorter list is approved more often than a long one.

Frequently Asked Questions

Do I need to ask for a read scope alongside its write scope?

No. A write scope carries its read scope automatically, and the implication is transitive — transactions:write reaches annotate and read without either being listed.

The user turned on Allow changes but my write still fails with insufficient-scope. Why?

The switch arms writes; it does not grant them. The connection also needs the specific :write scope for that operation. Have the user edit the connection's scopes under Settings > AI & API Connections.

Can I request a scope the user did not grant, and get it added?

No. A connection receives exactly what the user approved at consent, and it cannot widen its own access. Changing scopes is something the user does in the app.

Why did my write get a 403 with no field named?

A field the request would have changed is hidden by this connection's privacy settings. The message deliberately does not say which one — naming it would let a caller map the hidden set by probing. Check the envelope's redacted_fields on a read of the same resource.

Is there a scope for moving money, or for bank credentials?

No. There is no such operation on this API at all. The write surface is transactions, categories, budgets, and goals — nothing reaches payments, transfers between institutions, or the credentials behind a bank connection.

Getting Started with the API

Auth, envelopes, errors, pagination, and limits.

Writing Data

Idempotency, versioning, and every write endpoint.

Data Privacy & Redaction

The full privacy-gateway model.

AI & API Connections

Create and manage connections in the app.

Need more help? Contact us at [email protected].