Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ just check
This validates the Mint configuration and OpenAPI definitions, checks links and redirects, and runs
the documentation accessibility checks.

## V3 API reference source

The v3 API reference uses the checked-in `v3/openapi.json`, not a live Stainless URL.
The former source,
`https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml`,
returned HTTP 404 and prevented Mint validation from completing.

The snapshot was recovered on September 15, 2026 from the OpenAPI YAML blocks in the
published `https://docs.stagehand.dev/v3/api-reference/{language}/{page}.md` pages.
All 32 pages (eight endpoints each for Python, Java, Go, and Ruby) were compared.
Their common metadata, operations, and shared components agreed. The snapshot
combines those fragments without changing descriptions, schemas, authentication,
servers, or code samples: eight endpoints, 67 schemas, and two security schemes.
The four language sections retain their existing navigation and generated page paths.

Treat this as the versioned v3 documentation source. Future spec updates must be
reviewed as content changes, including endpoint titles/URLs, request and response
schemas, authentication, and code samples. Do not replace it with a different
server-generated spec just to make validation pass. Run the docs checks and
`pnpm --filter @browserbasehq/stagehand-docs test:unit` after updates.

## Publishing

Documentation is deployed through the Mintlify GitHub integration after changes reach the
Expand Down
10 changes: 5 additions & 5 deletions packages/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
}
},
"api": {
"openapi": "https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml"
"openapi": "v3/openapi.json"
},
"navigation": {
"versions": [
Expand Down Expand Up @@ -228,7 +228,7 @@
{
"group": "API Reference",
"openapi": {
"source": "https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml",
"source": "v3/openapi.json",
"directory": "v3/api-reference/python"
},
"pages": [
Expand Down Expand Up @@ -256,7 +256,7 @@
{
"group": "API Reference",
"openapi": {
"source": "https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml",
"source": "v3/openapi.json",
"directory": "v3/api-reference/java"
},
"pages": [
Expand Down Expand Up @@ -284,7 +284,7 @@
{
"group": "API Reference",
"openapi": {
"source": "https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml",
"source": "v3/openapi.json",
"directory": "v3/api-reference/go"
},
"pages": [
Expand Down Expand Up @@ -312,7 +312,7 @@
{
"group": "API Reference",
"openapi": {
"source": "https://app.stainless.com/api/spec/documented/stagehand/openapi.documented.yml",
"source": "v3/openapi.json",
"directory": "v3/api-reference/ruby"
},
"pages": [
Expand Down
100 changes: 100 additions & 0 deletions packages/docs/tests/v3-openapi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";

const config = JSON.parse(readFileSync(new URL("../docs.json", import.meta.url), "utf8"));
const spec = JSON.parse(readFileSync(new URL("../v3/openapi.json", import.meta.url), "utf8"));
const endpoints = [
["POST /v1/sessions/start", "Start a new browser session"],
["POST /v1/sessions/{id}/navigate", "Navigate to a URL"],
["POST /v1/sessions/{id}/act", "Perform an action"],
["POST /v1/sessions/{id}/observe", "Observe available actions"],
["POST /v1/sessions/{id}/extract", "Extract data from the page"],
["POST /v1/sessions/{id}/agentExecute", "Execute an AI agent"],
["POST /v1/sessions/{id}/end", "End a browser session"],
["GET /v1/sessions/{id}/replay", "Replay session metrics"],
];

function findReferences(value: unknown): string[] {
if (value === null || typeof value !== "object") return [];
return Object.entries(value).flatMap(([key, entry]) =>
key === "$ref" && typeof entry === "string" ? [entry] : findReferences(entry),
);
}

describe("v3 API reference", () => {
it("uses the local spec for all four existing API navigation sections", () => {
expect(config.api.openapi).toBe("v3/openapi.json");
const version = config.navigation.versions.find(
(entry: { version: string }) => entry.version === "v3",
);
const apiSections = version.dropdowns.filter(
(entry: { dropdown: string }) => entry.dropdown !== "TypeScript",
);
expect(apiSections.map((entry: { dropdown: string }) => entry.dropdown)).toEqual([
"Python",
"Java",
"Go",
"Ruby",
]);
for (const section of apiSections) {
const reference = section.groups.find(
(group: { group: string }) => group.group === "API Reference",
);
expect(reference.openapi).toEqual({
source: "v3/openapi.json",
directory: `v3/api-reference/${section.dropdown.toLowerCase()}`,
});
expect(reference.pages).toEqual(endpoints.map(([endpoint]) => endpoint));
}
});

it.each(endpoints)("preserves the title and code sample languages for %s", (endpoint, title) => {
const [method, path] = endpoint.split(" ");
const operation = spec.paths[path][method.toLowerCase()];
expect(operation.summary).toBe(title);
expect(operation.description).toBeTruthy();
expect(operation.responses["200"]).toBeDefined();
expect(operation["x-codeSamples"].map((sample: { lang: string }) => sample.lang)).toEqual([
"JavaScript",
"Python",
"Go",
"Java",
"Kotlin",
"Ruby",
"PHP",
"C#",
]);
for (const sample of operation["x-codeSamples"]) {
expect(sample.source.trim()).not.toBe("");
}
});

it("resolves every schema reference without fetching an external document", () => {
const references = findReferences(spec);
expect(references.length).toBeGreaterThan(0);
for (const reference of references) {
expect(reference).toMatch(/^#\//);
const resolved = reference
.slice(2)
.split("/")
.map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))
.reduce((value, key) => value?.[key], spec);
expect(resolved, reference).toBeDefined();
}
});

it("preserves the API server and authentication schemes", () => {
expect(spec.servers).toEqual([{ url: "https://api.stagehand.browserbase.com" }]);
expect(spec.security).toEqual([{ BrowserbaseApiKey: [], BrowserbaseProjectId: [] }]);
expect(spec.components.securitySchemes.BrowserbaseApiKey).toMatchObject({
type: "apiKey",
in: "header",
name: "x-bb-api-key",
});
expect(spec.components.securitySchemes.BrowserbaseProjectId).toMatchObject({
type: "apiKey",
in: "header",
name: "x-bb-project-id",
});
});
});
Loading
Loading