Environments and variables, without the separate client.
Base URL, variables and default headers per environment, switched from the header bar, kept in the reader’s own browser. The reason people keep a separate API client next to their docs — removed.

REAL SCREENSHOT, SHOWN AT 1:1
One environment per context, color included.

{
"environments": [
{
"name": "Sandbox",
"baseUrl": "https://sandbox.example.com/v1",
"variables": {
"auth.bearerAuth": { "value": "", "sensitive": true },
"accountId": { "value": "acc_123", "sensitive": false }
},
"defaultHeaders": { "X-Client": "api-docs" }
}
]
}A missing variable blocks the send.
{{var}} interpolates in the URL, headers and body. If a variable has no value in the active environment, the send is blocked with a visible signal — the literal {{var}} is never sent to your API, not once. Typing {{ autocompletes from the variables in scope, each with its provenance.Auth follows your securitySchemes.
Every scheme the schema declares is fillable: bearer, basic, apiKey in a header, query or cookie, OAuth2, OpenID Connect. The convention is one variable per scheme — scheme X reads auth.X — so credentials live where variables live, per environment.
OAuth2 runs entirely in the browser, no backend: Authorization Code with PKCE as a full-page redirect with in-browser code exchange, and client credentials. The config carries a clientId at most — never a secret.

The reader's session becomes the credential.
apidoc.registerCredentialsProvider(async () => {
const r = await fetch("/api/docs-token", { credentials: "include" })
return r.ok ? { bearerAuth: (await r.json()).access_token } : null
})One URL sets a teammate up.
https://docs.example.com/#/?setup=eyJ2IjoxLCJlbnYiOnsibmFtZSI6IlN0YWdpbmci…Locked mode for controlled deployments.
environmentsLocked: true freezes the declared environments: no create, no edit, no delete — only the selector remains. Platform teams publish internal API docs with the right sandboxes pre-wired and nothing to misconfigure.
Everything an environment holds also feeds the try-it console, and its sensitive values are redacted from every export and history entry by default.
Local means unencrypted.
See it in the live demo.