Endpoints
Scrape
One URL in, clean markdown, HTML, screenshots, chunks, metadata or brand out.
Scrape fetches a single URL and returns the formats you ask for. With the default tier: "auto" it tries a plain fetch, runs a quality check, and only escalates to a headless browser when the page actually needs one. You pay for the tier that worked: 1 credit (rendering and screenshots included), +1 when you use browser actions.
Example#
curl -X POST "https://api.vermin.dev/v1/scrape" \
-H "Authorization: Bearer $VERMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/blog/post",
"formats": ["markdown", "chunks", "screenshot"],
"chunkSize": 800
}'Request#
urlstring (uri)required- Basic metadata (title, description, language, canonical, status) is always returned;
metadataadds parsed JSON-LD. onlyMainContentbooleandefault truesiteMarkdownbooleandefault falseUse the site's own markdown when it serves one (Accept: text/markdown, e.g. many docs sites) and it passes the quality check; otherwise the HTML is extracted as usual. About 2x faster where available, but usually with less metadata (no language or canonical URL). Ignored with thehtml,rawHtmlorlinksformats.includeTagsstring[]excludeTagsstring[]tier"auto" | "fetch" | "render" | "premium"default "auto"auto escalates fetch → render when the page needs it.premiumis not available yet.allowPremiumbooleandefault falseNot available yet; requests with it set are refused.waitForinteger≤ 30000timeoutintegerdefault 300001000–60000maxAgeintegerdefault 172800000Serve a cached copy younger than this (ms). Default 2 days; 0 forces a fresh scrape. Cache hits are served from the nearest data center.headersRecord<string, string>screenshotOptionsScreenshotOptionsHow the screenshot formats capture the page. Needs thescreenshotorscreenshotFullformat. Page actions run in the same window first. If the capture fails the scrape still succeeds without a screenshot, andquality.escalationssays why:screenshot:element_not_found,screenshot:invalid_selector,screenshot:element_not_visible,screenshot:timeoutorscreenshot:render_failed.locationobjectShowHide 2 fields
countrystringlanguagesstring[]
chunkSizeintegerdefault 1500200–8000ignoreRobotsbooleandefault falseSkip the robots.txt check. Only set this when you have permission to fetch the page.
Formats#
| Format | You get |
|---|---|
markdown | Main content as GitHub-flavoured markdown (default). |
html | Cleaned HTML of the main content. |
rawHtml | The page's HTML exactly as served or rendered. |
text | Plain text, no markup. |
links | Every absolute link on the page. |
screenshot / screenshotFull | Signed image URL of the viewport or the full page (expires in 24h). Forces the render tier, at no extra cost. Choose the window size or capture one element with screenshotOptions. |
chunks | Markdown split on headings into ~chunkSize-token pieces, each with its headingPath. Ready for your vector store. |
metadata | Adds the page's structured data (JSON-LD) to metadata. Title, description, language, canonical URL, status code, author, dates and OG image come back on every scrape, whatever the formats. |
brand | The page's brand identity, extracted in the same pass. |
Actions#
Need to click a cookie banner, scroll a feed or type into a search box first? Pass up to 20 actions; they run in order in a real browser before the page is captured.
{
"url": "https://example.com/feed",
"actions": [
{ "type": "click", "selector": "#accept-cookies" },
{ "type": "scroll" },
{ "type": "wait", "ms": 1500 }
]
}Screenshot size and element capture#
Screenshots use a 1280 × 800 desktop window by default. Set screenshotOptions to choose the window, the pixel density and whether to emulate a phone, or to capture just one element. Page actions run in the same window first, so you can open a menu or switch a tab before the capture.
{
"url": "https://vermin.dev",
"formats": ["screenshot"],
"screenshotOptions": {
"viewport": { "width": 390, "height": 844, "deviceScaleFactor": 3, "mobile": true },
"selector": "header"
}
}| Option | What it does |
|---|---|
viewport.width, viewport.height | Window size in CSS pixels, 240 to 3840 each. |
viewport.deviceScaleFactor | Pixel density, 1 to 3 (default 1). At 3 a 390 × 844 window gives a 1170 × 2532 image. Width and height × density must each be at most 7680. |
viewport.mobile | Phone emulation: the page's meta viewport is honoured and touch is on, so responsive sites show their mobile layout. The site still sees our normal browser user agent. |
selector | Capture only the first element matching this CSS selector, whole, even where it runs past the window (up to 16,384 device pixels). Fixed and sticky elements elsewhere, such as headers and chat buttons, are hidden so they don't cover it. Waits up to 5 seconds for the element to appear. Use it with screenshot, not screenshotFull. |
screenshotOptions needs the screenshot or screenshotFull format. If the capture fails, you still get the page and its other formats, without a screenshot, and quality.escalations says why: screenshot:element_not_found, screenshot:invalid_selector, screenshot:element_not_visible or screenshot:timeout.
Site markdown#
Many docs sites (Next.js, Docker, GitHub Docs, Microsoft Learn, Cloudflare and others) publish their own markdown for each page. Set siteMarkdown: true and we ask for it (Accept: text/markdown) and return it as-is when it passes the quality check, with method: "markdown". It's about twice as fast as extracting the HTML, and sites that don't have it still send HTML in the same request, so it never costs extra credits.
It's off by default because the site's version usually carries less metadata: title and description come from its front matter or first heading, and the language and canonical URL are often missing. On our benchmark its content also scored slightly below our own extraction. It's ignored with the html, rawHtml and links formats, which need the page itself.
Caching#
Public pages are cached for two days by default and a cache hit costs 1 credit, served from the nearest data center in milliseconds (cached: true). Set maxAge (ms) to decide how fresh is fresh enough, or maxAge: 0 to always scrape live. Requests with custom headers, actions or screenshot formats are never cached or served from cache, and neither are pages the site marks no-store or private.
Response#
{
"success": true,
"data": {
"url": "https://example.com/blog/post",
"finalUrl": "https://example.com/blog/post",
"markdown": "# Shipping at the edge\n\n...",
"chunks": [
{ "index": 0, "text": "# Shipping at the edge ...", "headingPath": ["Shipping at the edge"], "tokens": 781 }
],
"screenshot": "https://api.vermin.dev/artifacts/screenshots/7f3c….png?sig=…",
"metadata": { "title": "Shipping at the edge", "language": "en", "statusCode": 200 },
"tier": "render",
"cached": false,
"quality": {
"score": 0.93,
"issues": ["js_required"],
"escalations": ["fetch→render: js_required"],
"reason": "The fetched HTML was an empty app shell; the rendered page had the article."
}
},
"credits_used": 1,
"latency_ms": 1240,
"request_id": "req_01J9V3P0ZC"
}The quality report#
quality is Vermin's answer to "why is my markdown empty?":
score: 0–1 confidence that the content is complete and is the page's real main content.issues: what we noticed.thin_content,js_required,bot_wall,cookie_wall,login_wall,soft_404,truncated,non_html.escalations: what the pipeline did about it, e.g.fetch→render: js_required.reason: the same story in words.
robots.txt#
Scrape checks the site's robots.txt (for verminbot, then *) before fetching, and refuses disallowed URLs with blocked_url at 0 credits. The verdict is cached per origin for an hour. If robots.txt can't be reached in time, the scrape goes ahead; a 5xx or 429 from robots.txt counts as "disallow everything". Pass ignoreRobots: true only when you have permission to fetch the page.
Errors#
Common ones: blocked_url (private/local address or robots.txt), blocked_by_site, timeout, capacity_exceeded, and not_configured if you ask for a tier this deployment doesn't have. All free. See Errors.