---
title: OpenAPI / AsyncAPI
description: Drop in an OpenAPI or AsyncAPI spec and get a native API reference — one real page per operation, in your sidebar and search.
---

Point Blume at an OpenAPI spec and it generates a native API reference: one **real page per operation**, grouped by tag in a tab-scoped sidebar, with schema tables, request/response examples, generated code samples, and an interactive [Try it](#try-it-playground) panel. Because each operation is a genuine Blume page, it gets its own URL, shows up in **site search** and `llms.txt`, and gets an Open Graph image — the same as any hand-written doc. The config below points Blume at the public Petstore spec as an example.

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  spec: "https://petstore3.swagger.io/api/v3/openapi.json",
}
```

That mounts the reference at `/reference` (an overview page) with each operation at `/reference/<tag>/<operation>`. The `spec` is either an `http(s)` URL or a path to a local file in your project. Blume parses it with [Scalar's OpenAPI parser](https://github.com/scalar/scalar) — Swagger 2.0 and OpenAPI 3.0 specs are upgraded to 3.1 automatically.

The reference doesn't add a header tab on its own. To surface it, point a [navigation tab](/docs/content/navigation#tabs) at its route — this also scopes the operations sidebar for the native renderer:

```ts blume.config.ts
navigation: {
  tabs: [{ label: "API", path: "/reference" }],
}
```

:::note
Operations are indexed for search by their **summary and tag**. The rendered schema tables and code samples aren't full-text indexed; search matches an operation's title and section, then links to its own page.
:::

## A local spec

A relative path is resolved from your project root and read at build time. Both JSON and YAML work:

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  spec: "./openapi.yaml",
}
```

## Route

`route` controls where the reference mounts — the overview page and the prefix for every operation route (and the route you point a navigation tab at):

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  route: "/api",   // overview at /api, operations at /api/<tag>/<operation>
  spec: "./openapi.yaml",
}
```

## Code samples and schemas

`codeSamples` picks which languages render per operation (built in: `curl`, `js`, `python`); `expandSchemas` starts nested schema rows expanded rather than collapsed:

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  spec: "./openapi.yaml",
  codeSamples: ["curl", "js"],
  expandSchemas: true,
}
```

## Try it playground

Operation pages rendered natively ship an interactive **Try it** panel by default. Blume generates the form from the operation itself: an input per path, query, and header parameter, a body editor built from the request-body schema, everything prefilled from the spec's examples. A server picker lists the spec's `servers`, with a free-text field for any other base URL, and auth inputs match the operation's [resolved security](#authorization) — bearer token, API key, and basic credentials, with OAuth2 as a token paste field (bring an access token; Blume doesn't run the flow).

The panel and the code samples stay in lockstep: values typed into the form update the generated samples live, so a copied curl command always matches exactly what **Send** would do. And it stays out of the way — the panel is server-rendered collapsed, and its JavaScript loads only when a reader first opens it. Readers who never touch it download none of it.

`playground: false` is the entire off switch:

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  spec: "./openapi.yaml",
  playground: false,
}
```

### Credentials

Credentials typed into the auth inputs stay in memory and vanish on reload. Checking **Remember on this device** persists them in `localStorage`, scoped to the docs origin — they're never sent anywhere except the API being called. Code samples keep showing placeholders (`YOUR_TOKEN` and friends) whatever's typed, unless the reader toggles **Include my values in samples**.

### CORS and the proxy

As with the [Scalar renderer](#the-scalar-renderer), requests go **directly from the browser** to the target API, so the API must allow cross-origin requests from the docs site (`Access-Control-Allow-Origin`). For APIs that can't, set `playground.proxy`: a URL routes requests through a proxy you host, and `true` enables the built-in `/_api-proxy` route — which needs a server build, so it requires [`deployment.output: "server"`](/docs/deployment#server-rendering):

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  spec: "./openapi.yaml",
  playground: {
    proxy: true,   // or a URL of your own
  },
}
```

The built-in proxy only forwards requests to the origins your specs declare in `servers` — including across redirects — so a public docs deployment can't be aimed at other hosts on its network. A **Custom base URL** typed into the panel isn't a documented server: with the proxy enabled, requests to it are refused with a 403.

## Multiple specs

Use `sources` to publish more than one spec. Each source gets its own overview route, operation pages, and header tab. Give each a `label` (used for the tab and to derive its route), or set an explicit `route`:

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  sources: [
    { label: "Public API", spec: "./public.json" },   // → /reference/public-api
    { label: "Admin API", route: "/admin", spec: "./admin.json" },
  ],
}
```

`spec` is shorthand for a single-entry `sources`, so you only reach for `sources` when you have more than one.

### Per-source indexing

Generated pages participate in search, `llms.txt`, and crawler indexing by default. A secondary or overlapping spec can opt out of any surface without hiding its pages or removing it from navigation:

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  sources: [
    { label: "Public API", route: "/api", spec: "./public.json" },
    {
      label: "Platform API",
      route: "/platform",
      spec: "./platform.json",
      includeInSearch: false,
      includeInLlms: false,
      noindex: true,
    },
  ],
}
```

- `includeInSearch: false` keeps the source's overview and operations out of site search.
- `includeInLlms: false` keeps them out of both `llms.txt` files.
- `noindex: true` adds crawler noindex metadata and removes the pages from the sitemap.

With the [Scalar renderer](#the-scalar-renderer), only `noindex` applies — a Scalar-rendered reference already sits outside Blume's search and `llms.txt`, so the two `include*` settings have nothing to act on there.

## Authorization

Operations that declare [security requirements](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) render an **Authorization** section above their parameters, and the generated code samples send a placeholder credential (`Authorization: Bearer YOUR_TOKEN`, an API-key header, or a query key — whatever the scheme calls for). There's nothing to configure: Blume reads `security` from the spec, so the reference always matches what the API actually enforces.

The OpenAPI semantics carry over as written:

- An operation's own `security` overrides the document's root default; `security: []` marks it **public** and renders no Authorization section.
- Multiple requirement entries are alternatives — rendered as "or" groups; every scheme inside one entry is required together. The first alternative feeds the code samples.
- An empty `{}` entry means auth is **optional** for that operation, and the section says so.
- OAuth2 scopes are listed per scheme; scheme `description`s from `components.securitySchemes` render inline.

## The Scalar renderer

The native renderer is the default — operation pages, search integration, and the [Try it playground](#try-it-playground) above are all its work. If you'd rather embed [Scalar](https://scalar.com)'s self-contained API reference UI — its own sidebar, search, theme, and request client on a single route — set `renderer: "scalar"`:

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  renderer: "scalar",
  spec: "./openapi.yaml",
  theme: "purple",   // a Scalar theme name (Scalar renderer only)
}
```

A Scalar-rendered reference is a self-contained embed on its own route — it doesn't weave into Blume's sidebar, search, or `llms.txt`, and Blume's [`playground`](#try-it-playground) config doesn't apply to it. Scalar brings its own request client, which calls your **target API directly from the browser** (the `playground.proxy` route isn't available here), so the API must allow cross-origin requests from the docs site (`Access-Control-Allow-Origin`). `theme` applies to the Scalar renderer only.

### Passing Scalar options

`theme` is a shorthand for the one option most people reach for, but Scalar supports many more. A `scalar` object forwards any [Scalar configuration](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) straight to the embedded reference — Blume doesn't gate the keys, so anything Scalar accepts flows through:

```ts blume.config.ts lineNumbers
openapi: {
  enabled: true,
  renderer: "scalar",
  spec: "./openapi.yaml",
  scalar: {
    localization: { locale: "es" },   // translate Scalar's own UI
    agent: { disabled: true },         // disable the Scalar Agent
    hideTestRequestButton: true,
    orderSchemaPropertiesBy: "preserve",
  },
}
```

Blume's own [`i18n`](/docs/content/i18n) translates the docs chrome, but Scalar has a separate localization system — set `scalar.localization.locale` to translate the embedded reference too. Options in the `scalar` object win over Blume's derived config, so anything set here (including `theme`, `customCss`, or the spec `content`/`url`) overrides Blume's defaults. The same `scalar` block works on the `asyncapi` reference.

## AsyncAPI

Event-driven APIs use a sibling `asyncapi` block with the same shape — and the same native renderer. Each `send`/`receive` operation becomes a real page with message payload and header schema tables, channel parameters, protocol bindings, an Authorization section derived from the spec's `securitySchemes` (server-level and operation-level, alternatives as "or" groups), and a [Try it](#try-it-for-events) message composer. Only the default route differs (`/events`):

```ts blume.config.ts lineNumbers
asyncapi: {
  enabled: true,
  spec: "./asyncapi.yaml",
}
```

AsyncAPI **2.x specs are normalized to 3.x automatically** with the official AsyncAPI converter, so `publish`/`subscribe` channels map onto `send`/`receive` operation pages with stable URLs — later upgrading the spec file itself through the converter moves nothing. Operations group by tag; untagged operations group under their channel address.

Code samples are **protocol-aware**, keyed off the operation's binding (or its servers' protocol): `wscat` and a browser `WebSocket` snippet for WebSockets, `kcat` for Kafka, `mosquitto_pub`/`mosquitto_sub` for MQTT. `codeSamples` filters that set, the same way it picks languages on the `openapi` block; a protocol without a supported tool renders the message payload example alone rather than a fabricated client.

Everything documented above carries over, [`playground`](#try-it-for-events) included: `route`, `sources` with `label`/`route`, `expandSchemas`, the [per-source indexing](#per-source-indexing) flags, and search indexing by operation summary and tag.

Setting `renderer: "scalar"` opts back into the embedded Scalar SPA, where — as with OpenAPI — only `noindex` applies. Scalar has no AsyncAPI playground of its own; its embed auto-detects the document type and renders channels, operations, messages, and a Models section, so that swap trades the composer away.

### Try it for events

Operation pages rendered natively ship a **Try it** panel here too, on the same terms as the [OpenAPI panel](#try-it-playground): server-rendered collapsed, with its JavaScript loaded only when a reader first opens it.

Whatever the protocol, the panel opens with a payload editor prefilled from the message's `examples` — or, when the message declares none, from a value sampled out of the payload schema — validated against the message payload schema as you type. Under it sit an input per channel parameter and a server picker fed by the channel's `servers`, with a free-text field for any other URL. The protocol-aware code samples stay in lockstep with the form exactly as curl, js, and python do on an HTTP operation: the channel address template is filled in with the parameter values you type, so a copied `wscat`, `WebSocket`, `kcat`, or `mosquitto_pub` snippet matches what the form says.

Live connect is WebSocket-only. On a `ws` or `wss` binding the panel connects to the resolved channel URL, shows the connection state, and logs every frame with a timestamp. AsyncAPI 3 states an action from the API's side, and the panel follows it: a `receive` operation is one the API receives from you, so it gets a **Send** button that publishes the composed payload; a `send` operation only streams messages at you, so it connects and logs. There's no reconnect logic — once a socket closes, it stays closed until you connect again. Kafka, MQTT, AMQP, and every other protocol get the composer and the copyable CLI samples, and the panel says as much on the page: Blume doesn't fake broker connectivity from a browser tab.

`asyncapi.playground` mirrors `openapi.playground` — on by default with the native renderer, and `false` is the entire off switch:

```ts blume.config.ts lineNumbers
asyncapi: {
  enabled: true,
  spec: "./asyncapi.yaml",
  playground: false,
}
```

:::note
`playground.proxy` is OpenAPI-only. It forwards HTTP requests, and a WebSocket connect goes straight from the browser to the server named in the URL, so there's nothing for a proxy to sit in front of.
:::

The event composer collects no broker credentials. Each operation page's **Authorization** section documents what the broker expects, and a WebSocket connect carries only what's already in the URL. Nothing is persisted for event operations.
