Skip to main content
Version: 3.x

API tokens and scopes

Version
3.x
Updated
View markdown

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 adminConfigBox → 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.

AreaHoldsEntitiesGrantableIf a mistake happens
catalogWhat 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 otherwiseread, writerecoverable — a typo someone fixes; the storefront shows all of it anyway
storeHow 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, zonewrite only — every token reads it (§4, rule 3)costly — a wrong tax rate is an invoice that already went out
customersPeople: customer records, addresses, reviews, parties and their relationships.customer, address, review, party, party-relationshipread, writeirreversible — personal data; a leak cannot be un-leaked
ordersMoney 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, paymentread, writeirreversible — who bought what, plus their address
conversationsWhat 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-noteread, writeirreversible — conversational personal data
codePHP 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:readdangerous — code execution on the server
systemThe install itself: migrations, charset, system variables, orphaned strings.— (operations, not records)read, writecostly — 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.

  1. Within an area, write implies read. catalog:write can describe, list and get the catalog. A token is stored as the highest level per area: minting catalog:read,catalog:write stores catalog:write.
  2. Across areas, nothing implies anything. Seven independent switches. read,admin-style ladders are gone: orders:write says nothing about the catalog, and customers:write nothing about orders.
  3. store:read is the floor. Every token has it, ungranted and unrevokable — it is not a scope mint accepts. 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 are apiSensitive and 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.
  4. code:write is never enough on its own. Writing a code calculation needs catalog:write and code:write; importing a transfer package that carries code calculations needs both too. cbx_check_product_package reports carriesCode and the scopes still needed before anything is written. No preset includes code: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.

PresetScopesFor
authorcatalog:writeBuilding and maintaining the catalog.
fulfilmentorders:write, customers:readWorking orders, and seeing who they belong to.
insightscatalog:read, conversations:readLearning from the advisor: what visitors ask, against what the catalog offers.
operatorstore:write, system:writeKeeping the install healthy: migrations, charset, shipping and tax configuration.
everythingevery area but code, at writeA 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.

TokenOver RESTOver MCP
catalog:write (author)Full CRUD on the 13 catalog entities; GET on store entities; 403 on customers, orders, conversations16 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 products5 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, storeThe read tools with both areas in their enum; no writers
store:write + system:write (operator)CRUD on store entities; 403 on the catalogcbx_apply_migrations, cbx_convert_charset, …; refused cbx_get_entity product
everything + code:writeEverythingAll 23 tools
only the floor (e.g. conversations:read)GET /tax-class 200; GET /payment-method/1 200 without paramsStore 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

SituationRESTMCP
No token / bad token401 with WWW-Authenticate: Bearer, one message for missing, malformed, revoked and expired401 at the HTTP layer, before any JSON-RPC conversation
Token lacks the scope403 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 nameRefused just the same — listing is a hint, the gate is next to the work
Code body without code:writeWriting 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:

LegacyExpands toWhy this mapping
readcatalog:read, customers:read, orders:read, conversations:read, system:readIt was "everything readable". Narrow it afterwards.
writecatalog:write, code:writeA 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.
adminstore:write, customers:write, orders:write, system:writeThe 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

WhatWhere
The vocabulary — areas, scopes, floor, presets, entity map, grantIncludes()helpers/apitokens.php (ConfigboxApiTokenHelper)
Mint / verify / revoke, the token formatsame file
REST enforcement per entitycontrollers/apiv1.php (authenticateForEntity())
MCP enforcement per tool and per entity, tool annotationshelpers/mcp.php (getToolScopes(), getToolAnnotations(), getModelForEntity())
The admin screenmodels/apitokens.php, property type apitokenscopes (the grid)
The CLIconfigbox:token:mint, token:list, token:revoke, token:scopescom_configbox_cli_commands.md
The migrationhelpers/updates/3.8.17.php
The pinning teststests/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).