Skip to content

Interactive API Docs (Swagger + Stoplight Elements)

The AstraPBX API ships a browser-based, try-it-live reference generated from a single OpenAPI spec. This page documents how the docs are served, how the spec stays in sync with the code, and the auth model that the docs default to.

What is served, and where

The API server (api/src/server.js) serves the same OpenAPI document three ways:

Path What Engine
/reference Primary reference — clean three-panel layout Stoplight Elements (@stoplight/elements@9.0.21, pinned via jsDelivr)
/api-docs (also /api, /docs) Classic try-it Swagger UI (swagger-ui-express)
/api-spec.json Raw OpenAPI JSON served from the in-memory swaggerDocument

On prod this is devpbx.astradial.com/reference; on staging stagepbx.astradial.com/reference. Both hostnames sit behind Cloudflare Access.

/reference used to be Scalar; it was switched to Stoplight Elements because Scalar's request console always showed Cookies/Headers/Query-Parameters sections that confused integrators and could not be hidden via config.

The spec

The source of truth is the hand-written api/docs/API_SPECIFICATION.yaml (OpenAPI 3.0.3). At boot, server.js overrides swaggerDocument.servers (see Try-It and Cloudflare Access).

Current coverage: 124 path keys / 182 operations / 22 tags. Every mounted router group is documented — Authentication, Organization Management, SIP Trunks, DID Numbers, Users, Queues, Greetings, Music on Hold, IVR, Outbound Routes, Call Management, Webhooks, Configuration Management, CRM (/crm/*), API Keys (/api-keys), Ticket Alerts (/orgs/{orgId}/ticket-alerts), WhatsApp Admin (/admin/whatsapp/*), DID Pool (/did-pool/*), and Customer Tunnels (/customer-tunnels/*).

Each endpoint description is one plain sentence (minimalist style); request bodies carry a ready-to-edit example object so "Try It" pre-fills concrete values.

Keeping the spec in sync — check:routes CI

api/scripts/check-routes.js runs on every PR that touches api/src/server.js or the spec (workflow Check API routes). It makes two assertions:

  1. Snapshot — the set of app.<method>("/path") routes in server.js must match scripts/routes.snapshot.txt verbatim. Catches server.js silently losing endpoints during a merge.
  2. OpenAPI — every implemented route must appear in the spec under paths:, and vice-versa, beyond a recorded baseline (scripts/routes.openapi-baseline.json). Only new drift fails.

Important: the OpenAPI check scans both server.js and the mounted routers (src/routes/*.js), resolving each app.use('<mount>', …, <router>) to the router file and prefixing its router.<method>('/sub') paths. Without this, documenting a router-group endpoint (CRM, etc.) would register as "doc-only drift" and fail CI. The snapshot check stays server.js-only.

After an intentional route change: npm run check:routes -- --update regenerates the snapshot + baseline; commit them.

Authentication (what the docs default to)

There are two auth systems — see Authentication & API Keys for the full model. In short:

  • ak_ API key → X-API-Key header (ApiKeyAuth). Scoped keys created in the editor's API & Webhooks page. This is the simple path — no login, no secret needed. The docs' Authorize dialog now defaults to ApiKeyAuth on every org endpoint (the spec lists ApiKeyAuth before BearerAuth in each operation's security). Admin endpoints keep BearerAuth.
  • org_ api_key + api_secret → JWT via POST /auth/login (BearerAuth). The org secret is bcrypt-hashed and can never be read back — only rotated (see below).

Known gap: ak_ permission scoping is not enforced

An ak_ key created with limited permissions (e.g. crm.read only) currently gets full org accessrequirePermission() in api/src/middleware/rbac.js checks req.userRole (set only for user JWTs), not req.orgApiKeyPermissions. So today any ak_ key = full org access regardless of the permissions selected. Pre-existing; flagged for a follow-up.

Self-service API-secret rotation

Because the org secret is hashed, the only way to obtain a usable one is to rotate it:

Endpoint Auth Use
POST /api/v1/organizations/{id}/regenerate-secret org (X-API-Key/ak_/JWT), own org only Self-service — returns {api_key, api_secret} once
POST /api/v1/admin/organizations/{id}/regenerate-secret admin JWT Recovery for an org locked out (lost secret and ak_ key)
GET /api/v1/admin/organizations/{id}/credentials admin JWT Returns api_key only (api_secret: null). No longer mutates

The GET …/credentials previously regenerated the secret on every GET, so a browser prefetch or a Scalar "Try It" silently rotated the live secret and broke integrations. It is now non-mutating; rotation is POST-only.

Try-It and Cloudflare Access

The docs hostnames are behind Cloudflare Access, and the API returns Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — an invalid combination that browsers block for cross-origin credentialed requests. So:

  • The spec's first (default) server is a relative /api/v1, so "Try It" always calls the same origin that served the docs → no CORS check, and the Cloudflare Access session cookie rides along. (This is why Swagger's try-it worked while an absolute-server Elements try-it failed with a Network Error.) The absolute custom-domain URL is kept as a second server for external tools (Postman).
  • Stoplight Elements is configured with tryItCredentialsPolicy="include" so the session cookie is sent.

server.js builds swaggerDocument.servers = [{ url: '/api/v1', … }, { url: 'https://<SWAGGER_DOMAIN>/api/v1', … }]. The old http://<ip>:3000 and localhost servers were removed — they can't work from an HTTPS docs page.

How to use it (operator quickstart)

  1. Editor → API & Webhooks → create an API key → copy the ak_….
  2. /referenceAuthorizeAPI Key → paste into X-API-Key.
  3. Open any endpoint → Send API Request. No password/login/token needed.
  4. For a JWT: rotate the org secret (Organization Management → Rotate secret), then POST /auth/login with api_key + api_secret.

Source references

  • Docs serving + servers override: api/src/server.js (/reference, /api-docs, /api-spec.json, the swaggerDocument.servers block)
  • Spec: api/docs/API_SPECIFICATION.yaml
  • Route-consistency check: api/scripts/check-routes.js, scripts/routes.snapshot.txt, scripts/routes.openapi-baseline.json
  • Secret rotation + credentials: api/src/server.js (org + admin regenerate-secret handlers)