Skip to content

heydenys.com API docs

heydenys.com is a 10-page portfolio that also serves itself as data. Every page comes back as Markdown if you ask for it, the whole site fits in one file, and there is a read-only JSON API described by OpenAPI 3.1.

Read the whole site in one request

/llms-full.txt is every page concatenated as Markdown — about 18 KB, roughly 4,400 tokens. It is the cheapest way to answer almost any question about Denys Ishchenko, and it is complete: there is nothing behind a link on this site that is not already in it.

/llms.txt is the shorter llmstxt.org overview: what this site is, when it is worth consulting, when it is not, and a link to every page.

Every page, as Markdown

Ask any page for Markdown and you get Markdown, at the same URL, per the acceptmarkdown.com convention. Appending .md to a path does the same thing for clients that send no Accept header.

curl https://www.heydenys.com/about -H 'Accept: text/markdown'
curl https://www.heydenys.com/about.md

The Markdown is hand-written per page rather than converted from the HTML, so it carries the content without the layout. Q-values are honoured and a client that refuses both HTML and Markdown gets a 406 listing what is on offer.

The JSON API

Version 1.0.0. Read-only, no authentication and no key. Responses are cached at the edge for an hour and every one of them sets Access-Control-Allow-Origin: *, so a browser-side agent can call them directly.

Every endpoint answers at two addresses and returns identical bytes at both: the pinned one below, and the same path without /v1.

  • GET /api/v1
    This index: every endpoint, and where the API is described.
  • GET /api/v1/pages
    Every page on the site with its title, summary and last-modified date.
  • GET /api/v1/pages/{slug}
    One page, including its complete Markdown body.
  • GET /api/v1/profile
    The canonical facts about Denys Ishchenko: role, location, history, contact.
  • GET /api/v1/markdown/{slug}
    One page as raw Markdown, unwrapped.

Start at /api/v1, which lists all of it, both policies below, and links back here.

OpenAPI

The complete description lives at /openapi.json — OpenAPI 3.1, with an operationId, a summary and a description on every operation, typed parameters, and a response schema for every status. It is generated from the same route table the site is built from, so it cannot describe an endpoint that does not exist.

The same document is served at /openapi.yaml and /api/openapi.json. Every page also advertises it with rel="service-desc", so a crawler that has only fetched the homepage can still find it.

An agent holding nothing but the domain can start one step earlier, at the RFC 9727 catalogue: /.well-known/api-catalog is a linkset naming the spec, these docs and the versioned base.

Versioning and deprecation

/api/v1/… is pinned to the v1 response shapes and is the one to hard-code. /api/… serves the same bytes but always tracks the current major, so it is the convenient one and the one that can move under a client. Every JSON response says which shape produced it in API-Version.

Nothing is deprecated. When something is, the responses that are going away start carrying `Deprecation` (RFC 9745) with the date the decision took effect and `Sunset` (RFC 8594) with the date they stop answering, at least 180 days apart, plus `Link: rel="deprecation"` pointing at the page that explains it. A breaking change to a response shape is not a deprecation: it is a new major at /api/v2, and /api/v1 keeps answering until its own Sunset date. Pin /api/v1 if that matters; /api/... without a version always tracks the current major.

Rate limits

600 requests per 60 seconds per client address, enforced at the origin. Every JSON response carries the quota, readable from a browser too, so a client can pace itself before its first failure:

RateLimit-Policy: "content";q=600;w=60
RateLimit-Limit: 600

Successful responses are cached at the edge for an hour and mostly never reach the origin, so they are never counted — and that is also why a cached 200 carries no live counter. A per-client number inside a shared-cached body would be about whoever missed the cache, handed to everyone who hit it afterwards. The live RateLimit header appears on the responses that are no-store: every error, and the 429, which also carries Retry-After in seconds.

Polling is the wrong shape for this site in any case. It is 10 pages that change a few times a year; /llms-full.txt is all of them in one request.

From the command line

There is a one-file client, with no install step and no dependencies. It calls the pinned base, honours Retry-After, and prints the API’s own error hints:

curl -fsSL https://www.heydenys.com/cli/heydenys.mjs -o heydenys.mjs
node heydenys.mjs profile
node heydenys.mjs page fitline
node heydenys.mjs read

Node 18 or newer. It is not on npm — the source is the file itself, at /cli/heydenys.mjs.

Errors are JSON

Every non-2xx response under /api uses one envelope, with one exception: a Markdown read of a page that does not exist answers 404 in Markdown, listing every page that does. code is stable and safe to match on; hint always names a call that would have worked.

{
  "error": {
    "status": 404,
    "code": "page_not_found",
    "message": "No page exists with the slug \"portfolio\".",
    "hint": "Call GET /api/pages for every slug this site serves.",
    "documentation": "https://www.heydenys.com/docs",
    "requestedSlug": "portfolio",
    "availableSlugs": ["index", "about", "playground", "…"]
  }
}

What this API is not

It is read-only, and it will stay that way — there is nothing here to write to. It publishes no availability, no rates and no contract terms, because this site does not publish those anywhere; an answer inferred from this data would be invented. Email me@heydenys.com for those.

It also is not an API for PM-International or for FitLine. It covers Denys Ishchenko’s own design work and nothing else.