apiwarden
Point it at a directory of OpenAPI specs and it serves them as live documentation — a browsable site for people, and an MCP server plus plain JSON for AI agents. Nothing to export, nothing to re-share: the specs are read from disk on every request, so whatever is on the branch is what the docs say.
Runs standalone, or mounts into an existing Django project in two lines.
pip install apiwarden
apiwarden serve ./api-docs
Why
API documentation that lives in files has to be sent to whoever needs it, again after every change. A frontend team ends up working from whichever copy they were last given, and no one can tell what moved. Serving the specs instead of sending them makes that whole problem go away: one URL, always current, for people and for the agents they work with.
The two audiences
People get a portal built on RapiDoc:
a nav of colour-coded methods and URL paths, the read layout, server selection
and try-it. On top of that it adds an API switcher and search across every
spec, and it rescues the two things OpenAPI renderers normally drop — the
info.description narrative, which becomes navigable headings, and the
top-level x-* blocks where teams record rate limits, TTLs and everything else
that does not fit the schema, which become tables in the overview.
The sidebar also holds one Bearer token field, not one per spec. Set it once
and it applies to try-it on every API — it lives in the browser's
localStorage, never on the server, so it survives switching between APIs
without being re-entered.
An edit to a spec reaches an open page in about a second, swapped in through the renderer rather than by reloading, so nobody loses their place.
Agents get the same content as data:
| Endpoint | What it is |
|---|---|
POST /mcp |
MCP server — list_apis, search_operations, get_operation, get_schema, get_conventions, list_changes, get_spec |
GET /index.json |
Every operation across every spec, one compact document |
GET /llms.txt, /llms-full.txt |
The doc set as plain text |
GET /openapi/<name>.json, .yaml |
The raw specs, byte-faithful |
GET /display/<name>.json |
The renderer's copy: x-* folded into the overview |
GET /operation/<id>.json |
One operation, $refs inlined |
GET /revision.json |
Content hashes — poll to tell whether anything changed |
GET /changes.json |
The changelog, newest first; ?since=… compares against one baseline |
Point an agent at the MCP endpoint once and it never reads a stale spec again:
{
"mcpServers": {
"apiwarden": { "type": "http", "url": "https://your-host/api-docs/mcp" }
}
}
Locally, over stdio instead:
apiwarden mcp ./api-docs
Standalone
apiwarden serve ./api-docs # http://127.0.0.1:8080, reloads as you edit
apiwarden check ./api-docs # lint: operationIds, summaries, unresolved $refs
apiwarden build ./api-docs -o dist/ # self-contained static copy, for CI publishing
apiwarden changes ./api-docs --since v1.4.0
apiwarden snapshot ./api-docs -o baseline.json
serve takes a directory, a port, or both, in either order — a bare number is
read as a port, so the common case of "same specs, different port" is short:
apiwarden serve 8081 # default directory, port 8081
apiwarden serve ./api-docs 8081 # both
apiwarden serve ./api-docs --port 8081 # the explicit form, still fine
It watches the spec files and pushes a reload to open browsers, so editing a spec updates the page without a restart.
What changed
Being always current is only half the problem — the other half is knowing what
moved. apiwarden keeps a changelog by itself: whenever a spec changes — an edit
while it is serving, or a new deploy noticed at startup — it compares the API
contract against the last one it saw and records the difference with a
timestamp. Open /changes and the history is there, newest first, each change
sorted by what it does to a caller. There is nothing to tag or configure.
$ apiwarden changes ./api-docs
2026-10-05 14:32 (2 breaking, 0 additive, 1 informational)
breaking accounts POST /v1/accounts/otp/ field-added: request.device_id (required)
breaking accounts POST /v1/accounts/otp/ response-removed: 429 no longer documented
info accounts POST /v1/accounts/otp/ summary-changed: Send a one-time login code.
Breaking is an operation or field disappearing, a new required field or parameter, a type change, an enum value being removed, or authentication being added. Additive is anything a current caller can ignore. Only the contract counts: rewording a description is not logged. Saves less than 15 minutes apart fold into one entry, and an edit undone within that window leaves no trace.
The log lives in your cache directory (~/.cache/apiwarden/), so the spec
directory is never written to. Set history to a path on persistent storage to
keep it across container rebuilds. On the very first run, if the specs are in a
git checkout, the recent commits that touched them are read once to seed it.
To compare against one specific version instead — a release tag, a commit, or a
file written earlier with apiwarden snapshot — pass --since (or ?since= on
the page). --fail-on-breaking exits non-zero, so CI can gate on it:
$ apiwarden changes ./api-docs --since v1.4.0 --fail-on-breaking
The same history is in /changes.json and the list_changes MCP tool, so an
agent can answer "will this break my client?" directly.
In a Django project
# settings.py
INSTALLED_APPS += ["apiwarden"]
APIWARDEN = {
"root": BASE_DIR / "api-docs",
"title": "Platform API",
"servers": ["https://api.example.com"], # what try-it should call
"watch": DEBUG,
"token": os.environ.get("APIWARDEN_TOKEN"), # omit for a public portal
}
# urls.py
urlpatterns += [path("api-docs/", include("apiwarden.urls"))]
That is the whole integration. The portal serves its own assets, so there is no
collectstatic step, and it adds no dependency beyond PyYAML. It coexists with
whatever documentation the project already has — it reads spec files and does
not touch your URLs, views, or schema generation.
Two production notes:
watchdefaults toDEBUG. Live reload holds an SSE connection open, which pins a sync worker; in production, agents pollrevision.jsoninstead.- "Try it" calls the API host from the browser, so that host needs to allow the docs origin in its CORS configuration.
Configuration
Settings are the same for both, via APIWARDEN, an apiwarden.toml beside the
specs, or CLI flags.
| Key | Default | Meaning |
|---|---|---|
root |
api-docs |
Directory holding the specs |
title |
derived | Portal title |
servers |
spec's own | Base URLs offered for try-it |
theme |
auto |
auto follows the reader's OS setting; light/dark pin it |
watch |
False |
Reload when the spec files change |
token |
None |
Require a shared token on every request (also APIWARDEN_TOKEN) |
sources |
discovered | Explicit {name: path} map |
history |
cache dir | File the changelog is kept in |
How specs are discovered
- An explicit
sourcesmap, if you set one. - Otherwise a
redocly.yamlnext to the specs — itsapis:entries are used as-is, keeping the names and ordering an existing doc set already has. - Otherwise every
openapi.yaml/.yml/.jsonbelowroot, each named after its parent directory.
Specs are expected to be self-contained, using local #/components/... $refs.
Development
python scripts/vendor_assets.py # download the renderer bundle
pip install -e ".[dev]"
pytest
# The browser tests are opt-in; they catch things a server-side test cannot,
# such as the renderer silently failing to load.
pip install -e ".[dev,browser]" && playwright install chromium
pytest tests/test_browser.py
tests/fixtures/sample-api/ is a small generic doc set this repo ships as its
own test fixture and demo — try apiwarden serve tests/fixtures/sample-api to
see it running without needing specs of your own yet. Every ./api-docs
above is illustrative: point it at whatever directory holds your specs.
License
MIT
Metadata
Release files for apiwarden 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| apiwarden-0.4.0.tar.gz | 292.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| apiwarden-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 573.5 kB
Release files / apiwarden-0.4.0.tar.gz
| Download URL | apiwarden-0.4.0.tar.gz |
|---|---|
| Size | 292.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c2321bdc213ec35ad356175951b7e9c4adb491b82995c59bdabb77e280f06122
|
|
BLAKE2b-256 checksum How to use checksums |
d63d4684b043e84cded19cf9ef9d28a7cf09f98c43cda93d8ad6f75ec303eba2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / apiwarden-0.4.0-py3-none-any.whl
| Download URL | apiwarden-0.4.0-py3-none-any.whl |
|---|---|
| Size | 281.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2ba11e23b712b793d5035ebd00b4ab795a3ac3df48776539e0f7299cbea15969
|
|
BLAKE2b-256 checksum How to use checksums |
802bcbc915d1b3bb11eb1d0e007dedf86a02b419cb30fc4b264f69f1adada918
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency log