Operating system set to macOS
Documentation jurisdiction set to United States
Sign inConnect Claude

Changelog

Versioned releases. Pin a version with Archie-Version: YYYY-MM-DD in your requests to freeze the behaviour your code was built against. Breaking changes are flagged β€” they're rare, and come with migration notes.

Slack: citation cards on Archie's answers β€” for the first time, ever

  • β€’Citation reference cards on Archie's own Slack answers have never rendered β€” and couldn't: the delivery path asked Slack to unfurl the bot's own message after posting, but Slack only accepts an unfurl for a link it saw a human share (apps never receive `link_shared` for their own messages). Yesterday's activation note blamed the missing unfurl-domain registration; with the domain registered the call still failed, which exposed the deeper rule. Cards now travel inside the answer message itself as attachments, built before posting β€” same visual, a mechanism Slack actually permits. Proven live: a real ASC 606 answer carries five citation cards (capped at five per message). (PR #7032)
  • β€’Citation links a person pastes into Slack now also get cards: `/r/` reference links joined topic permalinks in the `link_shared` handler β€” previously the one mechanism Slack does allow for citation cards wasn't wired. Topic links and citation links mix in a single unfurl; only topic links carry Work Object entities. (PR #7032)
  • β€’Repo manifests now place `unfurl_domains` where Slack's manifest API accepts it (`features`, not `event_subscriptions`) β€” they previously worked only as human reading material and could never be applied programmatically. (PR #7028)

Three skill outputs gain the uniform disclosure surface

  • β€’`derive_period`, `enrich_tb_rows`, and `detect_tb_duplicates` outputs now carry the platform-standard `stub_fields` and `warnings` arrays like every other skill output β€” they had shipped without the disclosure mixin, so callers reading those fields uniformly would have hit missing keys. Additive on the wire (both fields default to empty lists); the OpenAPI spec, agents.md, and llms.txt are regenerated to match. (PR #7074)

For contributors: alpha PRs are test-gated again (they hadn't been since May 1)

  • β€’When the PR line moved from develop to alpha on 2026-05-01, no test workflow followed: the regression harness still filtered on develop, so no automated test gated any alpha merge for six weeks. Two gates now run on every alpha PR touching `apps/api`: the contract suite (1,400+ architectural guards, infra-free, ~1 minute) and the resurrected regression harness β€” real Postgres and Redis containers, migrations, seeded data, a booted API (running as a non-superuser role so row-level security stays enforced in CI), and L0/L1 smoke. The harness's green run on its own enabling PR was the first in its recorded history. (PRs #7051, #7077)
  • β€’The rot the gates surfaced is fixed alongside: stale guards repinned to deliberate changes, a test file whose fixtures never existed anywhere now runs (first time since #4090), and live-DB contract audits skip cleanly in CI via the new `ARCHIE_TEST_SENTINEL_KEYS` signal instead of failing on the sentinel DSN. (PRs #7051, #7069)

One identity per API key: REST, SDK, and CLI topics now work end-to-end

  • β€’The same sk_ key resolved to two different identities depending on transport: MCP acted as the key's creator, while REST (and therefore the Python SDK and CLI, which ride it) invented a synthetic per-key user that row-level security didn't recognise. The visible symptoms: `topic_list` always returned `{topics: [], total: 0}` over REST while the same key saw everything over MCP, `topic_get` 500'd, and every topic WRITE over REST failed with a 500 β€” including from the published PyPI 1.3.0 package. One shared resolver now derives the caller identity on every surface: act as the key's creator when the key has one, fall back to the per-key service actor otherwise. (PR #6917)
  • β€’What changes for you: topic reads and writes succeed over `POST /api/v1/skills/topic_*/invoke`, the typed SDK methods, generic `skills.invoke`, and `archie skills run topic-*` β€” verified live in both directions against MCP with the same key. Note the semantics: requests made with a key now run as the person who minted it (their topic visibility, their attribution), which is what MCP has always done.
  • β€’Also in this pass: every scope example in the Python SDK docstrings used a pre-consolidation vocabulary (`tax:read` etc.) that matches no skill since the 11-scope taxonomy β€” copied examples silently filtered the registry to zero skills. Examples now use live scopes, and a denylist test keeps retired vocabulary out of the packaged sources. (PR #6973)

Topics hardening: creator membership parity and the artifact-attach 500

  • β€’Topics created over MCP/REST now record the creator as a topic member, exactly like topics created in the web app β€” previously agent-created topics had no membership row, so anything that reads memberships (participant pips, share lists, the membership clause of the visibility gate) treated them differently. (PR #6955)
  • β€’`POST /api/v1/topics/{id}/artifacts` with a `metadata` object committed the row, bumped the topic's activity, then returned a 500 β€” and the natural retry hit `409 already linked`. Cause: the add path returned the database's raw JSON-as-string metadata into a response model that requires an object; the read path already normalised it. Both paths now return the same parsed shape; the call returns 201 with your metadata echoed back, queryable. (PR #6960)

Typed SDK methods and CLI commands, generated from the skills registry

  • β€’One generator now reads the canonical skills registry and emits the typed surface for all three clients: 65 typed Python methods (sync and async β€” `client.skills.list_clients(...)` with real keyword arguments instead of a payload dict), 65 typed TypeScript methods at `archie.skills.generated` (each with its own input interface, e.g. `topicAsk(input: TopicAskInput)`), and 65 `archie skills run <skill>` CLI commands. A new skill in the registry appears in all three on the next regenerate, and a CI parity check fails the build if any artifact drifts. (PRs #6874, #6878)
  • β€’CLI: every wire field becomes a flag β€” `archie skills run list-clients --json`, `archie skills run derive-period --kind bas_au_monthly --anchor-date 2026-04-30 --json`. Required fields are enforced (`--kind TEXT [required]` in `--help`), nested object/array inputs are passed as JSON strings, and the generated commands share the exact auth and `--json` plumbing of the generic `archie skills invoke`.
  • β€’What your install gets today: the typed surface ships in heyarchie 1.4.0, which is not yet on PyPI β€” `pip install heyarchie` currently serves 1.3.0, where the generic `client.skills.invoke(slug, payload)` is the only entry point. The TypeScript package (heyarchie 2.1.0) is not yet published to npm; until then build it from the repo's `apps/sdk-typescript`. The generic `invoke()` path is unchanged either way β€” typed methods delegate to it.
  • β€’TypeScript fixes in the same pass: `skills.list()` called `/v1/skills`, which 404s against the live API β€” now `/api/v1/skills` β€” and `SkillCatalog` gained the `skills` array the wire actually returns (`data` kept for older shapes). The client config key is `baseURL`.
  • β€’Generator hardening from the review round (#6878): `$ref`/`$defs` resolution so string-enums type as strings rather than objects, zero-parameter skills no longer emit invalid signatures, reserved-name collision guards abort generation rather than shadow hand-written methods, and docstrings escape backslashes and quotes from skill descriptions.

topic_messages: read a topic's thread from any surface

  • β€’A seventh topic skill completes the read direction of surface parity: `topic_messages` returns a topic conversation's messages chronologically, paginated β€” per message the content, role, `query_id` (the thinking-steps key), model, citations, and artifact refs from the persisted envelope. (PR #6898)
  • β€’It pairs with `topic_ask`: when an ask times out the response says the turn is still running server-side β€” `topic_messages` is how you poll for the answer. Turns made on the web are now readable over MCP/REST, and turns made over MCP/REST were already readable on the web, so a topic can be picked up from either side.
  • β€’The reader never writes: an empty topic returns an empty page instead of auto-creating a conversation, and a `conversation_id` from another topic is rejected. Requires the `topics:read` scope. Live on MCP, REST (`POST /api/v1/skills/topic_messages/invoke`), the Agent 365 manifest, and the typed SDK/CLI surface above.

Portal: sign-out actually signs you out, plus a design pass

  • β€’Sign-out on the dashboard was a no-op: it cleared the session cookie under a hard-coded fallback domain while the API sets the cookie under the tier's real cookie domain, and cookie deletion only matches an exact (name, domain, path). The portal now clears every candidate domain, calls the API's `POST /auth/logout` first so the token is revoked server-side, and `GET /api/v1/auth/me` now rejects revoked tokens β€” previously a revoked token still validated on the exact endpoint the portal uses to check sessions. (PR #6879)
  • β€’Design pass across the portal, driven by a 29-page desktop + mobile survey of the live site: the dashboard is now usable on a phone (the fixed sidebar rail hides on small screens, replaced by a sticky swipeable section strip), two pages with invisible navy-on-navy headings (Settings, Organizations) are legible again, and the stale light-theme modals (revoke / rotate key) plus four divergent empty states (Usage, Logs, Connections, Webhooks) moved onto the dark portal token system. Mobile nav and dark-theme consistency throughout. (PR #6892)
2026-06-10alphabreaking

Permission scopes consolidated: 16 β†’ 11, catalog now registry-derived

  • β€’API-key permission scopes are now exactly 11 resource-level slugs: `ask:execute`, `clients:read`, `documents:analyse`, `reports:generate`, `accounting:write`, `topics:read`, `topics:write`, `workstreams:read`, `workstreams:write`, `search:read`, `email:draft`. The old vocabulary mixed mega-buckets with single-skill micro-scopes; the new set folds and renames: `client:read` + `period:derive` β†’ `clients:read`; `journal:create` + `trial_balance:enrich` + `trial_balance:propose_mapping` β†’ `accounting:write`; `workstream:author` + `workstream:execute` + `approve:execute` β†’ `workstreams:write`; `search:knowledge` β†’ `search:read`; singular `topic:*` / `workstream:*` β†’ plural. (PR #6830)
  • β€’Existing keys were migrated in place β€” each key's permissions array was mapped old β†’ new with de-duplication, so no key rotation is needed and nothing changes at call time. (The never-enforced legacy portal strings such as messages:create have no mapping and pass through as inert entries.) Scopes do not yet gate skill invocation (the invoke path self-grants per skill today), so the rename costs running integrations nothing.
  • β€’What can break: creating or updating keys with retired slugs. `POST /api/v1/connect-keys` now rejects unknown slugs with a 400 naming the offenders (`unknown_permissions`) and the valid set, where it previously accepted any list of strings. The retired vocabulary β€” `messages:create`, `conversations:read`, `documents:read`, `drafts:write`, `journal:write`, `reports:read`, `standards:read`, `workstreams:execute`, and the rest β€” is pinned in a contract-test denylist and will not return. Migration: switch key-creation calls to the 11 slugs above.
  • β€’The catalog itself is now derived from the live skills registry instead of a hand-written list (PR #6813): `GET /api/v1/connect-keys/_helpers/permission-catalog` serves one entry per scope with the skills it unlocks, and the dashboard's New-API-key dialog reads it β€” so the offered checkboxes can never again drift from what the registry enforces (the old hard-coded list contained slugs enforced nowhere and missed new permissions entirely).

Topics land on the Connect surface: six topic_* skills

  • β€’Topics β€” previously reachable only through the web app's session-authed REST β€” are now first-class Connect skills: `topic_list`, `topic_create`, `topic_get`, `topic_link_conversation`, `topic_post_message`, `topic_ask`. Each has typed input/output schemas and is live on MCP, REST (`POST /api/v1/skills/{slug}/invoke` with an API key), and the Agent 365 manifest. (PR #6804)
  • β€’`topic_ask` dispatches a real orchestrator turn inside the topic and waits for the answer; if it can't finish in time it returns `status="running"` and the turn keeps executing server-side β€” nothing is lost on a client timeout. `topic_post_message` persists the message and dispatches fire-and-return, reporting `submitted` or an inline `completed` with the answer.
  • β€’`topic_get` optionally resolves the topic's canvas descriptor (None when no domain run has produced one). `topic_link_conversation` is idempotent when re-linking within the same topic and errors loudly on a cross-topic conflict.
  • β€’Scopes: `topics:read` / `topics:write` for the CRUD skills, `ask:execute` for the two dispatch skills (named under the consolidated taxonomy β€” see the scopes entry above).

Outbound webhooks: Archie tells you when he's done

  • β€’Register an HTTPS endpoint and the events you care about, and Archie POSTs a signed payload when a matching event lands, so you can stop polling for the result of a long job. Today that covers `workstream.run.completed` and `workstream.run.failed` (with `playbook.run.*` kept as legacy aliases) plus `api_key.created` and `api_key.deleted`. Manage endpoints from the dashboard Webhooks page; each one gets a signing secret shown exactly once.
  • β€’Every delivery is signed. The `X-Webhook-Signature` header is `v1=HMAC-SHA256` over `{timestamp}.{body}`, alongside `X-Webhook-Timestamp`, `X-Webhook-Id`, and `X-Webhook-Event`. Verify it against the raw body and reject anything signed more than five minutes ago. Delivery is at least once with exponential backoff (1, 2, 4, 8, 16s) and a dead-letter after five attempts, so treat `X-Webhook-Id` as the idempotency key.
  • β€’Endpoints are restricted to public HTTPS hosts, re-checked on every delivery, so an endpoint cannot be pointed at an internal address. The new `/docs/webhooks` guide covers registration, the delivery shape, signature verification (copy-paste Python and Node), retries, and the events catalogue. The live event list is served at `GET /v1/webhooks/event-types`.

Idempotency on the skills API, plus a documentation accuracy sweep

  • β€’The skills API now honours the `Idempotency-Key` header on writes. A retried call carrying the same key within 60 seconds replays the original response (marked `X-Idempotent-Replayed: true`) instead of running again, so a network retry cannot post twice. It is opt-in and namespaced per user, so reads are unaffected. See the Retry Semantics section of `/docs/error-handling`.
  • β€’New `/docs/environments` page. There is one production API at `api.heyarchie.ai` and one environment. The `sandbox` or `live` label on a key is organisation metadata that gates nothing, which the page now states plainly.
  • β€’New `/docs/pagination` page documenting the real `limit` / `offset` / `has_more` pattern the list endpoints share. The WorkStreams page previously described a `next_cursor` the API does not return, and the error-handling page promised an `Idempotency-Key` that was not wired; both are now corrected (the idempotency one by wiring it, above).
  • β€’The API reference adds a Download OpenAPI control plus a note that the 3.1 spec imports straight into Postman or Insomnia, and its left nav collapsed the duplicated WorkStreams tags into one consistent family.

WorkStreams taxonomy: Blueprint renamed to Workplan

  • β€’The WorkStreams noun for a template bound to a client is now **Workplan**, not Blueprint. The taxonomy reads WorkStream β†’ Workplan β†’ Cycle β†’ Task. The canonical REST surface is `/api/v1/workstreams/workplans/...` (list, create, get, update, archive, write-status, enable/disable-writes, triggers).
  • β€’Non-breaking. Every old `/api/v1/workstreams/blueprints/...` path still works. It is now a deprecated alias on the same handler, flagged `deprecated` in the OpenAPI spec, and will be removed in a future release. Update integrations to the `workplans` path when convenient.
  • β€’Response and request field names are unchanged: `blueprint_id` is retained as a deprecated alias field so existing payloads keep parsing. OpenAPI schema titles moved to Workplan / WorkplanCreate / WorkplanListResponse / WorkplanUpdate.
  • β€’Spec, llms.txt, agents.md, and the API reference regenerated from the live registry so the catalogue reads Workplan everywhere. No DB, FGA, or Temporal rename in this pass; only the developer-facing URL and display layer moved.

Per-key knobs land β€” workspace pin, rate-limit cap, description

  • β€’API keys now carry per-key metadata our mirror table holds independently of WorkOS: `workspace_id` (pin a key to a workspace in multi-workspace orgs), `rate_limit_per_minute` (override the 60/min default β€” wins over org override), `description` (free-text label), `environment` (sandbox|live). All four are editable in the portal Keys page (click a row to expand the detail panel β€” auto-saves 600ms after you stop typing) or via the SDK's new `client.keys.update(key_id, **fields)`.
  • β€’New SDK methods on `client.keys` (heyarchie 1.2.0): `get(key_id)` for single-key reads, `update(key_id, **fields)` for PATCH, `rotate(key_id, grace_period_hours=24)` for the mint-replacement-then-expire-parent flow. The rotated key inherits all of the parent's metadata; backend reaper job revokes the parent after the grace window expires. Existing `create()` now accepts all four mirror fields at create time.
  • β€’Backend now owns the entire keys lifecycle. `/api/v1/connect-keys/*` is the canonical customer-facing surface (list, get, create, patch, delete, rotate); WorkOS is a private secret-store implementation detail. Portal dashboard, SDK, CLI, and MCP all speak the same surface.
  • β€’Cache invalidation is automatic β€” PATCH writes the new rate-limit value directly into the in-process cache, DELETE flushes both the validator and the per-key cache, so any in-flight cached token gets 401 on the very next request (no 5-minute staleness window).
  • β€’Rate-limit error code is now `RATE_LIMIT.USER` (per-key/default) or `RATE_LIMIT.ORG` (org override). Branch on `code`, not `message`. The `X-RateLimit-Limit` header reflects the effective ceiling β€” your per-key override if set, else the org override, else 60/min.
  • β€’Backend reaper job (`apps/api/src/jobs/connect_keys_reaper.py`) runs every 15 minutes, walks `archie_connect.keys` for expired rows, revokes them at WorkOS, stamps `revoked_at` on the mirror, and flushes both caches. Replaces the portal's `app/api/cron/reap-ephemeral-keys` Next.js cron β€” that route now returns 410 with a pointer.
  • β€’Docs: new `/docs/managing-keys` walkthrough covers portal + SDK paths side-by-side. `/docs/auth` adds the per-key metadata table + correct scopes (18 slugs the create modal exposes). `/docs/rate-limits` rewritten with the override resolution order. `/docs/sdk/python` adds the Managing API keys section.

Reliability + observability sweep (internal, non-breaking)

  • β€’Accounting fine-tune promotion: v6.4-r20 is now the default `consult_accounting_expert` model on the dev tier. No API change β€” same skill names, same envelopes β€” but answer quality + citation discipline improved across the TAX / ASR / CMP / WLT clusters.
  • β€’Anthropic-overload resilience: the specialist now retries on `overloaded_error` and `rate_limit` with exponential backoff (PR #4710), and never persists an empty assistant message when a downstream specialist crashes (PR #4709). Partner-visible effect: fewer transient 5xx during Anthropic load spikes, fewer empty `messages` rows in chat history.
  • β€’Telemetry baggage Phase 3: conversation_id, file tags, and Haiku title-gen route now ride OpenTelemetry baggage end-to-end (PRs #4674 / #4676 / #4683). Doesn't change the public API; it does make Studio's Deep Inspector trace tree more reliable for any partner using the Connect API + the Studio trace surface.
  • β€’D-RAG corpus sync primitive (PR #4711) + alpha-deploy smoke check on Slack-install row presence (D7). Internal observability; doesn't change the skill catalogue.
2026-05-13alphabreaking

Playbooks surface retired β€” WorkStreams everywhere

  • β€’The `/api/v1/playbooks/*` REST surface is gone. The OpenAPI spec, llms.txt, agents.md, and MCP tool catalogue now publish the WorkStreams surface only: `POST /api/v1/workstreams/{templates,blueprints,cycles}` plus lifecycle (pause/resume/cancel/approve) and triggers. Replaces playbooks entirely; partner integrations should migrate to the new endpoints.
  • β€’Terminology: a WorkStream **template** binds to a client to become a **blueprint**; each execution of a blueprint is a **cycle**; a cycle contains **tasks**. NEVER "runs" β€” WorkStream executions are cycles. Skill-call docs and SDK examples updated to match.
  • β€’Developer portal: `/dashboard/playbooks/*` and `/docs/playbooks` removed. WorkStream UI lives in Archie WorkStream Studio (Tauri app), not the developers portal.
  • β€’Back-compat: the `playbook.creator` skill slug (`POST /v1/skills/playbook.creator`) is retained for SDKs in flight. Description reframed β€” it now produces WorkStream drafts. Prefer `workstream.draft` for new integrations.

Connect surface readiness + skill catalogue parity

  • β€’Skill catalogue is now 52 (51 public, 41 Teams-visible, 27 Slack-visible) and the developer portal hero stat + MCP catalogue (/api-reference, content/mcp-tools.json) match the live SKILLS registry. The 2026-04-08 sandbox playground (Try-it browser REPL) is fully removed β€” stale references in the status page and changelog were cleaned up.
  • β€’Connect surfaces re-audited end-to-end: MCP / Cowork / Slack / Connect REST / Agent 365 Path A + Path B / Teams / Copilot. Per-surface smoke runners ship at apps/api/scripts/smoke_connect_*.sh (5/5 pass on the dev tier).
  • β€’Capability check (the explain_capabilities tool) reads the V4 tool registry as fallback for chat surfaces β€” previously it told users zero tools were live on Slack/MCP/Web because state.tools_available was only populated by the Excel plugin path. WorkStream read tools (list / describe / status) now part of the catch-all skill (ask_archie) so 'what WorkStreams can you see?' resolves end-to-end.
  • β€’FGA shadow mode logging upgraded: both permission_shadow_allow (INFO) and permission_shadow_deny (WARNING) now emit, both tagged with surface=<canonical-label>. Closes the observability gap on shadow grants.
  • β€’Tool_allowlist contract enforced: 22 of 52 skills had pre-Phase-7 stale references (artifact_generation, generate_docx, generate_excel, workflow_advance, xero_get_payroll, plus 12 never-built names). All cleaned up; a sentinel test pins the contract to prevent future drift.

API reference redesign, playbooks in REST spec, auto-provisioning

  • β€’API reference redesigned: two-column layout (ops sidebar + content), no more cramped three-column grid. Adds authentication, error format, rate-limit headers, and versioning sections to the overview page.
  • β€’Playbooks added to the public OpenAPI spec: list, create, get, update, delete, invoke run, stream events. Playbooks were always a first-class REST resource β€” now they're in the spec and the API reference.
  • β€’Auto-provisioning on first MCP connector sign-in: users whose first contact with Archie is via Claude Desktop or Cursor are now provisioned a platform account automatically instead of hitting a 403.
  • β€’Connector base URL is now environment-driven. Alpha points at claude.alpha.heyarchie.com; production at claude.heyarchie.ai. Set NEXT_PUBLIC_CONNECTOR_URL per deploy to override.
  • β€’Docs sidebar nav unified into a single shared module. Previously the /docs and /api-reference layouts each maintained their own copy, which silently diverged on renames.

SDK streaming, playbooks docs, llms.txt playbooks

  • β€’Python SDK: invoke_stream() on both sync and async SkillsResource. Yields SkillStreamChunk(delta, event, data) objects as SSE events arrive β€” no polling, no buffering.
  • β€’Playbooks reference page at /docs/playbooks: create, invoke, stream events, run lifecycle states, debug breakpoints API, Python SDK examples, MCP tools, permission scopes.
  • β€’llms.txt updated with a Playbooks section so LLM consumers know the create/invoke/stream/poll endpoints and required permission scopes.

Agent-first developer experience

  • β€’agents.md and llms.txt published β€” LLM-readable skill catalogue and call examples baked into the OpenAPI regen template so they stay in sync.
  • β€’Quickstart code samples now inject the signed-in user's sandbox key prefix (curl + Python blocks).
  • β€’MCP docs page: quick-connect bar with one-click Cursor deeplink and anchor buttons for Claude Desktop, Claude Code, Windsurf.
  • β€’Docs landing: live platform status badge (operational/degraded/outage) with 60-second cache.
  • β€’Playbooks dashboard page (/dashboard/playbooks): list view + detail/invoke page with input form and run history.
  • β€’Scalar API reference: layout: modern, full Archie brand CSS variables.

Public beta opens

  • β€’Claude Connect (MCP) live at claude.heyarchie.ai with AuthKit MCP and dynamic client registration.
  • β€’REST gateway live at api.heyarchie.ai with bearer auth, rate limits, and audit logging.
  • β€’Registry-driven POST /api/v1/skills/{slug}/invoke so SDK callers stay in sync as skills evolve.
  • β€’Python SDK shipped to PyPI as heyarchie.
  • β€’Sandbox seed data: 3 test clients, 10 documents, 5 conversations, 1 WorkStream.

Skill registry rewrite (KSR)

  • β€’Skill catalogue grew from 19 to 36 canonical skills under a two-tier dispatch model: a general-intent classifier picks a primary cluster (TAX, ASR, CMP, WLT, BOP, XCT), then a specialist resolves the exact skill.
  • β€’Each skill now exposes x-skill-cluster in the OpenAPI spec so SDK callers can see which cluster a skill belongs to.
  • β€’Skill input/output schemas regenerated; LLM-readable summaries at /llms.txt and /agents.md updated to match.
  • β€’Skills reachable at POST /api/v1/skills/{slug}/invoke; for chat-style flows, the ask_archie skill is the first-class entry point that routes through the orchestrator and accepts free-form queries.

Portal preview

  • β€’Developer portal shell at developers.heyarchie.ai with API key management, usage charts, request logs, webhook config.
  • β€’OpenAPI 3.1 spec published at /openapi.json; rendered via Scalar viewer at /docs/api-reference.