Your first unsurf
We will deploy unsurf to your Cloudflare account and use the scout tool to capture every API endpoint on a public website. By the end, you will have a complete OpenAPI spec generated automatically.
Prerequisites
Section titled “Prerequisites”- A Cloudflare account
- Bun installed
- An API key from Cloudflare (for Alchemy deployment)
Deploy
Section titled “Deploy”-
Clone and install
Terminal window git clone https://github.com/acoyfellow/unsurfcd unsurfbun install -
Set your Alchemy password
Terminal window echo 'ALCHEMY_PASSWORD=your-secure-password' > .env -
Generate the database migration
Terminal window bunx drizzle-kit generate -
Deploy to Cloudflare
Terminal window bun run deployAlchemy will create your D1 database, R2 bucket, and Browser Rendering binding automatically. You will see a URL in the output — that is your unsurf instance.
Scout your first site
Section titled “Scout your first site”Now we will capture the API behind JSONPlaceholder, a free public API.
-
Call the scout tool
Terminal window curl -X POST https://your-unsurf-url.workers.dev/tools/scout \-H "Content-Type: application/json" \-d '{"url": "https://jsonplaceholder.typicode.com","task": "discover all API endpoints"}' -
Read the result
The response contains:
siteId— the unique identifier for this siteendpointCount— how many endpoints were capturedpathId— a replayable pathopenApiSpec— a complete OpenAPI 3.1 specification
-
View captured endpoints
Terminal window curl https://your-unsurf-url.workers.dev/sites/{siteId}/endpointsYou will see every endpoint that was called during the scout —
GET /posts,GET /posts/:id,GET /users, and more — each with inferred request and response schemas.
What happened
Section titled “What happened”unsurf launched a headless browser on Cloudflare’s edge, navigated to the site, and captured every XHR/fetch request. For each unique endpoint, it:
- Normalized the URL pattern (
/posts/1→/posts/:id) - Inferred a JSON Schema from the response body
- Saved everything to D1 (via Drizzle) and R2
- Generated an OpenAPI 3.1 spec from all captured endpoints
You now have a typed API definition for a site that may have no public documentation.
Here is how a captured endpoint is represented internally:
import { Schema } from "effect";
const HttpMethod = Schema.Literal("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS");
export class CapturedEndpoint extends Schema.Class<CapturedEndpoint>("CapturedEndpoint")({
id: Schema.String,
siteId: Schema.String,
method: HttpMethod,
pathPattern: Schema.String,
requestSchema: Schema.optionalWith(Schema.Unknown, { as: "Option" }),
responseSchema: Schema.optionalWith(Schema.Unknown, { as: "Option" }),
sampleCount: Schema.Number,
firstSeenAt: Schema.String,
lastSeenAt: Schema.String,
}) {} - How to replay a captured API — use the worker tool to call endpoints directly
- MCP Tools reference — full input/output specification