← WebExtract · Docs · Try it

Measured results: WebExtract on 10 public pages

Read this first. Test date 2026-10-26 (KST); runs between 09:17 and 09:56 KST. One client: a single cloud-hosted machine, so results from your network will differ. Sample: 10 pages, 4 runs per page per endpoint, 80 sandbox calls, plus 14 calls on the live free path. Sandbox latency includes the testnet payment settlement, so it is not the latency you see on the free or the mainnet path. We did not run any competitor. We did not test mainnet payments or paying through MCP. Competitor facts below are copied from their public pages, each with a link and the date we read it. The sandbox and the live /v1 route run in the same server process and call the same extraction function; only the payment step differs. Every number here is computed by a script from the raw files linked at the bottom, and you can rerun it.

Try it yourself

Two commands, no account, no wallet. The free tier needs the opt-in header X-Billing: free (or ?free=1 in the URL). Without it the same URL answers HTTP 402 with the price options.

curl -H "X-Billing: free" "https://api.webextract.dev/v1/markdown?url=https://example.com"
curl -H "X-Billing: free" "https://api.webextract.dev/v1/extract?url=https://example.com"

Output of the first command, seen on 2026-10-26 at 18:48 KST (fetched_at will differ on your run):

{"url":"https://example.com","final_url":"https://example.com/","fetched_at":"2026-10-26T09:48:57.337Z","content_source":"raw_html","title":"Example Domain","markdown":"This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.","markdown_truncated":false,"word_count":25}

The response header X-Free-Remaining shows how many free calls are left for your address today; curl -s https://api.webextract.dev/v1/balance shows the same number without using a call. When the free calls run out the answer is HTTP 402. To make up to 14 calls and stop cleanly at the first 402, use the free-sample scripts (Python and Node, no dependencies).

1. Sandbox runs

The 10 pages were fetched through /sandbox/v1/markdown and /sandbox/v1/extract, which call the same extraction function as /v1 in the same server process (only the payment step differs: Base Sepolia test USDC), so the volume costs no real money and uses no free quota. Success means HTTP 200 with non-empty markdown. Latency counts successful runs only; p95 is the nearest-rank percentile. Failed runs are kept in the success rate.

EndpointRunsSucceededSuccess rateMedian msp95 msField checks passed
markdown403895.0%3130560575/75
extract4040100.0%30414231212/212

Failed runs:

Field completeness is the share of simple checks that passed. The checks do not use WebExtract: the expected values are parsed straight from each page's HTML (ground_truth.py). For both endpoints: the page's h1 and longest paragraph appear in the markdown, and every table cell appears in the markdown. For extract also: metadata.title equals the page title, the number of tables and rows per table match, and on the product page product.name and product.price match. These checks show that the content is present; they do not show that every field you might want is present.

Which record counts. For each page, endpoint and run number we count the first attempt that was not an HTTP 429 (rate-limit) answer. The sandbox allows 20 requests per minute, so the script waits and resends after a 429. If the first attempt failed for another reason, it still counts as the result, even if a resend later succeeded (this happened once, see the failed runs above). 84 records were logged; 4 are not counted and are listed here:

Time (KST)PageEndpointRunHTTPWhy it is not counted
2026-10-26 09:18:35sts-formsextract1429HTTP 429 rate-limit answer; the request was resent after a wait
2026-10-26 09:18:36httpbin-htmlmarkdown1429HTTP 429 rate-limit answer; the request was resent after a wait
2026-10-26 09:18:36httpbin-htmlextract1429HTTP 429 rate-limit answer; the request was resent after a wait
2026-10-26 09:19:03sts-formsmarkdown1200a later attempt; the first attempt for this page, endpoint and run number is the one counted
Per page results
PageKindEndpointRunsOKMedian msp95 ms
examplestatic-articlemarkdown4437025154
examplestatic-articleextract4424124364
books-homelistingmarkdown4432643724
books-homelistingextract4433423665
books-productproductmarkdown4430704960
books-productproductextract4426163221
books-mysterylistingmarkdown4433764971
books-mysterylistingextract4433823542
quoteslistingmarkdown4427942913
quoteslistingextract4432765658
wsio-productproductmarkdown4428604192
wsio-productproductextract4422282581
wsio-tablestablemarkdown4429665605
wsio-tablestableextract4421982597
sts-simplelistingmarkdown4443506374
sts-simplelistingextract4440294231
sts-formstablemarkdown4333043432
sts-formstableextract4435863811
httpbin-htmlstatic-articlemarkdown4332393558
httpbin-htmlstatic-articleextract4430413100

Each page is public and its robots.txt does not forbid the path; the note for each page is in urls.json. With 4 runs per cell, the per-page p95 is just the slowest run.

2. Are failed fetches charged?

Sandbox, measured. We read the test wallet's on-chain USDC balance, made 10 calls that must fail (a missing page, a host that does not resolve, a non-HTML file, two private addresses; both endpoints each), read the balance again, then made one successful control call and read it a third time. The control must show a charge, otherwise the check proves nothing.

CaseEndpointHTTPError codeMessage
http-404markdown422upstream_errortarget site returned HTTP 404; not charged
http-404extract422upstream_errortarget site returned HTTP 404; not charged
dns-failuremarkdown422dns_failedDNS lookup failed
dns-failureextract422dns_failedDNS lookup failed
not-html-pngmarkdown415unsupported_typeunsupported content type: image/png; not charged
not-html-pngextract415unsupported_typeunsupported content type: image/png; not charged
private-127.0.0.1markdown400forbidden_hostprivate/internal addresses are not allowed
private-127.0.0.1extract400forbidden_hostprivate/internal addresses are not allowed
private-10.0.0.1markdown400forbidden_hostprivate/internal addresses are not allowed
private-10.0.0.1extract400forbidden_hostprivate/internal addresses are not allowed

Server log check. The server operator compared the access-log and payment-journal rows for the same minutes. The log itself is not published. Each sandbox call appears as two access rows: an unpaid 402 first, then the retry with the payment. For all 10 failing calls the journal has a verify line (the payment authorisation was checked) and no settle line, and the 10 retries are 422, 415 or 400. The one control call is the only settlement in the window (22 access rows, 11 verify lines, 1 settle line). The access log does not record the target URL, so the domains were matched to our list by order and by status code.

Live /v1 route. The 10 failures were measured on the sandbox route only; it shares the extraction code with the live route, and the docs state that failed fetches are not charged. We did not run failing URLs through the paid live path, so this page does not present that as measured.

3. Live free path, small sample

The free tier is 20 calls per caller IP per day, shared by REST and MCP, one counter (see the docs). The counter is per caller address: an IPv4 address counts on its own, an IPv6 address counts by its /48 prefix, and a client that uses both IPv4 and IPv6 is counted as two callers. The sample was capped at 14 calls and stopped at the first non-200. Statuses: 200: 13, 402: 1. The 402 is the free quota of the calling address running out, which is the documented behaviour. Our machine's outgoing address is not fixed (the counter the server reported went 11, 10, 9, then back up to 19), so this sample does not measure the 20-call limit.

EndpointnMedian msp95 ms
markdown716751874
extract69921474

With 6 or 7 calls per endpoint these latencies are a sample, not a benchmark, and a page may have been served from a cache. Do not compare them with the sandbox numbers.

4. Price quote latency (no payment)

An unpaid request to the live /v1 endpoints is answered with HTTP 402 and the price options, without fetching the page. This is how fast the quote comes back, 5 requests per endpoint.

EndpointnMedian msp95 msHTTP status
markdown5765786402
extract5765803402

5. Other tools, from their own public pages

We did not run these tools and publish no numbers for them. "Not found" means the page we read does not say. Prices and limits change, so check the linked pages. All pages were read on 2026-10-26 (KST).

WebExtractJina Reader
source: jina.ai/reader, read 2026-10-26
Firecrawl
source: firecrawl.dev/pricing and docs.firecrawl.dev/introduction, read 2026-10-26
Account or API key needed to startNo (free opt-in, or pay per call)Rate-limit table on their page, Reader API row: 20 RPM "w/o API Key", 500 RPM "w/ Free API Key", 500 RPM "w/ Paid API Key", 5000 RPM "w/ Premium API Key"Docs, "Get started": "No account or API key is required for this request." and "Add an API key when you need higher limits."
Markdown outputyes, /v1/markdownyes, r.jina.ai/<url>yes, formats: ["markdown"]
Structured JSONfixed schema in /v1/extract: product, tables, metadatatheir FAQ: "Use the x-json-schema header with a JSON schema definition, or use x-instruction header with natural language instructions. Both features work with ReaderLM-v2""structured JSON" format (their docs); pricing page: "the JSON, Question, and Highlight formats add 4 credits per page"
How it is pricedper call: $0.002 markdown, $0.005 extract (the x402 quote, see quotes.json); prepaid credits also existtokens: the Reader API row says "Count the number of tokens in the output response"; the token price is not on the page we readcredits: 1 credit per scraped page; plans from Free (1,000 credits per month, "no card") to Scale; pricing page: "we add credits in 5 USD increments" on paid plans
Payment methodsUSDC through x402 on 7 networks; card (Stripe) for prepaid creditsFAQ: "Payments are processed through Stripe, supporting a variety of payment methods including credit cards, Google Pay, and PayPal"; x402: not foundFAQ: Stripe (cards, PayPal); sold on AWS Marketplace and Google Cloud Marketplace; x402: not found
Failed requestsSandbox: not charged, measured above. Live: see section 2FAQ: "No, tokens are not deducted for failed requests."FAQ: "A scrape that returns no result is not charged. A page that responds with an error status such as 403 or 404 is still returned to you and costs 1 credit."
Free allowance20 calls per caller IP per day, REST and MCP share one counter (REST needs the opt-in header; details in the docs)FAQ: new users get an auto-generated API key with free tokens; without a key the limit above is 20 RPM (a rate limit, not a quota)Free plan: 1,000 credits per month

Limits of this test

Check it yourself

Everything is in the repository: examples/benchmark.

python run_quotes.py        # free: unpaid requests, answered 402 with the price
python run_live_free.py     # at most 14 calls on the free path
X402_TESTNET_PRIVATE_KEY=<a Base Sepolia test key you own> python run_sandbox.py 4
X402_TESTNET_PRIVATE_KEY=... python run_failures.py
python analyze.py && python make_results.py

Use only a testnet key. The scripts register the Base Sepolia network only, so they cannot sign a mainnet payment. Set your own User-Agent in common.py.