VayuPress API Reference
Last reviewed: 2026-10-16, against VayuPress 3.18.7
Overview
Base URL: https://yourdomain.com/api/v1 — or https://api.yourdomain.com/api/v1,
the challenge-free host for scripts, CI and agents (keep its CDN proxy off).
Authentication: an API key, as X-API-Key: <key> or Authorization: Bearer <key>.
Content-Type: application/json
Machine-readable spec: GET /api/v1/openapi.json (OpenAPI 3.0.3).
GraphQL: GET|POST /api/v1/graphql, query-only — writes go through REST.
Reads of published content (GET /api/v1/articles, search, tags) are public.
Everything that writes, and every administrative read, needs a key.
Keys
There are two kinds:
- Scoped keys — minted in VayuOS → API keys. Each grants chosen
section:actionpairs from a 12 × 6 grid (sections: posts, themes, design, plugins, domains, analytics, media, comments, members, mail, settings, backup; actions: read, write, delete, apply, install, export), with an optional expiry and its own per-minute budget. Stored only as a hash; shown once. Every call is checked against the key's grant and written to the audit log. Reference → - The install key —
API_KEYin/etc/vayupress/env. It can do everything; keep it for the operator's own use and rotate it as RB-09 in OPERATIONS.md describes.
A key-authenticated request needs no CSRF token. The CSRF double-submit
(vp_csrf cookie echoed in X-CSRF-Token) applies to requests made with a
signed-in browser session.
Rate limits
- Per key, for the API and MCP alike: the key's own budget (a number, or
unlimited), or else the default set on API keys › Limits (a number or
unlimited), or else the built-in 600 a minute (
VAYU_API_RATE_PER_MIN). An authenticated call meets no other limit.
Over the budget the answer is 429 with Retry-After. Use
POST /api/v1/articles/bulk for many posts at once.
Errors
{
"error": {
"code": "invalid_slug",
"message": "Slug must be lowercase alphanumeric with hyphens/underscores.",
"request_id": "a1b2c3d4",
"docs": "/docs/api/errors#invalid_slug"
}
}
request_id matches the X-Request-Id response header and the server log line.
Articles
List articles
GET /api/v1/articles?page=1&limit=20&tag=go
limit is at most 100. Response:
{
"articles": [
{
"id": "7ff8e4c056eb6015e09e9e4a148a7ca5",
"title": "My Article",
"slug": "my-article",
"tags": ["go", "sqlite"],
"created_at": "2026-10-16T16:41:52Z",
"updated_at": "2026-10-16T16:41:52Z"
}
],
"page": 1,
"limit": 20,
"total": 234727,
"pages": 11737
}
Get an article
GET /api/v1/articles/{slug}
GET /api/v1/articles/{slug}/toc
Returns the article with its content; /toc its table of contents.
Create
POST /api/v1/articles
X-API-Key: <key>
{
"title": "My Article",
"slug": "my-article",
"content": "<p>Hello world</p>",
"tags": ["go", "sqlite"]
}
The write is queued and applied by a worker: the answer is 202 with
{"status": "queued", "id": "…", "slug": "my-article"}, and the article is
readable a moment later. Content is HTML, sanitised to the
article allowlist; an inline <svg> diagram is kept under its own allowlist.
Tags are normalised to topics (one address each).
Create many
POST /api/v1/articles/bulk
An array of the same objects, up to 1,000 per request.
Update, delete
PUT /api/v1/articles/{slug} { "title"?, "content"?, "tags"? }
DELETE /api/v1/articles/{slug}
PUT changes only the fields it carries. Each write purges the article's
cached pages and updates the search index.
Search and topics
GET /api/v1/search?q=<query>&limit=20
GET /api/v1/tags
Search is VayuFind, built in. /tags lists topics with their counts.
Comments
GET /api/v1/articles/{slug}/comments
POST /api/v1/articles/{slug}/comments (a signed-in member)
GET /api/v1/admin/comments (key)
A comment appears once it is approved.
Media
Media is uploaded through the console API, which accepts a key as well as a session:
POST /os/api/media/upload (multipart: file)
GET /os/api/media
Files are stored content-addressed, checked by magic number, and downscaled when oversized.
Analytics
GET /api/v1/analytics/overview | pageviews | pages | referrers | browsers |
devices | os | utm | events | realtime | sessions | funnels | retention |
revenue | goals | journey
GET /api/v1/analytics/export (Parquet)
GET /api/v1/sites/{site}/analytics/overview | realtime | pages | sources |
events | searches | goals | funnels | funnels/{id} | performance |
content
The /api/v1/analytics family adds up every site, so a key bound to one site
is refused it. The /api/v1/sites/{site}/analytics family reads one site,
named by its id or its host, with the analytics grant: an install key reads
any site, and a site-bound key reads its own site only. Any other site answers
404, exactly as a site that does not exist. ?days= sets the window (1 to
365, 30 by default). A site's searches gives each query's searches,
visitors and searches that found nothing, and follow_up: how many of the
window's searches were followed in their visit by a page, by another search
first, or by nothing, the pages opened after searching, and the queries tried
again as others. performance gives the readers' vitals at p75 (a vital no
reader reported is null, with each vital's own samples_*), and server
and server_by_day: the site's requests by status and its pages' server
time (the mean, and p50, p75 and p95 as the bound of their bucket,
_at_most), with the pages the page cache answered and those it rendered,
each with its mean (cached_pages, cached_mean_ms, rendered_pages,
rendered_mean_ms). content gives each of the seven categories of
what the content did (docs/ANALYTICS.md) with its rule, its total, the 25
pages it lists first with their figures and reason, or not_compared when
the site's pages were too few for it.
No cookie is set, and no IP address or user agent is stored. A visit is keyed
by a hash whose salt rotates daily, so a reader cannot be followed from one day
to the next. For the same reason retention answers
{"measurable": false, "identity": "daily", "reason": …, "cohorts": []}:
no visitor can be seen to return on a later day, so there are no cohorts to
report.
VayuShield
The shield is set from the console, so its endpoints are the console's: a
signed-in administrator's session, with the CSRF token on every write, for
the install's and for each site's (docs/VAYUSHIELD.md).
GET /os/api/shield/export the shield's settings and lists
POST /os/api/shield/settings save the install's settings and rules
POST /os/api/shield/simulate replay recorded views against a draft
POST /os/api/shield/release release a jailed address
POST /os/d/{id}/api/shield save one site's settings and rules
POST /os/d/{id}/api/shield/simulate replay that site's views against a draft
GET /os/d/{id}/shield/feed that site's newest refusals
A client with a key reads the shield through MCP: vayushield_status,
vayushield_posture, vayushield_settings and vayushield_history.
Operations
GET /api/v1/queue the write queue
POST /api/v1/queue/replay replay dead-lettered jobs
GET /api/v1/admin/mode the install's mode
GET /api/v1/admin/outbox/stats the event outbox
GET /api/v1/admin/search/drift search index against the database
POST /os/api/search/reindex rebuild the search index
POST /os/api/storage/refresh-pages redraw every cached page
Health and metrics
GET /health, /health/live, /health/ready public
GET /health/db | /workers | /queue | /search | /storage | /migrations | /dependencies
GET /metrics Prometheus format
The detail probes and /metrics answer only a request carrying the API key or
coming from the machine itself; anyone else gets 404.
Webhooks
POST /api/v1/admin/webhooks registers an endpoint for article events. Each
delivery carries X-VayuPress-Signature: sha256=<hex>, an HMAC of the raw body
under the webhook's secret, so the receiver can verify it;
GET /api/v1/admin/webhooks/{id}/deliveries shows its history.
The extension contract (VCB)
GET /api/v1/vcb/contract the live contract
POST /api/v1/vcb/validate validate a plugin.json or theme.json
MCP
The same capabilities, as tools for Claude and any MCP client, are at /mcp
with OAuth 2.1. The connector →
API Stability Tiers
Which routes are stable, and what a stable route promises, is in compatibility/stability-matrix.md and compatibility/api-contracts.md.