Skip to content

SDKs

TypeScript SDK

The official Vermin client for TypeScript. This page is the SDK's README, rendered.

@vermin/sdknpm install @vermin/sdk

Official TypeScript SDK for Vermin, the web-data API for AI: scrape, crawl, map, search, extract and brand.

  • Zero runtime dependencies. It uses the global fetch, so it runs on Node 18+, Bun, Deno, Cloudflare Workers and browsers.
  • ESM and CommonJS builds, with full types generated from the OpenAPI contract.
  • Automatic retries with jittered backoff on 429, 5xx and network errors. It honours retry_after_ms / Retry-After.
  • Auto-generated Idempotency-Key on scrape and crawl.start, reused across retries.
  • creditsUsed and requestId on every result, plus a typed VerminError.
bash
npm install @vermin/sdk

Quick start#

ts
import { Vermin } from "@vermin/sdk";

const vermin = new Vermin({ apiKey: process.env.VERMIN_API_KEY });

const page = await vermin.scrape({ url: "https://example.com", formats: ["markdown", "metadata"] });
console.log(page.data.markdown, page.data.metadata?.title);
console.log(`cost: ${page.creditsUsed} credits (request ${page.requestId})`);

Options#

ts
new Vermin({
  apiKey: "vmn_live_…",            // default: VERMIN_API_KEY env var
  baseUrl: "https://api.vermin.dev", // default: VERMIN_API_URL env var, then https://api.vermin.dev
  timeoutMs: 60_000,               // per attempt
  maxRetries: 2,                   // retries on 429 / 5xx / network errors
  fetch: customFetch,              // optional, e.g. for proxies or tests
  headers: { "X-Team": "growth" }, // extra headers on every request
});

Every method also takes an optional last argument of RequestOptions: { signal, timeoutMs, maxRetries, idempotencyKey, headers }.

API#

scrape(params)#

ts
const { data } = await vermin.scrape({
  url: "https://news.ycombinator.com",
  formats: ["markdown", "links", "chunks"],
  onlyMainContent: true,
  tier: "auto",          // fetch → render → premium (if allowPremium)
  chunkSize: 1200,
  actions: [{ type: "click", selector: "#more" }, { type: "wait", ms: 1000 }],
});
data.chunks?.forEach((c) => console.log(c.headingPath, c.tokens));
data.quality?.issues; // e.g. ["js_required"]

crawl#

ts
const job = await vermin.crawl.start({ url: "https://docs.example.com", limit: 200, maxDepth: 3 });

// Poll until the crawl finishes and collect every page (follows `next` cursors):
const result = await vermin.crawl.wait(job.id, { pollMs: 2000, onStatus: (s) => console.log(s.completed, "/", s.total) });
console.log(result.status, result.data?.length, result.creditsUsed);

// …or stream pages as they arrive over server-sent events:
for await (const event of vermin.crawl.stream(job.id)) {
  if (event.type === "page") console.log(event.data.url);
  if (event.type === "status") console.log(event.data.completed);
  if (event.type === "done") console.log("finished:", event.data.status);
}

// Lower level:
const status = await vermin.crawl.get(job.id, { limit: 50 });
if (status.next) await vermin.crawl.get(job.id, { cursor: status.next });
for await (const doc of vermin.crawl.documents(job.id)) console.log(doc.url);
await vermin.crawl.cancel(job.id);

crawl.wait resolves with the final status even if the crawl failed or was cancelled, so check result.status. Pass timeoutMs to give up after a deadline. A failed crawl carries result.error ({ code, message }). crawl.stream reconnects with Last-Event-ID if the connection drops before the done event (waiting the server's retry: delay, else backoff), up to maxReconnects consecutive times (default maxRetries; the budget resets after each page/status event), then throws a connection_error. Resume with vermin.crawl.stream(id, { lastEventId: "41" }). Break out of the loop, or pass a signal, to stop it.

map(params)#

ts
const { links } = await vermin.map({ url: "https://example.com", search: "pricing", limit: 500 });
// Only some paths (globs)
await vermin.map({ url: "https://example.com", includePaths: ["/blog/*"], excludePaths: ["/blog/tag/*"] });

search(params)#

ts
const { results } = await vermin.search({
  query: "best vector databases 2026",
  limit: 5,
  timeRange: "month",
  scrapeOptions: { formats: ["markdown"] }, // optional: scrape each result (billed as scrapes)
});
results.forEach((r) => console.log(r.position, r.title, r.document?.markdown?.length));
// r.date and r.source are set when the provider reports them (e.g. news results)

extract<T>(params)#

Pass a JSON Schema, or a zod ≥ 4.2 schema (or any Standard JSON Schema). A zod schema is converted to JSON Schema for the request. The response is then validated and typed with the schema.

ts
import { z } from "zod";

const Product = z.object({ name: z.string(), price: z.number(), inStock: z.boolean() });

const { data } = await vermin.extract({
  urls: ["https://shop.example.com/p/123"],
  schema: Product,
  prompt: "Extract the product",
});
data.price; // number (typed from the schema)

// Plain JSON Schema passes through untouched; type the result yourself:
const res = await vermin.extract<{ title: string }>({
  urls: ["https://example.com"],
  schema: { type: "object", properties: { title: { type: "string" } }, required: ["title"] },
});

If the data doesn't match a zod schema, extract throws a VerminError with code invalid_response. Pass validate: false to skip the check. zod 3 has no built-in converter, so pass zodToJsonSchema(schema) instead.

brand(domain)#

ts
const { data: brand, cached, stale } = await vermin.brand("stripe.com");
// or: vermin.brand({ domain: "https://stripe.com/pricing", maxAge: 0 }) — 0 forces a fresh extraction
brand.logo?.url;       // Vermin-hosted copy; logo.sourceUrl is where it was found
brand.colors?.find((c) => c.role === "primary")?.hex; // each colour has textColor + WCAG contrast
// Every image found, tagged with type ("logo" | "icon" | "wordmark") and background ("light" | "dark" | "any")
const forDarkUi = brand.logos?.filter((l) => l.background === "dark" && l.type !== "icon");
// stale: a cached result older than maxAge, served because a fresh lookup failed

Errors#

Every failure throws a VerminError:

ts
import { VerminError } from "@vermin/sdk";

try {
  await vermin.scrape({ url: "https://example.com" });
} catch (err) {
  if (err instanceof VerminError) {
    err.code;         // "rate_limited" | "insufficient_credits" | "blocked_by_site" | … | "connection_error" | "aborted"
    err.status;       // HTTP status (0 if no response)
    err.requestId;    // quote this to support
    err.retryAfterMs; // server hint, when present
    err.retryable;    // whether retrying could help
  }
}

Retries apply to 429, 5xx (including capacity_exceeded), timeouts and connection errors. By default the SDK makes up to 3 attempts, with jittered exponential backoff from 0.5 s up to 8 s. When the server sends a retry_after_ms hint (or a Retry-After header, seconds or HTTP date), the SDK waits exactly that long. It never retries invalid_request, unauthorized, forbidden, not_found, insufficient_credits, spend_cap_reached, blocked_url, not_implemented, not_configured or extract_failed, whatever the status. Failed requests cost 0 credits.

Webhooks#

Crawl webhooks carry a Vermin-Signature: t=<unix seconds>,v1=<hex> header, where v1 is the HMAC-SHA256 of "<t>.<raw body>" keyed with your signing secret (Dashboard → Webhooks). Verify it against the raw request body before trusting the payload. The helper compares in constant time, accepts any of several v1= values (secret rotation) and rejects timestamps more than 300 s from now (tolerance; 0 disables the check).

ts
import { verifyWebhook, VerminError } from "@vermin/sdk";

export async function POST(request: Request) {
  try {
    const event = await verifyWebhook(await request.text(), request.headers.get("Vermin-Signature"), process.env.VERMIN_WEBHOOK_SECRET!);
    if (event.type === "crawl.page") console.log(event.data.url);
    return new Response(null, { status: 204 });
  } catch (e) {
    if (e instanceof VerminError && e.code === "invalid_signature") return new Response(null, { status: 400 });
    throw e;
  }
}

isValidWebhook() resolves to a boolean instead, and signWebhook() builds a header for your tests.

Types#

All request and response types come from the spec, including Document, ScrapeParams, CrawlStatus, Brand and CrawlEvent. The raw generated components / paths / operations types are exported too:

ts
import type { Document, ScrapeParams, components } from "@vermin/sdk";