Your OAuth guide explains the code exchange in four paragraphs, but readers still post the code to the token endpoint from the browser. The one picture that would have shown the order of the calls is a PNG somebody drew two years ago. Nothing in the build knows that the PNG still shows an endpoint the last release renamed.
What you get
You will end up with sequence diagrams that live beside the reference pages in version control. A CI check fails when a diagram names an operation the spec no longer has, and the renders read on a dark page. This is for you if you document an API with a flow of more than one call.
Short answer
Write the flow as a Mermaid sequence diagram in a fenced block beside the reference page, with an accessible title and description and a paragraph that says the same thing in words. Name the operationId in each message, and run a check in CI that fails when the spec no longer has that operation. Render the dark theme as well as the default, because a diagram themed for one page is unreadable on the other.
You will need
Node 22 or later, an OpenAPI 3.1 document, and a site generator that renders a mermaid fence.
GitHub renders one in any markdown file,
and the integrations list names
the plugins for Docusaurus, MkDocs, Sphinx, and GitLab. The render step needs mermaid-cli and a
Chromium it can start. Verified 2026-09-25 against Node 22.22.2, @mermaid-js/mermaid-cli
12.0.0, and yaml 2.9.1.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| An exported image from a drawing tool | A one-off figure that nobody will edit again | A long text description you write and maintain by hand, and a diff that is a binary blob | The flow will change with the spec, which a multi-call flow does |
| D2 | You want a say in the layout, with a choice of three layout engines | A Go binary in the docs build, and SVG files you commit or generate because no site generator renders it natively | Your site renders Mermaid fences already and the flows are short |
| Mermaid | Docs on GitHub or a generator with a Mermaid plugin, and flows short enough to need no layout control | The least control over layout of the three, and colors that belong to the theme rather than to your page | Layout matters more than toolchain, or the flow has dozens of messages |
| PlantUML | A team with Java in the build already, and a house set of skinparam settings to apply | Java in the docs build, Graphviz for some diagram types, and PNG output unless you ask for SVG | Nobody wants a JVM in the documentation pipeline |
Mermaid asks for nothing your site does not already have, and gives you the least say over where things go. D2 and PlantUML each put a binary in the docs build and hand you layout engines or skin parameters in return. The drawn image costs the most over time, because nothing in the build can tell you when the spec moves and the picture does not.
Write the flow beside the page, naming the operation in each message
The diagram is text in the same markdown file as the prose, so a pull request that changes the flow shows the change as lines. Each message that corresponds to an API call ends with the operationId in parentheses.
sequenceDiagram
accTitle: Authorization code exchange
accDescr: The browser is sent to authorize, comes back to the client with a code, the client exchanges the code for a token at the token endpoint, and calls the Orders API with it.
participant B as Browser
participant C as Client
participant A as Authorization server
participant O as Orders API
B->>A: GET /oauth/authorize (authorize)
A-->>B: 302 to the redirect URL with a code
B->>C: GET /callback with the code
C->>A: POST /oauth/token (exchangeToken)
A-->>C: 200 with access_token and expires_in
C->>O: GET /orders (listOrders)
O-->>C: 200 with the orders
Three things in that block do work beyond drawing. accTitle and accDescr become a title
and a desc element inside the SVG, and Mermaid points aria-labelledby and
aria-describedby at them, which
its accessibility page shows in full. The
operationId in parentheses is what the CI check reads. And the paragraph after the fence says
the flow in words, for the reader with images turned off and the agent that never loads them.1
The webhook diagram is the same shape. Its orderCreated message cites an operation that
lives under the document’s
webhooks key rather than under
paths, and the checker reads both.
Check every diagram against the spec
No linter reads pictures, so a diagram goes on contradicting the spec from the day an endpoint is
renamed. The check parses every mermaid fence under docs, and holds each cited operation to the document.
A run that finds no fences fails, so a CI step pointed at the wrong directory cannot pass forever.
for (const msg of seq.messages) {
if (!msg.id) continue
const op = ops.get(msg.id)
if (!op) {
findings.push({ kind: 'unknown-operation', detail: `${at(msg.line)} names ${msg.id}, which is not in the spec` })
continue
}
if (msg.method && msg.method !== op.method) {
findings.push({ kind: 'wrong-method', detail: `${at(msg.line)} says ${msg.method} and the spec says ${op.method} for ${msg.id}` })
}
if (msg.path && op.kind === 'path' && msg.path !== op.path) {
findings.push({ kind: 'wrong-path', detail: `${at(msg.line)} says ${msg.path} and the spec says ${op.path} for ${msg.id}` })
}
}
Against the release the diagrams were written for, everything matches.
node check-diagrams.mjs docs openapi.yaml
2 sequence diagrams under docs, against openapi.yaml 3.2.0
docs/oauth-exchange.md:7 "Authorization code exchange" 7 messages, 3 cite an operation
docs/webhook-round-trip.md:7 "Webhook round trip" 6 messages, 3 cite an operation
every cited operation matches the spec
Release 4.0.0 renames exchangeToken to issueToken and moves listOrders to /v2/orders.
The same command against that document is the pitfall, caught.
node check-diagrams.mjs docs openapi-next.yaml
2 sequence diagrams under docs, against openapi-next.yaml 4.0.0
docs/oauth-exchange.md:7 "Authorization code exchange" 7 messages, 3 cite an operation
unknown-operation docs/oauth-exchange.md:17 names exchangeToken, which is not in the spec
wrong-path docs/oauth-exchange.md:19 says /orders and the spec says /v2/orders for listOrders
docs/webhook-round-trip.md:7 "Webhook round trip" 6 messages, 3 cite an operation
2 findings
Each finding carries the file and line of the message, so the fix is an edit to one line of the diagram, in the same pull request as the spec change. The checker also refuses a diagram with more than twelve messages, because a long flow is the one that drifts first and the one nobody rereads. Split it, and give each half a page.
Render it for a dark page as well as a light one
Mermaid’s themes color the text, and the page colors the background, so a render made for one
page can be invisible on the other. render.mjs draws every diagram twice with mermaid-cli,
in the default theme and the dark theme, and writes the SVG files under rendered. It needs
a Chromium to start, so it runs on a workstation and the files are committed.
node render.mjs
The inspection reads the committed files. It takes the background the page will paint and computes the contrast of the message text against it, as WCAG 2.1 defines the ratio. The dark value here is this site’s own code panel.
node inspect-svg.mjs '#101018' rendered/oauth-exchange.dark.svg rendered/oauth-exchange.default.svg
rendered by @mermaid-js/mermaid-cli 12.0.0
page background #101018
oauth-exchange.dark.svg
role sequence, title "Authorization code exchange", description 33 words
text lightgrey on #101018: contrast 12.6:1, readable
oauth-exchange.default.svg
role sequence, title "Authorization code exchange", description 33 words
text #333 on #101018: contrast 1.5:1, below the 4.5:1 floor
The default theme’s text is #333, which is 1.5:1 on a dark panel, and the ratio reverses to
the decimal on white. Serve both renders and let the browser choose with a
picture
element and a prefers-color-scheme media query, or let the site’s Mermaid plugin pick the
theme at render time. Do not fix the theme inside the diagram unless the site has one color
scheme, because the diagram’s own frontmatter wins over every other setting.2
Check it worked
Twenty tests pin the checker and the inspection. Two carry the page: the diagrams match
release 3.2.0 and contradict 4.0.0 on unknown-operation and wrong-path, and the dark render
is readable on the dark panel while the default render is not.
node --test diagrams.test.mjs
1..20
# tests 20
# suites 0
# pass 20
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 255.037197
Open the page with images turned off, or read its markdown twin. The paragraph that starts
In words: is the diagram, and if it is missing the checker reports no-prose for that fence.
When it goes wrong
The checker reports an unknown operation and the diagram is right. The operationId in the
parentheses has a typo, or the operation is a webhook and the document keeps it somewhere other
than the top-level webhooks key. Copy the id from the spec rather than from memory.
The site shows the diagram source as a code block. The generator has no Mermaid support, or the plugin is installed and not enabled. The integrations list names the plugin for each generator, and GitHub needs nothing.
The diagram fails to render and the source looks fine. A message or a note contains the word
end, which the sequence grammar reserves.3 Wrap it in parentheses, quotation marks, or
brackets.
The render is fine on the workstation and unreadable on the site. The site switched to dark mode
and the diagram carries a fixed theme in its frontmatter. Remove it, and let the plugin or the
picture element choose.
When not to do this
Do not diagram a single request and its response. A sequence diagram earns its place when the order of three or more calls is the thing readers get wrong, and a two-line flow is better as a sentence.
Do not put the whole API in one diagram. The checker’s ceiling of twelve messages is not a limit of the tool, it is the point past which nobody reads the picture and nobody updates it. One flow per diagram, one diagram per page.
Do not let the picture be the only description. An agent reading the markdown twin gets the fence’s source text, which reads as code, and a reader with images turned off gets nothing. The paragraph in words is the diagram for both of them.
Do not reach for D2 or PlantUML to get layout control you have not yet needed. Both cost a binary in the docs build. The flows on a reference page are short enough that Mermaid’s layout is fine until it is not, and that is the day to switch.
Related how-tos
- Detect a reference site drifting from its OpenAPI spec
- Publish an OpenAPI document at a stable URL for agents
Last verified
Verified 2026-09-25 against Node 22.22.2, @mermaid-js/mermaid-cli 12.0.0, and yaml 2.9.1.
Every output block is what the command preceding it printed. The SVG files under rendered were
drawn by node render.mjs on this machine with a headless Chromium, and the manifest beside
them records the tool. CI reads the committed files and starts no browser.
Footnotes
-
The W3C’s tutorial on complex images asks for a two-part text alternative. The short description identifies the image and says where the long one is, and the long description carries the essential information. It offers three ways to attach the long one. The third wraps the image and its description together in an HTML5
figure, so the description is text on the page beside the picture. A reader who never sees the image gets the same sentences as one who does. ↩︎ Back to text -
Mermaid’s theming page lists the order in which a setting wins. The diagram’s own frontmatter or
%%{init}%%directive comes first, then what was passed toinitialize(), then the diagram type’s default, then the global default. Since version 12.0.0 a sequence diagram’s own default is theredux-colortheme with theneolook, and the page says that the most specific thing you say wins. A theme written into the diagram is the most specific thing anyone can say. ↩︎ Back to text -
The sequence diagram syntax opens with a warning that the word
endcould break the diagram, due to the way that the Mermaid language is scripted. If unavoidable, it must be enclosed in parentheses, quotation marks, or brackets. A grammar for describing the ends of things has one word it cannot say plainly, and its documentation puts that first. ↩︎ Back to text