Two operator-visible defects and three doc corrections, closing the tail of the 2026-09-03 run. THE BADGE. The two kiosks read the same physical badges and disagreed. The parts kiosk understood both shapes a scanner emits - the bare SSO and the PayNo wrapper 0<digits>BZ - while the USB kiosk compared the raw string to an SSO, so the wrapper resolved to nobody. An operator whose keyboard-wedge scanner emits it could take a 3D-printed part and could not check out a drive, with the same badge, at two cabinets a few metres apart. The shape now lives in shopdb/utils/badges.py on the contract surface, because a plugin may not import another plugin and because a badge is a property of the site rather than of a cabinet. Both kiosks use it, and both store the DIGITS rather than the string that arrived: one row per spelling makes a custody history that cannot be joined to itself. That closed a subtler hole in the same place. str.isdigit() is true for Arabic-Indic and other decimal forms, and int() parses them, so a badge written in those characters was accepted, matched a REAL employee, and was then stored as an SSO string nothing else matches. ASCII digits only. An empty scan and an unreadable one now get different answers, because they are different problems for the person at the cabinet. THE IMAGES. /api/applications/image/<filename> and /api/models/image/<filename> required a permission, and a browser fetching <img src="..."> sends no Authorization header - so the gate did not protect the file, it guaranteed the picture never appeared. Both are ungated now, like the four sibling image routes (branding logo, map blueprint, employee photo, part picture), and registered in PUBLIC_ENDPOINTS with that reasoning. /api/printers/lookup?fqdn= never matched anything: both arguments were compared against Communication.ipaddress, so a hostname lookup 404d on printers whose hostname was on the record. An FQDN now matches the hostname, with a fallback to the short name. The slide manager gained the per-slide hold time. The PATCH route and the feed have always carried `seconds`; nothing called it, so the one documented per-slide control was curl-only. Docs: API-REFERENCE listed the employee lookups and the whole notifications surface as unauthenticated when they answer 401, and called both kiosks "deliberately open" when they are off by default and refuse anything without the site's device token. The installer download says applications.download, which is what the code requires. api-inventory.json said printers/lookup was jwt-optional. Fourteen tests. The badge rule verified by planting the old per-kiosk behaviour: three fail, one in each kiosk. Scope: product
12 KiB
API reference (index)
This page is an index and a pointer, not a full specification. It answers three questions: what endpoints exist, who calls them, and what auth they require. The detailed request and response shapes live in the live generated docs and in the per-surface contract docs linked from each table below.
Live and generated docs
The repo ships hosted, generated API docs. Start there:
- Interactive spec:
GET /api/docs- a self-hosted Redoc page over the generated OpenAPI spec. The Redoc bundle is vendored undershopdb/core/api/staticdocs/, so it renders fully offline on the air-gapped prod box (no CDN). - Raw spec:
GET /api/docs/openapi.json- OpenAPI 3.1, roughly 238 paths and 362 operations. Generated byscripts/gen_openapi.pyfromdocs/api-inventory.json; regenerate after any API change. - LLM / agent entry point:
GET /api/docs/llms.txt- a concise API guide following the llms.txt convention, plus a read-only MCP server (mcp/shopdb_mcp.py, built withFastMCP.from_openapiover the same spec). The MCP server exposes a curated set of GET endpoints as tools for an agent to query the asset database over HTTPS with a scoped read token; it never runs on the prod box. Set it up on a work PC with<imaging-share>/github/setup-mcp.cmd.
The docs blueprint is shopdb/core/api/docs.py (a core blueprint, always
mounted regardless of which plugins are staged into a site build).
Contract docs (linked per table below) hold the deep semantics: field mappings, idempotency rules, rotation, error envelopes, staged rollout. This page only routes you to the right one.
1. Fleet and client contracts (unauthenticated or token)
These are the endpoints the shopfloor PC fleet, kiosks, displays, and printer installers call. They are consumed by machines, not by the interactive UI, and they authenticate with a scoped service token or nothing at all.
| Endpoint | Auth | Purpose | Contract doc |
|---|---|---|---|
GET /api/geenforce/manifest?pctype=<scope> |
geenforce.fetch service token (X-API-Key or Bearer PAT) |
Serve the current published manifest for a PC-type scope. ETag / 304 supported. | GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md |
GET /api/geenforce/payload/<sha256> |
geenforce.fetch service token |
Serve a payload blob (installer) by content hash so share-less PCs pull over HTTPS instead of SMB. Rate limited and size capped; the sha256 is the integrity guarantee. | GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md |
POST /api/geenforce/report |
geenforce.report service token |
Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes. | GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md |
POST /api/collector/<plugin> |
X-API-Key env key or a collector.ingest managed token |
Generic idempotent inventory upsert; the PC fleet targets /api/collector/computers. |
COLLECTOR-INTEGRATION.md |
POST /api/collector/pc, /apps, /heartbeat, /bulk |
X-API-Key or collector.ingest token |
Legacy computers-only collector paths (predate ADR-006); deprecated in favor of /api/collector/computers. |
COLLECTOR-INTEGRATION.md |
GET /api/collector/status |
X-API-Key or collector.ingest token |
Collector liveness and endpoint list. | COLLECTOR-INTEGRATION.md |
GET /api/printers/install-list |
optional JWT (anonymous fleet or logged-in browser) | Flat list of network printers with floor-map positions for the signed installer. ?format=text returns a pipe-delimited variant. |
PRINTER-INSTALLER.md |
GET /api/printers/pc-default?machine=NNNN |
optional JWT | The PC's default printer by machine (asset) number, via the defaultprinter relationship. ?format=text supported. |
PRINTER-INSTALLER.md |
GET /api/printers/install-batch?printerids=1,2,3 |
optional JWT | Generate a self-deleting Windows .bat that installs the selected printers. |
PRINTER-INSTALLER.md |
GET /api/dashboarddefaults/display-role?fqdn=<fqdn> |
public (none) | Resolve what a display PC should show (role dashboard/lobby/partskiosk, frontend path, business unit). FQDN-first, IP fallback. |
GE-ENFORCE-DISPLAY.md |
GET /api/dashboarddefaults/visitor-location?fqdn=<fqdn> |
public (none) | Resolve the business unit for a lobby display by FQDN (IP fallback). | GE-ENFORCE-DISPLAY.md |
The geenforce.fetch and geenforce.report scopes accept both X-API-Key and
Authorization: Bearer transports, the same managed-token pattern the collector
uses (see COLLECTOR-INTEGRATION.md for how to mint, deploy, and rotate a scoped
token). A fetch token may be further resource-bound to specific scopes; a bound
token is denied (403 on manifest, 404 on payload) anything outside its scopes.
2. Import API
The import surface (an admin PAT plus X-Import-Mode to preserve legacy
timestamps) lets a script load an entire legacy database through the same
endpoints the UI uses. It is documented in full, per resource, in IMPORT-API.md
and is not duplicated here. The dashboarddefaults import fields (FQDN-preferred
keying) are covered there as well.
3. Core UI API
Everything else is the core UI API: the endpoints the Vue frontend calls. As a
rule these are JWT-authenticated (a login token or a managed Personal Access
Token) and versioned by the plugin contract (__contract_version__; the
current value is in PROJECT-MAP.md, which is generated). Behavior and stability guarantees are in CONTRACT-STABILITY.md;
sister sites should pin tight core_version ranges until the contract reaches
1.0.
Two auth patterns dominate the reads:
- Public (no token ever). The endpoints below are reachable with no credential at all. This is the surface a firewall or deployment reviewer asks about, so it is enumerated in full.
- Optional JWT (
jwt_required(optional=True)). Nearly every core and plugin GET (list / detail / report / dashboard-summary) is optional-auth: it serves reads anonymously and only requires a JWT for writes. There are well over a hundred of these; rather than reprint them, enumerate them from the live spec at/api/docs(filter to theGEToperations). All product reports (/api/reports/*and every plugin.../report*) are optional-auth by the same convention.
Every mutating endpoint (POST / PUT / PATCH / DELETE) requires a JWT and is
gated by require_role or require_permission; none are public.
The map write surface splits along that line. Repositioning markers
(/api/mappositions/*) needs assets.edit - the same permission the old
per-asset PUT /api/assets/<id> always needed, so bulk editing grants nobody
anything new. Creating buildings and levels or uploading a blueprint
(POST|PUT|PATCH|DELETE /api/maplevels/*) needs role:admin, because a level's
dimensions are the coordinate space every marker on it is expressed in.
Two GETs deliberately break the optional-auth convention and require a permission, because what they return is not asset metadata but a file that carries value or liability of its own:
| Endpoint | Requires | Why |
|---|---|---|
GET /api/applications/package/<filename> |
applications.download |
Licensed vendor installers. A separate permission from applications.view on purpose: seeing that an application exists is not the same as being handed the vendor's installer, and an anonymous URL is a redistribution channel. The script-body download is gated the same way. |
GET /api/warranty/proof/<filename> |
warranty.view |
Invoices and certificates carry pricing and a service tag. |
Their sibling upload/delete routes are applications.edit / warranty.edit
like any other mutation.
Fully public endpoints (auth = none)
| Endpoint | Purpose |
|---|---|
POST /api/auth/login |
Obtain a JWT. |
GET /api/setup/needs-admin |
First-run check: does the instance have zero users. |
POST /api/setup/create-admin |
First-run only; creates the first admin, then 403s forever. |
GET /api/settings/map-blueprint/<filename> |
Serve a floor-map blueprint image. Pre-0.11.0 single-map path, retained; new uploads are served per level below. |
GET /api/maplevels |
Buildings and their levels, each with blueprint paths, native pixel size and marker count. Public because the printer-installer map draws a floor plan before anyone logs in; it exposes level names and image paths, nothing about assets. |
GET /api/maplevels/<levelid> |
One level's name, building, blueprints and size. |
GET /api/maplevels/<levelid>/blueprint/<filename> |
Serve a level's blueprint image, with sandbox headers so an SVG floor plan cannot execute as script. |
GET /api/settings/branding/<filename> |
Serve site branding assets (logo, etc.). |
GET /api/settings and GET /api/settings/<key> |
Read-only, and only the public allowlist: the branding and map categories, a few named site keys, plus any key a plugin declares public in get_settings_defaults (e.g. printedparts_label_prefix, which the logged-out parts kiosk renders). Every other key answers 404 to an anonymous caller. |
GET /api/models/image/<filename> |
Serve a model image. |
GET /api/applications/image/<filename> |
Serve an application image (tiles render before login). |
GET /api/dashboard/navigation |
Public navigation tree. |
GET /api/dashboard/health |
Liveness / health probe. |
GET /api/plugins/enabled |
List enabled plugins (no claims used). |
GET /api/dashboarddefaults/display-role |
Display role resolution (see section 1). |
GET /api/dashboarddefaults/visitor-location |
Lobby business-unit resolution (see section 1). |
GET /api/employees/search, /lookup/<sso>, /lookup |
NOT public. These need employees.view and answer 401 anonymously. A kiosk resolves a badge through its own gated endpoint, not through these. |
GET /api/employees/photo/<filename> |
Serve an employee photo. |
GET /api/notifications/shopfloor, GET /api/notifications/employee/<sso> |
The two the public display and kiosks actually read. The rest of /api/notifications (the list, /types, /<id>, /active, /calendar, /dashboard/summary) needs notifications.view and answers 401 anonymously. |
GET /api/slides/feed |
Slide feed for the lobby display and screensaver. |
GET /api/slides/img/<surface>/<filename> |
Serve a slide image. |
GET /api/printedparts/image/<filename> |
Serve a printed-part image. |
GET /api/printedparts/kiosk/item/<itemcode> |
Kiosk part lookup. Carries no JWT, but is NOT open: the routes 404 unless printedparts_kiosk_enabled is on AND the request carries the site's X-Kiosk-Token. |
POST /api/printedparts/kiosk/take |
Kiosk part checkout, behind the same flag and device token. |
GET /api/usb/kiosk/device/<device_id> |
USB kiosk device lookup, behind usb_kiosk_enabled plus X-Kiosk-Token. |
POST /api/usb/kiosk/checkout, /checkin |
USB kiosk custody moves, same gate. |
"No authentication" is not "open." Both kiosks are off by default and refuse every request without the site's device token; with the flag off they answer 404 rather than 403, so a kiosk a site has not turned on does not announce itself. The token is a credential for a screen, not authorization for a person: it says which cabinet is asking, and the badge on the request says who is standing at it.
See also
docs/adr/README.md- architecture decision records index.docs/DEPLOY.md- deployment; the public-endpoint inventory in section 3 above is the site-exposure surface a deploy reviewer needs.docs/PLUGINS.md- the plugin catalog.- Contract docs: GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md, COLLECTOR-INTEGRATION.md, PRINTER-INSTALLER.md, GE-ENFORCE-DISPLAY.md, IMPORT-API.md, CONTRACT-STABILITY.md.