The BOM REST API — the API the site *exposes*
Audience: developers & AI agents, and anyone integrating against Beta-Calco · Scope: the outbound "CSI REST API" — auth, endpoints, response shape · Last reviewed: 2026-07-25
TL;DR — The site publishes a small REST API ("Beta Calco CSI REST API", v0.9) so other systems can ask for a flat BOM without a login: authenticate with client credentials that are ordinary Joomla site user credentials, get a bearer token good for 24 hours, then request materials by Infor item name or by configuration code. It serves its own OpenAPI 3.0.3 document. Note that the token lives in APCu, so it does not survive a PHP restart and is not shared across web servers.
Everything here is served by one controller: CUST/controllers/csimaterials.php.
What it is & why it exists
The BOM logic — cache, CPQ, explosion — is the site's, but the consumers are not all the site's. Costing tools, spreadsheets and internal automation need the same answer the quote line gets. Rather than duplicate the logic, the site exposes it.
The API is deliberately small: two useful operations, one token endpoint, and a self-describing schema.
How it fits together
| Task | Purpose |
|---|---|
getToken | exchange client credentials for a bearer token |
getMaterialsForItems | flat BOM for up to 200 Infor item names + quantities — fresh, straight from Infor |
getMaterialsFromConfigurationCodes | flat BOM for configuration codes, resolved through CPQ |
getDocumentation | the OpenAPI 3.0.3 document (documents only getToken and getMaterialsForItems) |
Base path is whatever KLink::getRoute('index.php?option=com_configbox&view=customview&viewname=csimaterials')
resolves to — i.e. it depends on the menu item alias configured for that view (see
SEF links). The code's own examples use /csi-rest-api/.
Integrations & contracts
Authentication
Client-credentials, where the "client" is a Joomla site user:
# Credentials in the body (or as Basic Auth — PHP_AUTH_USER / PHP_AUTH_PW are accepted)
curl -s -X POST 'https://betacalco.com/csi-rest-api/getToken' \
-d 'client_id=<username>' -d 'client_secret=<password>'
# → {"token_type":"Bearer","access_token":"<64 hex chars>","expires_in":86400}
The token is 32 random bytes hex-encoded, stored in APCu under csi.oauth.tokens.<token> with a 24-hour
TTL. Failures return 401 with {"error":"invalid_client"|"invalid_grant", "error_description":"…"}.
Then send it as a bearer token:
curl -s 'https://betacalco.com/csi-rest-api/getMaterialsForItems/json/PH-130000-40/10/PH-130010-CP-F1/2' \
-H 'Authorization: Bearer <access_token>'
isAuthorized() accepts either an Authorization header (any prefix is stripped — Bearer <token> and a
bare token both work), or an access_token query parameter, or simply being logged into the site in
that browser session. Any logged-in Joomla user passes; there is no permission check beyond authentication.
getMaterialsForItems
| Parameter | Required | Notes |
|---|---|---|
output_type | yes (documented); defaults to json in code | json or xlsx |
item_1 … item_200 | at least one | Infor item name |
qty_1 … qty_200 | no | defaults to 1; must be numeric and non-zero |
Item/qty pairs are read until the first gap, so numbering must be contiguous from 1. Passing more than 200
(or supplying item_201) returns 422. An empty item name or a zero/non-numeric quantity also returns 422
with validationIssues.
json returns the envelope below; xlsx streams a spreadsheet as BOM.xlsx with the columns
Root Item · Material Item name · PMT Code · UOM · Qty · Path · Signature.
Every call is logged with the requesting username to the custom_flat_bom_requests log, and its duration to
custom_syteline_performance.
This endpoint is fresh, not cached — it calls getMaterialsForItems() directly, which runs the
five-level explosion against Infor. It is therefore slow (seconds to minutes)
and load-bearing on the Infor connection. It does not read the BOM cache.
getMaterialsFromConfigurationCodes
Takes item_1 … item_N as configuration codes (spaces stripped), splits each on /, and resolves it
through getCpqMaterialsFromCode() — product lookup via the linear price list, then CPQ. The
response maps each configuration code to its material list, or null where resolution failed.
This task's authorization check is wrong. It calls
parent::isAuthorized()—KenedoController's base implementation — instead of$this->isAuthorized(). The base returnstrueunconditionally for any controller whose name does not begin withadmin, so the token check is bypassed and the endpoint is reachable unauthenticated. Its siblinggetMaterialsForItemscalls$this->isAuthorized()and is correctly protected. Established by reading the two call sites againstKenedoController::isAuthorized(); recorded indocs/_known-issues.mdwith the rest of the auth cluster.
Response envelope
All JSON responses share the ConfigboxJsonResponse envelope:
{
"success": true,
"feedback": "",
"errors": [],
"validationIssues": [],
"timeMs": 1234.5,
"data": [
{
"rootItem": "PH-130000-40",
"item": "SOME-PART",
"pmtCode": "P",
"um": "EA",
"qty": "24.00000000",
"path": "[\"PH-130000-40\",\"SUB\",\"SOME-PART\"]",
"signature": "d41d8cd98f00b204e9800998ecf8427e"
}
]
}
Status codes in use: 200, 401 (missing/invalid token), 422 (validation), 400 (any thrown exception —
the message is returned verbatim).
SEF path form
getSegmentMatching() maps positional URL segments to parameters:
| Task | Segment structure |
|---|---|
getToken | /getToken/<client_id>/<client_secret> |
getMaterialsForItems | /getMaterialsForItems/<output_type>/<item_1>/<qty_1>/… |
Do not use the path form of
getTokenin production. It puts a username and password in the URL, where they land in web-server access logs, proxy logs and browser history. Use the body/Basic-Auth form.
Admin settings
None. The API has no settings of its own — no enable flag, no allow-list, no rate limit, and no separate credential store. Access is governed entirely by Joomla user accounts, and the data it returns depends on the Infor DB and CPQ settings documented in the Infor hub and cpq.md.
Data model
The API owns no tables. Tokens live only in APCu; materials are read live from Infor (or, for the configuration-code endpoint, produced by CPQ). Nothing is persisted apart from log entries.
Deployment runbook (manual steps)
-
Nothing to migrate — the controller ships with the code and is always reachable.
-
Create (or identify) the Joomla user account the consumer will authenticate as. There is no API-key concept: the account's password is the client secret, so use a dedicated account rather than a person's.
-
Ensure the Infor DB settings are configured — every response depends on them — and CPQ settings if the consumer will use configuration codes.
-
Confirm APCu is enabled for the web SAPI. Without it token storage silently fails and every authenticated call returns
401. -
If a clean URL is wanted, add the menu item for the
csimaterialsview — see SEF links. Otherwise theindex.php?option=…&task=…form works. -
Smoke test: request a token, then request one known item and confirm rows come back:
TOKEN=$(curl -s -X POST "$BASE/getToken" -d "client_id=$U" -d "client_secret=$P" | jq -r .access_token)curl -s "$BASE/getMaterialsForItems?output_type=json&item_1=PH-130000-40&qty_1=1" \-H "Authorization: Bearer $TOKEN" | jq '.success, (.data | length)'
Turning it off: there is no switch. Disabling the consumer's Joomla account revokes access at the next token request (existing tokens stay valid for up to 24 hours). Blocking the route at the web server is the only immediate stop.
Testing
No automated coverage. The OpenAPI document (getDocumentation) can be pasted into any Swagger UI to explore
the two documented operations interactively.
Gotchas & caveats
- Tokens are APCu-only. A PHP-FPM restart, an APCu flush, or a second web server invalidates them —
consumers must handle a sudden
401by re-authenticating, not by treating 24 hours as guaranteed. - Any logged-in user is authorized. Authentication is the only gate; there is no role or permission check,
so any site account can read BOM data through the API even though the on-site BOM screens require
com_configbox.core.manage. - A browser session authenticates too.
isLoggedIn()short-circuits the token check, so these endpoints are reachable from a logged-in browser with no token at all — convenient for debugging, easy to forget when reasoning about exposure. getMaterialsFromConfigurationCodesis not token-protected (above).- No rate limiting. Each call can trigger a multi-minute Infor query; a loop over many items is an effective self-inflicted denial of service on the ERP connection.
- The 200-item cap is
maxItemRequests, shared with the admin tools — change it and both move. exit()in afinallyends the request insidegetMaterialsForItems, so nothing after the controller runs. Don't expect the usual response pipeline.
Possible follow-ups
- Fix
getMaterialsFromConfigurationCodesto call$this->isAuthorized(). - Gate the API on a dedicated permission rather than "any logged-in user".
- Move tokens out of APCu (a table with a TTL) so they survive restarts and work multi-server.
- Rate-limit per token, given each call can hit Infor for minutes.
Related docs
- Hub: BOM & CPQ · spokes: position-boms.md · bom-cache.md · cpq.md
- SEF links — how the clean path is produced
- Infor · known issues:
docs/_known-issues.md