Authentication
API keys for your own team, OAuth with PKCE and dynamic client registration for integrators, and how scopes gate the tool surface.
Every request to https://api.superbooks.io/mcp carries a credential. There are
two kinds, and which one you want depends on who the code runs for.
| Use when | Setup cost | |
|---|---|---|
| API key | Your code acts on behalf of your own team — a script, a backend job, your own AI client. | Mint one in the app. |
| OAuth | You are building a product that other SuperBooks teams sign in to. | Register a client, run the authorization-code flow. |
API keys
Minting a key
API keys are created in the SuperBooks app, at Settings → Developer (app.superbooks.io/settings/developer). Choose a scope preset when you create the key:
| Preset | Grants |
|---|---|
| All | The apis.all meta-scope — every resource, read and write. |
| Read Only | The apis.read meta-scope — every resource, read only. |
| Restricted | Only the individual scopes you tick. |
The key is displayed once, at creation. It is stored hashed, so it cannot be
shown again — if you lose it, delete the key and mint another. Keys begin with
sb_.
An API key carries the full authority of its scopes over your team's financial data. Keep it in a secret store or an environment variable — never in source control, and never in client-side code, where anyone can read it.
Sending a key
Send it as a bearer token:
POST /mcp HTTP/1.1
Host: api.superbooks.io
Authorization: Bearer sb_your_api_key_here
Content-Type: application/json
Accept: application/json, text/event-streamx-api-key is accepted as an alias, for clients that will not let you set an
Authorization header:
x-api-key: sb_your_api_key_hereWhen a credential is missing or invalid
The endpoint answers 401 and points at its OAuth metadata, which is how MCP
clients discover the sign-in flow:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.superbooks.io/.well-known/oauth-protected-resource"Scopes
The scope catalogue is fine-grained — <resource>.<permission> — plus two
meta-scopes that expand to a bundle:
bank-accounts.read bank-accounts.write
customers.read customers.write
documents.read documents.write
inbox.read inbox.write
invoices.read invoices.write
notifications.read notifications.write
reports.read
search.read
tags.read tags.write
teams.read teams.write
tracker-entries.read tracker-entries.write
tracker-projects.read tracker-projects.write
transactions.read transactions.write
users.read users.write
apis.read → every .read scope
apis.all → every scope, read and writeThe authoritative list is served live at
/.well-known/oauth-authorization-server
under scopes_supported.
That catalogue is the whole API-key and OAuth vocabulary, which is not the same set as the MCP tool domains. Two mismatches are worth knowing before you go looking for a scope that does not exist:
- There is no
categoriesscope, even thoughcategoriesis an MCP domain with four tools. Access to it comes from the tier your credential unlocks, described below — not from a scope named after it. usersandnotificationsare real scopes but have no MCP tools at all.
So do not try to derive the tool surface from the scope list, or the scope list from the tool surface. They are related but neither is a subset of the other.
How scopes gate the tool surface
This is the part worth reading carefully, because the two vocabularies do not
line up one-to-one. The scope catalogue is per-resource, but the MCP tool filter
is coarse: it sorts every tool into one of three tiers — read, write, or
destructive — and decides which tiers your credential unlocks.
| Your credential holds | Tools you see |
|---|---|
Any .read scope, or apis.read | The read tier |
Any .write scope, or apis.all | The read and write tiers |
apis.all, and the team has enabled destructive AI tools | All three tiers |
The consequence is important enough to state bluntly: fine-grained scopes do
not survive this mapping. Because the filter is tier-based, a restricted key
holding a single write scope such as tags.write unlocks the entire write
tier — including unrelated tools like invoices_send. It does not confine the
key to tag tools.
Scope your keys accordingly. Treat any write scope as "write across the whole surface", and prefer Read Only for anything that does not need to make changes. The one boundary that is genuinely tight is the destructive gate below, which no fine-grained scope can open.
Destructive tools need two gates
Eight of the 45 tools are marked destructive: the seven *_delete tools, plus
invoices_void. Voiding is the odd one out — it is a soft cancel that moves the
invoice to canceled and preserves the audit trail rather than removing
anything — so read "destructive" as takes something away from the team rather
than strictly deletes a row. They stay hidden from tools/list, and refuse to
run, unless both of these are true:
- The credential holds
apis.all(a read-only or restricted key never qualifies, no matter which scopes it lists), and - The team has turned on destructive AI tools, in the app under Settings → AI.
Two independent gates is deliberate: a leaked key on its own cannot reach a destructive tool on a team that has not opted in. Each destructive tool is labelled in the API reference.
OAuth for integrators
If other teams sign in to your product, use OAuth. The server implements authorization code with PKCE, refresh tokens, dynamic client registration, and the discovery documents MCP clients look for — which together are what let a client like Claude offer "Sign in with SuperBooks" without you preregistering anything.
Discovery
Two documents describe the server. Both are public and unauthenticated:
| Document | URL |
|---|---|
| Authorization server metadata (RFC 8414) | https://api.superbooks.io/.well-known/oauth-authorization-server |
| Protected resource metadata (RFC 9728) | https://api.superbooks.io/.well-known/oauth-protected-resource |
The authorization server document advertises:
{
"issuer": "https://api.superbooks.io",
"authorization_endpoint": "https://api.superbooks.io/oauth/authorize",
"token_endpoint": "https://api.superbooks.io/api/oauth/token",
"registration_endpoint": "https://api.superbooks.io/api/oauth/register",
"revocation_endpoint": "https://api.superbooks.io/api/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "none"]
}PKCE with S256 is the only challenge method — plain is not accepted.
Dynamic client registration
You can register a client at runtime, with no manual onboarding (RFC 7591):
curl -sS https://api.superbooks.io/api/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Integration",
"redirect_uris": ["https://example.com/callback"]
}'{
"client_id": "sb_client_xxxxxxxxxxxxxxxxxxxxxxxx",
"client_name": "My Integration",
"redirect_uris": ["https://example.com/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}Registration returns 201. What it validates:
client_nameis required, 1–100 characters.redirect_urisis required, 1–10 entries.- Each redirect URI must be https, or a loopback address for local
development (
localhost,127.0.0.1,[::1]). Wildcards are rejected. client_uriandlogo_uriare optional, up to 500 characters.
Clients registered this way are public clients: token_endpoint_auth_method
is none, so there is no client secret and PKCE carries the security of the
exchange. Registration grants nothing on its own — no team's data is reachable
until a user completes the consent screen.
Failures come back in the RFC 7591 shape:
{
"error": "invalid_redirect_uri",
"error_description": "Invalid redirect URI: http://example.com/callback"
}Registration is rate limited to 5 requests per hour per IP address. See Rate limits.
The authorization flow
- Generate a PKCE verifier and its
S256challenge. - Send the user to
https://api.superbooks.io/oauth/authorizewithresponse_type=code, yourclient_id,redirect_uri, thescopelist you need,state,code_challenge, andcode_challenge_method=S256. - The user signs in and approves the scopes on the SuperBooks consent screen.
- You receive
codeon your redirect URI. Verifystatematches. - Exchange the code at
https://api.superbooks.io/api/oauth/tokenwithgrant_type=authorization_code, thecode, yourredirect_uri,client_id, and thecode_verifier. - Use the returned access token as a bearer token, exactly like an API key.
- Refresh with
grant_type=refresh_tokenwhen it expires; revoke athttps://api.superbooks.io/api/oauth/revoke.
Ask only for the scopes you actually use — the consent screen shows the user precisely what you requested, and a narrow list is approved more readily than a broad one.