Skillforge Field notes on shipping with AI tools

Web Page to Markdown API: 6 Options Compared (2026)

extract-apiapis

We earn commissions when you shop through the links below, at no extra cost to you. We only link products we would use ourselves.

If you are feeding web pages to a model, you are doing one of two jobs, and they have different prices and different failure modes.

Job one: page to markdown. You want the readable content, without the nav, the cookie banner, and 300KB of inline script. Cheap, and every provider does it. Expect to pay between $1 and $5 per thousand pages.

Job two: page to typed fields. You want {title, price, author, date} as JSON. Costs three to five times more everywhere, because most providers run a model over the page to produce it, and that model can be wrong in a way markdown conversion cannot.

The short answer: if you need markdown at volume and are happy on a monthly plan, Firecrawl's Standard tier and ScrapingBee are the cheapest per page. If your volume is low or spiky and you do not want a monthly floor, prepaid units (ours) or Tavily's pay-as-you-go cost less in total even though they cost more per page. If you need typed fields and cannot tolerate a fabricated value, read the section on nulls below before you pick anything.

Every price here was checked against the provider's own pricing page on August 16, 2026.

What each option costs per 1,000 pages

OptionModelMarkdown / 1,000Typed JSON / 1,000
Firecrawl Standard$83/mo billed yearly, 100,000 credits$0.83$4.15
ScrapingBee Freelance$49/mo, 250,000 credits$0.98not published
Tavily pay-as-you-go$0.008 per credit, no plan$1.60 (basic)n/a
Firecrawl Hobby$16/mo billed yearly, 5,000 credits$3.20$16.00
Skillforge Render API (ours)$50 prepaid, 24,000 units$4.17$16.67
Skillforge Render API (ours)$10 prepaid, 4,000 units$5.00$20.00
Jina Readerfree without a key; paid billed per output tokenvaries with page sizen/a

How those numbers are derived, so you can check them:

  • Firecrawl charges 1 credit per scraped page, and its docs state that "JSON mode costs 4 additional credits per page", so a typed extraction is 5 credits. The pricing page shows yearly-billed rates ($16/month Hobby with 5,000 credits, $83/month Standard with 100,000), each with a "Save" badge, so monthly billing is higher than the figures above.
  • ScrapingBee bills 5 credits for the default JavaScript-rendered request, and its HTML API can return the page as markdown directly via return_page_markdown. Freelance is $49 for 250,000 credits, so 50,000 pages. Its AI extraction parameters (ai_query, ai_extract_rules) exist but the documentation does not publish their credit cost, so we left that cell blank rather than guess.
  • Tavily prices Extract at 1 credit per 5 URLs in basic mode and 2 credits per 5 URLs in advanced, with pay-as-you-go at $0.008 per credit and 1,000 credits a month on the free tier. Extract returns page content, not a typed object against your schema, so the JSON column does not apply.
  • Ours sells prepaid units with no plan: $10 for 4,000 units, $25 for 11,000, $50 for 24,000. Markdown extraction is 2 units, structured extraction is 8. Units do not expire.
  • Jina Reader is free with no key at all (20 requests per minute), 500 RPM with a key. Paid usage is billed on the token count of the output, so a long page costs more than a short one and there is no flat per-page number to put in a table.

We are not the cheapest per page and there is no reading of that table where we are. What prepaid units buy is the absence of a floor: $10 of units is 2,000 markdown extractions that sit there until you use them, and $50 is 12,000.

A subscription only wins once you actually consume the credits it includes. Firecrawl Hobby at the yearly rate is $192 a year and includes 60,000 markdown scrapes over those twelve months. Buying 60,000 markdown extractions from us at the best prepaid rate costs about $250, so at full utilization Hobby is cheaper. Buy only 6,000 pages a year and Hobby is still $192 while ours is $25. The breakeven sits near 46,000 markdown pages a year; below that, prepaid costs less in total, above it the plan does. Against Firecrawl Standard, with its much lower per-credit rate, the same breakeven lands somewhere near 240,000 pages a year. Work out your real annual page count first, then read the table.

The part nobody benchmarks: how often the field is even there

Typed extraction has an obvious question behind it that pricing tables skip. When you ask a page for author, is the answer on the page at all?

We ran twelve real pages through our extraction transform and asked for six fields: title, description, image, author, date, price. The sample is a convenience sample (docs, blogs, marketing, commerce, reference, plus Hacker News as a deliberately bare case), not a random sample of the web, so read it as a shape rather than a statistic.

FieldPages where it resolved
title12 / 12
description8 / 12
image6 / 12
date4 / 12
author2 / 12
price0 / 12

Then we ablated the page: strip the JSON-LD blocks and re-run, strip the <meta> tags and re-run, strip the microdata attributes and re-run. Whichever removal makes a field go null is the layer that was actually supplying it. Across all 72 field-page pairs:

Source layerFields it supplied
nothing (null)40 / 72
<meta> / OpenGraph15 / 72
<title> or <time datetime> fallback12 / 72
JSON-LD4 / 72
microdata1 / 72

Two things fall out of that.

OpenGraph is the web's real metadata standard. JSON-LD gets all the SEO attention and supplied four fields out of seventy-two. The <meta> tags that exist so link previews look right in Slack supplied nearly four times as many. If you are writing your own extractor and have time for exactly one layer, write the OpenGraph one.

More than half of what you ask for is not on the page. Fifty-six percent of our field-page pairs had no answer anywhere in the markup. That is the number that should drive your architecture, and it is where deterministic and model-based extraction split apart.

Nulls, or: what does your extractor do when the answer is absent

Ask an LLM-backed extractor for the author of a page with no author, and you generally get an author. Sometimes it is the site name. Sometimes it is a person named in a quote halfway down. It is rarely flagged as invented, because the model was asked for a value and producing values is what it does.

Our structured mode has no model in it. Five levels of precedence (JSON-LD, then OpenGraph and <meta>, then microdata, then a small set of well-known-field fallbacks, then null), all pure string scanning, and a field that cannot be resolved comes back null. The response also tells you filled and total, so you can route on coverage instead of eyeballing it.

We take that rule seriously enough that building this article broke a feature. The probe above caught our own price handling inventing "$1" for nodejs.org/about and for the Cloudflare blog, neither of which sells anything: an old fallback was scanning body text for the first currency-looking token. A price in a sentence is not a price, so the fallback is gone as of today. That is why the price row above reads 0 / 12 and not 3 / 12. Price now resolves from JSON-LD, OpenGraph, or microdata or it returns null.

The honest trade: deterministic extraction will hand you null on pages where an LLM would have gotten it right by reading the prose. If you are populating a catalog and a human checks it, the model is more useful. If a null is cheaper for you than a plausible wrong answer, which is usually true for anything that auto-publishes, feeds a pricing decision, or gets stored as fact, take the null.

Does the headless browser actually help?

Every provider in that table renders in a real browser, and that is most of what you are paying for. So we checked what rendering buys for metadata specifically: same pages, once from raw pre-JavaScript HTML fetched with fetch(), once through the live Chromium-backed endpoint.

For metadata, it bought nothing. GitHub's repo page filled the same 4 of 6 fields either way. example.com filled the same 1 of 6. That is not a fluke: OpenGraph tags exist to be read by crawlers that do not run JavaScript, so sites emit them server-side even when everything else on the page is client-rendered.

Rendering still matters, just for the other job. A single-page app serves an empty <div id="root"> in its raw HTML, so markdown extraction from unrendered HTML gets you nothing at all while the metadata extraction from the same fetch looks fine. If your pipeline only needs title and image for link previews, a plain fetch() plus an OpenGraph parser is free and will cover most of your traffic. Pay for a browser when you need the body text.

One more measured detail: the Cloudflare blog homepage returned 502 render_failed on our endpoint during this run, and the response says "credits were not consumed". The balance confirmed it: 18 units for two structured extractions and one markdown extraction, nothing for the failure. Heavy pages do time out, on every provider in this category. What matters is whether you are billed for them.

Real requests

Markdown mode. The response is text/markdown; charset=utf-8, not JSON, so pipe it straight into your prompt:

curl -sS https://render.skillforge99.com/v1/extract \
  -H "Authorization: Bearer sfr_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com"}'
Example Domain

# Example Domain

This domain is for use in documentation examples without needing permission. Avoid use in operations.

[Learn more](https://iana.org/domains/example)

Structured mode. Pass a flat JSON-Schema-shaped object; you get back your fields plus the coverage counters:

curl -sS https://render.skillforge99.com/v1/extract \
  -H "Authorization: Bearer sfr_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://github.com/cloudflare/workers-sdk","mode":"structured",
       "schema":{"properties":{"title":{"type":"string"},"description":{"type":"string"},
       "image":{"type":"string"},"author":{"type":"string"},"date":{"type":"string"},
       "price":{"type":"string"}}}}'
{
 "url": "https://github.com/cloudflare/workers-sdk",
 "data": {
  "title": "GitHub - cloudflare/workers-sdk: ⛅️ Home to Wrangler, the CLI for Cloudflare Workers®",
  "description": "⛅️ Home to Wrangler, the CLI for Cloudflare Workers® - cloudflare/workers-sdk",
  "image": "https://opengraph.githubassets.com/bf7dc942cd363d8e19d90b4a99149430f3f6ea3122327713c739e59a324dbb23/cloudflare/workers-sdk",
  "author": "cloudflare",
  "date": null,
  "price": null
 },
 "filled": 4,
 "total": 6
}

date and price are null because that page states neither in markup, and nothing guesses on your behalf. author came from a microdata itemprop, the one field in our whole sample that did.

The schema is deliberately limited: a flat properties map, up to 24 fields, up to 8KB, each typed string, number, boolean, or array. No nested objects, because there is no model in the loop to reason about nesting.

Building it yourself

The self-hosted version of job one is genuinely small: Playwright for the render, Readability for the article extraction, Turndown for the markdown. On a DigitalOcean droplet at $4 to $6 a month that beats every hosted price in the table above once you are past a few thousand pages a month, and we ran that same math in more detail for screenshots in screenshot API or self-hosted droplet.

What you take on: Chromium patching, memory watching (long-running browser processes leak, and one stuck tab can take the box down), a queue once one instance cannot keep up, and URL filtering so your renderer does not become an open proxy into your private network. If you are already on Cloudflare, Browser Rendering in a Worker skips the box entirely and you write the Puppeteer code yourself.

Job two, the typed-fields half, is smaller than it looks if you take the deterministic route: our whole extraction transform is about 400 lines with no dependencies. It is the LLM-backed version that is hard to build well, and that is the one worth buying.

FAQ

What is the difference between a scraping API and a web page to markdown API? Scraping APIs (ScrapingBee, Zyte, Bright Data) sell you proxy rotation and anti-bot handling, and hand back HTML. Markdown APIs sell you the conversion step and hand back something a model can read. The categories have converged: most scraping APIs now have a markdown flag, and most markdown APIs now handle JavaScript. Pick on whether your target sites fight back. If they do, you want the proxy-heavy vendors and the extra credits they charge for stealth mode.

Can I use these to build a RAG pipeline over a set of sites? For the fetch-and-convert step, yes, that is what they are for. Budget on pages per month rather than per request, and check whether the provider charges for failed renders, because a crawl of real sites will have failures. Also check whether you can send a batch: per-URL endpoints plus a slow site means your pipeline is bounded by latency, not price.

Should I extract structured fields with an LLM instead? If a wrong-but-plausible value is expensive for you, no. Run the deterministic pass first, look at filled versus total, and send only the pages with gaps to a model, with the prompt telling it to return null when the page does not say. You pay model tokens on a fraction of your pages instead of all of them, and the deterministic answers are auditable back to a specific tag.

Do prepaid units expire? Ours do not. That is the main reason the per-page rate is higher: there is no monthly plan subsidizing it and no expiry date forcing you to use it.

How do I convert HTML I already have, without a fetch? Our extract endpoint takes url only and rejects html with 400 bad_params, because the whole point of the endpoint is the browser render. For HTML already in hand, Turndown in your own process is free and instant. Pay for an API when the fetch and the render are the hard part.