API tokens and scopes
- Version
- 4.0 preview
- Updated
Audience: developers integrating with CBX, and the admins who hand them tokens · Scope: the one credential every API caller presents, and the vocabulary that says what it may do · Since: 3.8.17 (the
<area>:<level>vocabulary; tokens themselves since 3.7.2)
TL;DR — A caller of the REST API (/cb-api/v1/…) or of MCP over HTTP (/cb-api/v1/mcp)
presents a bearer token. A token carries scopes, each <area>:<level> — catalog:write,
orders:read. Seven areas say what the token may touch; two levels say how. Write covers read
within an area; nothing carries across areas; every token can read store configuration; and
permission to run PHP (code:write) is never part of any bundle. Tokens are minted in the admin
(ConfigBox → API Tokens) or on the CLI (configbox:token:mint), shown once, and revoked
permanently. configbox:token:scopes prints this whole vocabulary from the running code.
1. What a token is
cbx_<id>_<secret>
| | |
| | +-- 43 characters of CSPRNG output. Never stored - only its SHA-256 is.
| +-------- the row id, so verifying is one indexed read and not a table scan
+------------- a fixed prefix, so a leaked token is RECOGNISABLE as one by a secret scanner
A token is presented as a standard bearer credential (RFC 6750):
Authorization: Bearer cbx_12_XNySf3kCGeGmN4TUkU6gzkDEzgEUWzmQwtm9fww
- Shown once. Only the hash is kept, so nothing can display it again — not the admin screen, not the CLI, not a support engineer with database access. Lost it? Revoke and mint another.
- Expires optionally, at the start of a UTC day. Prefer an expiry for anything run by an outside party: a token that ends by itself does not rely on anyone remembering to revoke it.
- Revoked permanently. The row stays, so the record of what existed survives; the caller is shut out on its next request.
- Last used is recorded (best-effort) so the "nobody uses this any more" conversation has data.
A token is for software. Staff log in to the admin with their own account; a token never opens an admin screen.
2. Where tokens are minted
In the admin — ConfigBox → API Tokens → Add. Name it after its holder, tick the scopes in
the grid (or start from a preset button), optionally set an expiry, save. The screen shows the new
token once in a warning panel; copy it there. The admin-guide article
docs/admin-guide/settings/manage-api-tokens.md walks a store admin through it in plain language,
and the same screen's help drawer shows it in place.
On the CLI — from the site root, under the site's own PHP:
php cli/joomla.php configbox:token:mint "catalog assistant" --preset=author
php cli/joomla.php configbox:token:mint "fulfilment bot" --scope=orders:write --scope=customers:read
php cli/joomla.php configbox:token:mint "pricing dev" --preset=author --scope=code:write --expires="+90 days"
php cli/joomla.php configbox:token:scopes # the vocabulary, from the code
php cli/joomla.php configbox:token:list # what exists, with last use
php cli/joomla.php configbox:token:revoke 12
The token is the last line of mint's output, unstyled, so … | tail -1 captures exactly it.
Both routes call the same ConfigboxApiTokenHelper::mint(); there is no third way to make one.
3. The seven areas
A scope is <area>:<level>. The area is what a token touches; the level is how.
| Area | Holds | Entities | Grantable | If a mistake happens |
|---|---|---|---|---|
catalog | What the shop sells: products, pages, questions, answers, lists and assignments, detail panes, examples, rules, formula and matrix calculations. | product, page, question, answer, product-list, product-list-assignment, calculation, calculation-code, calculation-matrix, product-detail-pane, example, plus any customization model that does not say otherwise | read, write | recoverable — a typo someone fixes; the storefront shows all of it anyway |
store | How the shop charges and delivers: settings, store identity, currencies, tax classes, shippers and shipping methods, payment methods and providers, connectors, notifications, salutations, geography. | settings, store, currency, tax-class, shipper, shipping-method, payment-method, payment-provider, connector, notification, salutation, country, state, county, city, zone | write only — every token reads it (§4, rule 3) | costly — a wrong tax rate is an invoice that already went out |
customers | People: customer records, addresses, reviews, parties and their relationships. | customer, address, review, party, party-relationship | read, write | irreversible — personal data; a leak cannot be un-leaked |
orders | Money that moved: sales orders, quotes, their lines and tax rates, payments. Frozen checkout snapshots — readable, and changed only through operations. | sales-order, order-line, order-line-config, order-tax-rate, quote, quote-line, quote-line-config, quote-tax-rate, payment | read, write | irreversible — who bought what, plus their address |
conversations | What visitors said to the advisor: conversations, turns, events, recommendations, outreaches. | chat-conversation, chat-turn, chat-event, chat-outreach, chat-recommendation, chat-recommendation-evidence, chat-recommendation-note | read, write | irreversible — conversational personal data |
code | PHP the pricing engine evaluates: code calculations, and transfer packages that carry them. | — (a permission, not a record set) | write only — reading a code calculation is catalog:read | dangerous — code execution on the server |
system | The install itself: migrations, charset, system variables, orphaned strings. | — (operations, not records) | read, write | costly — can freeze updates or skip migrations; recoverable from a backup |
Twelve scopes are grantable: catalog:read, catalog:write, store:write, customers:read,
customers:write, orders:read, orders:write, conversations:read, conversations:write,
code:write, system:read, system:write.
Why areas and not verbs. The first vocabulary was read / write / admin. It split by what a
token does, and the boundaries that matter run by what it touches: a catalog browser must not
dump customer addresses, an order-fulfilment token must not run migrations, and authoring a product
must not include writing PHP the engine evaluates. Every one of those is a subject boundary a verb
vocabulary cannot express. The pattern is Shopify's (read_products / write_products) and Stripe's
restricted keys (a matrix of resources × none/read/write) — the closest prior art for a shop.
4. The four rules
These are the entire model. They live in one function, ConfigboxApiTokenHelper::grantIncludes(),
which the REST controller and the MCP server both consult — so HTTP tokens and stdio runs cannot
disagree about what a grant means.
- Within an area, write implies read.
catalog:writecan describe, list and get the catalog. A token is stored as the highest level per area: mintingcatalog:read,catalog:writestorescatalog:write. - Across areas, nothing implies anything. Seven independent switches.
read,admin-style ladders are gone:orders:writesays nothing about the catalog, andcustomers:writenothing about orders. store:readis the floor. Every token has it, ungranted and unrevokable — it is not a scopemintaccepts. Currencies, tax classes, shipping methods and countries are the vocabulary every other area is expressed in; a token that could not read them could not make sense of a price, a product or an address. The floor is safe only because credential-bearing fields areapiSensitiveand never returned by any read — a payment method's provider parameters, a payment provider's settings, the licence key, the AI and MaxMind keys. A model that adds a secret to a store-area entity must mark it, or the floor hands it to every token.code:writeis never enough on its own. Writing a code calculation needscatalog:writeandcode:write; importing a transfer package that carries code calculations needs both too.cbx_check_product_packagereportscarriesCodeand the scopes still needed before anything is written. No preset includescode:write— it is always spelled out by hand.
5. Presets
A preset is a name mint expands to a scope list and then forgets. The token row stores the
explicit scopes, so a preset can be renamed or retired without touching a token, and there is never
a second vocabulary to keep in sync.
| Preset | Scopes | For |
|---|---|---|
author | catalog:write | Building and maintaining the catalog. |
fulfilment | orders:write, customers:read | Working orders, and seeing who they belong to. |
insights | catalog:read, conversations:read | Learning from the advisor: what visitors ask, against what the catalog offers. |
operator | store:write, system:write | Keeping the install healthy: migrations, charset, shipping and tax configuration. |
everything | every area but code, at write | A supervised session. Still cannot write PHP. |
--preset combines with --scope: --preset=author --scope=code:write is the whole ceremony for a
token that may author PHP calculations, and it is exactly enough.
6. What each token actually gets
The same token means the same thing on both surfaces. Over REST, an entity endpoint needs the
entity's area at the request's level (GET → <area>:read, POST/PATCH/DELETE →
<area>:write); over MCP, the generic entity tools are opened by any scope at their level and then
gated per entity, and every entity enum a token is handed lists only what that token may touch.
| Token | Over REST | Over MCP |
|---|---|---|
catalog:write (author) | Full CRUD on the 13 catalog entities; GET on store entities; 403 on customers, orders, conversations | 16 tools; the write enums list the 9 writable catalog entities; refused reading a customer (customers:read) and writing a code body (code:write) |
orders:write + customers:read (fulfilment) | GET on orders, customers, store; 403 on products | 5 tools — describe/list/get, cbx_set_order_status, cbx_clear_cache; the generic writers are withheld because no order entity is writable |
catalog:read + conversations:read (insights) | GET on catalog, conversations, store | The read tools with both areas in their enum; no writers |
store:write + system:write (operator) | CRUD on store entities; 403 on the catalog | cbx_apply_migrations, cbx_convert_charset, …; refused cbx_get_entity product |
everything + code:write | Everything | All 23 tools |
only the floor (e.g. conversations:read) | GET /tax-class 200; GET /payment-method/1 200 without params | Store entities readable; no credentials in any of them |
The discovery index is filtered too: GET /cb-api/v1/ lists only the entities the presenting
token may read. Any valid token may call discovery, openapi.json and the schemas — they describe
structure, not data.
7. Refusals
| Situation | REST | MCP |
|---|---|---|
| No token / bad token | 401 with WWW-Authenticate: Bearer, one message for missing, malformed, revoked and expired | 401 at the HTTP layer, before any JSON-RPC conversation |
| Token lacks the scope | 403 INSUFFICIENT_SCOPE naming the exact scope: This token does not carry the "orders:read" scope. | A tool error the model can act on: Reading "customer" needs the "customers:read" scope, and this connection carries only: catalog:write. |
| A tool the token is not offered, called by name | — | Refused just the same — listing is a hint, the gate is next to the work |
Code body without code:write | — | Writing a code calculation is running PHP on this server, and needs the "code:write" scope on top of catalog:write. … |
403 rather than 401 when the token is valid but too narrow, because retrying with the same credentials can never help — that distinction is what a client's retry logic reads.
8. The audit trail
Every MCP tools/call writes one line to configbox_authorization.log (the authorization KLog
category, alongside every other who-was-allowed-what decision): the tool name, the caller (a token's
id and name over HTTP, stdio on the CLI), and the granted scopes — never the arguments, which
can carry a customer's address. Last used on the token row is the coarse version of the same
record.
9. The legacy names, and migration 3.8.17
read, write and admin are accepted by mint for one release, expanded with a printed
notice, and never stored:
| Legacy | Expands to | Why this mapping |
|---|---|---|
read | catalog:read, customers:read, orders:read, conversations:read, system:read | It was "everything readable". Narrow it afterwards. |
write | catalog:write, code:write | A write token could author PHP calculations; keeping that is what makes the migration lossless. Revoke code:write from tokens that do not need it — most. |
admin | store:write, customers:write, orders:write, system:write | The grab-bag, unpacked into its four jobs. |
Migration 3.8.17 widens the scopes column to VARCHAR(512) and rewrites every stored token
through the same normalisation, idempotently. It logs a warning naming each token that came out
holding code:write, because that is the one line an admin should act on. A write token that had
no read widens slightly — catalog:write now reads the catalog, by rule 1.
10. For customization authors
A model that opts into the API with getEntityName() belongs to an area. The central map
(ConfigboxApiTokenHelper::getEntityAreas()) covers every stock entity and falls back to catalog
for one it does not know — right for the customization models that exist today (product content
such as summary-panel images), and wrong for a custom model holding customer data, which would then
be readable by any catalog token. Override getApiArea() on the model and return one of
ConfigboxApiTokenHelper::AREA_*. See customization/com_configbox_overriding_controllers_and_models.md.
11. Where the pieces live
| What | Where |
|---|---|
The vocabulary — areas, scopes, floor, presets, entity map, grantIncludes() | helpers/apitokens.php (ConfigboxApiTokenHelper) |
| Mint / verify / revoke, the token format | same file |
| REST enforcement per entity | controllers/apiv1.php (authenticateForEntity()) |
| MCP enforcement per tool and per entity, tool annotations | helpers/mcp.php (getToolScopes(), getToolAnnotations(), getModelForEntity()) |
| The admin screen | models/apitokens.php, property type apitokenscopes (the grid) |
| The CLI | configbox:token:mint, token:list, token:revoke, token:scopes — com_configbox_cli_commands.md |
| The migration | helpers/updates/3.8.17.php |
| The pinning tests | tests/specs/api/mcp-http-transport.spec.ts, entity-api.spec.ts, cli/cli-commands.spec.ts |
Related reading: com_configbox_api_surfaces.md (the four ways in), com_configbox_entity_api.md
(using REST), com_configbox_mcp_server.md (using MCP), admin-guide/settings/manage-api-tokens.md
(the store admin's view of the same screen).