# info.summary
Read-only JSON API for Krishna Veera Vanamali's writing archive at whyvanamali.com.

# info.description
Read-only access to the essays published on https://whyvanamali.com: list and search posts, fetch one post with its Markdown body, list the six categories, and get a summary of the site and its author.

Authentication: none. All endpoints are public and read-only; they only serve content that is already published on the site.

CORS: every response carries `Access-Control-Allow-Origin: *`, so browser-based agents can call the API directly.

Caching: successful responses are cacheable (`Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400`); content changes only when the site is deployed.

Errors: every error is an RFC 9457 `application/problem+json` document with a stable `code`, a human-readable `detail` and a `resolution` hint (see `ProblemDetails`).

Versioning and deprecation policy: the `/api/v1` paths are stable. A breaking change ships as a new major path (`/api/v2`) and the old one keeps working; any deprecation is announced at least 90 days ahead with `Deprecation` and `Sunset` response headers (RFC 9745, RFC 8594) and a note on the docs page.

Rate limits: 120 requests per 60 seconds per client IP (policy `api-v1`), enforced per server instance, so approximate rather than exact globally. Every response carries the IETF RateLimit-Policy and RateLimit fields plus RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; past the limit the API answers 429 application/problem+json (code rate_limited) with Retry-After in seconds. For bulk use prefer /llms.txt, the sitemap, or the Markdown representation of pages.

Markdown for agents: every HTML page on https://whyvanamali.com is also served as Markdown (Accept: text/markdown, or append .md). An MCP server with the same capabilities is at https://whyvanamali.com/mcp; docs at https://whyvanamali.com/docs.

# externalDocs.description
Human-readable docs: Markdown negotiation, JSON API, OpenAPI, MCP.

# paths./api/v1/posts.get.description
Returns posts newest first. Use `q` for a case-insensitive search across title, tags, summary and body (title matches rank first; every term must appear), `tag` and `featured` to filter, `limit`/`offset` to page, and `fields` to trim the payload.

# paths./api/v1/posts.get.parameters[].description
Search terms (space separated; all must match).

# paths./api/v1/posts.get.parameters[].description
Only featured (true) or non-featured (false) posts.

# paths./api/v1/posts.get.parameters[].description
Comma-separated subset of PostSummary properties to return. Allowed: slug, title, date, tags, summary, featured, url, linkedinUrl, images, youtubeUrl, pdfUrl, visual.

# paths./api/v1/posts.get.responses.400.description
A query parameter is invalid (problem code `invalid_parameter`).

# paths./api/v1/posts.get.responses.405.description
Only GET, HEAD and OPTIONS are supported (problem code `method_not_allowed`).

# paths./api/v1/posts.get.responses.429.description
The client exceeded 120 requests in 60 seconds (problem code `rate_limited`); wait Retry-After seconds.

# paths./api/v1/posts/{slug}.get.description
Returns the post identified by its slug (the last segment of the post URL), including the authored Markdown body, the full Markdown representation, margin notes and the neighbouring posts.

# paths./api/v1/posts/{slug}.get.responses.404.description
No post has that slug (problem code `post_not_found`).

# paths./api/v1/posts/{slug}.get.responses.405.description
Only GET, HEAD and OPTIONS are supported (problem code `method_not_allowed`).

# paths./api/v1/posts/{slug}.get.responses.429.description
The client exceeded 120 requests in 60 seconds (problem code `rate_limited`); wait Retry-After seconds.

# paths./api/v1/tags.get.summary
List the content categories with post counts

# paths./api/v1/tags.get.description
The six categories every post is filed under, with the number of posts in each and the filtered archive URL. `sort=count` puts the busiest categories first.

# paths./api/v1/tags.get.parameters[].description
`default` keeps the site's category order; `count` sorts by number of posts, most first.

# paths./api/v1/tags.get.responses.405.description
Only GET, HEAD and OPTIONS are supported (problem code `method_not_allowed`).

# paths./api/v1/tags.get.responses.429.description
The client exceeded 120 requests in 60 seconds (problem code `rate_limited`); wait Retry-After seconds.

# paths./api/v1/site.get.description
Name, description, author profile links, bio, the list of pages, companion playbooks, machine-readable resources (docs, llms.txt, sitemap, OpenAPI, MCP) and counts. `fields` trims the answer to the sections you need (name, url and description are always present).

# paths./api/v1/site.get.parameters[].description
Comma-separated subset of sections to return. Allowed: author, bio, pages, playbooks, resources, counts.

# paths./api/v1/site.get.responses.405.description
Only GET, HEAD and OPTIONS are supported (problem code `method_not_allowed`).

# paths./api/v1/site.get.responses.429.description
The client exceeded 120 requests in 60 seconds (problem code `rate_limited`); wait Retry-After seconds.

# components.schemas.ProblemDetails.description
RFC 9457 Problem Details. `code` is a stable machine-readable identifier, `resolution` says what to change, `documentation_url` links the error table in the docs.

# components.schemas.ProblemDetails.properties.type.description
URI identifying the problem type (an anchor in the docs).

# components.schemas.ProblemDetails.properties.title.description
Short, human-readable summary of the problem type.

# components.schemas.ProblemDetails.properties.detail.description
Human-readable explanation specific to this occurrence.

# components.schemas.ProblemDetails.properties.instance.description
The request path that produced the problem.

# components.schemas.ProblemDetails.properties.resolution.description
What the caller can do to fix or avoid the problem.

# components.schemas.PostSummary.description
A post in the writing archive, without its body.

# components.schemas.PostSummary.properties.slug.description
URL slug; the last segment of the post URL.

# components.schemas.PostSummary.properties.tags.description
One or two of the six content categories.

# components.schemas.PostSummary.properties.summary.description
One-line summary (curated, or derived from the first sentence).

# components.schemas.PostSummary.properties.featured.description
Whether the post is featured on the home page.

# components.schemas.PostSummary.properties.url.description
Canonical HTML URL. Request it with Accept: text/markdown for Markdown.

# components.schemas.PostSummary.properties.linkedinUrl.description
URL of the LinkedIn post this essay mirrors, when there is one.

# components.schemas.PostSummary.properties.youtubeUrl.description
Embedded YouTube video, when there is one.

# components.schemas.PostSummary.properties.pdfUrl.description
Attached PDF document (deck, report), when there is one.

# components.schemas.PostSummary.properties.visual.description
Whether the post appears on the /visuals wall.

# components.schemas.MarginNote.description
A time-travel margin note: a later annotation attached to the first paragraph containing `after`.

# components.schemas.MarginNote.properties.when.description
When the note was written, e.g. 'Jun 2026'.

# components.schemas.MarginNote.properties.link.description
Optional link (site-relative or absolute) the note points to.

# components.schemas.Post.allOf[].properties.content.description
The post body as authored (GitHub-flavoured Markdown).

# components.schemas.Post.allOf[].properties.markdown.description
The complete Markdown representation (title, meta, attachments, body with absolute links and inlined margin notes, navigation). Identical to what Accept: text/markdown returns.

# components.schemas.Post.allOf[].properties.adjacent.description
Neighbouring posts in the newest-first archive.

# components.schemas.PostList.properties.data.description
Page of posts, newest first (or by relevance when `q` is set). When `fields` is used, only those properties are present.

# components.schemas.Tag.properties.name.description
Category name (use it as the `tag` filter).

# components.schemas.Tag.properties.code.description
Two-letter code used in shareable /writing?t= links.

# components.schemas.Site.description
Who runs the site, what is on it, and where the machine-readable resources live.

# components.schemas.Site.properties.resources.description
Machine-readable entry points: docs, llms.txt, sitemap, OpenAPI, RFC 9727 catalog, API base, MCP endpoint and card, registry manifest, and the markdown convention.

# components.headers.RateLimit-Policy.description
The rate-limit policy as an IETF structured field: quota (q) per window in seconds (w), e.g. "api-v1";q=120;w=60.

# components.headers.RateLimit.description
Current state as an IETF structured field: remaining requests (r) and seconds until the window resets (t), e.g. "api-v1";r=119;t=55.

# components.headers.RateLimit-Remaining.description
Legacy form of the remaining requests in the current window.

# components.headers.RateLimit-Reset.description
Legacy form: seconds until the current window resets.

# components.headers.Retry-After.description
Seconds to wait before retrying (sent with 429).