Every SERP scraper carries the same assumptions: you will need a browser, you will need residential IPs, and it will cost you 20 credits or more per query. On DuckDuckGo none of that is true. A duckduckgo search api built on ScrapingBee reads ten full results for 1 credit, with no JavaScript and selectors that have not moved in years.
The whole trick is which hostname you ask.
Verified live on 2026-09-15.
https://html.duckduckgo.com/html/?q=<query>
Not duckduckgo.com/?q=. The main site is a JavaScript application that renders results client side, so it needs a browser and gives you a DOM to fight. DuckDuckGo also publishes a plain HTML version for browsers without scripting, and it returns the same ten results as static markup.
curl -G "https://app.scrapingbee.com/api/v1/" \
-H "Authorization: Bearer $SCRAPINGBEE_API_KEY" \
--data-urlencode "url=https://html.duckduckgo.com/html/?q=web+scraping+api" \
-d mode=auto31,885 bytes, spb-cost: 1, ten results. No render_js, no proxy flag, nothing to tune.
The markup is old fashioned in the best sense. Semantic class names, no build hashes, no CSS module suffixes:
| Field | Selector | Count on a live page |
|---|---|---|
| Result container | div.result |
10 |
| Title | a.result__a |
10 |
| URL | a.result__a then @href |
10 |
| Snippet | a.result__snippet |
10 |
| Display URL | a.result__url |
10 |
One rule set covers the whole page:
rules = {
"results": {
"selector": "div.result",
"type": "list",
"output": {
"title": "a.result__a",
"url": {"selector": "a.result__a", "output": "@href"},
"snippet": "a.result__snippet",
"display_url": "a.result__url",
},
}
}Live output, first result:
{
"title": "ScrapingBee - The Best Web Scraping API",
"url": "https://www.scrapingbee.com",
"snippet": "ScrapingBee is a web scraping API that handles ...",
"display_url": "www.scrapingbee.com"
}That was position 5 on a live web scraping api query. The snippet field is DuckDuckGo's own truncated summary, so expect it to end mid sentence.
Ten results per request, consistently. Note url and display_url are not the same field: the first is the link target and the second is the human readable string DuckDuckGo prints under the title, which is already stripped of protocol and query string. Use display_url for grouping by site and url for fetching.
The comparison worth making is against the same job on other engines. A Google SERP through the HTML API needs custom_google=true at 20 credits and can still hit a CAPTCHA. ScrapingBee's dedicated Google endpoint is 10 credits on a light request. DuckDuckGo through its HTML host is 1.
That changes what is affordable:
| Volume | Credits |
|---|---|
| 1,000 queries | 1,000 |
| 10,000 queries | 10,000 |
| 250,000 queries | 250,000, the whole entry plan |
A quarter of a million SERPs on the entry tier is a different kind of budget from 12,500 Google SERPs for the same money. For rank tracking across a long keyword list, or for any research task where the engine matters less than the coverage, that ratio is the argument.
DuckDuckGo results are largely Bing sourced, so treat this as a proxy for Bing style ranking rather than as a Google substitute. What it is good for: link discovery, site coverage checks, mention monitoring, and any pipeline where you need a lot of queries and do not need Google specifically.
DuckDuckGo's HTML endpoint takes the same query string parameters as the main site:
qis the query.klsets region and language, for exampleus-en,uk-en,de-de.soffsets results, in steps of roughly 30 for subsequent pages.dffilters by date, withd,w,mandy.
Each page is another 1 credit request. There is no per request result count parameter, so depth is a loop over s.
Measured from spb-cost response headers:
| Configuration | Credits |
|---|---|
html.duckduckgo.com via mode=auto |
1 |
| Rejected request | 0 |
mode=auto walks the proxy ladder cheapest first and bills only the rung that worked, which on this host is the plain rung every time. It charges nothing at all if every rung fails. It cannot be combined with render_js, premium_proxy or stealth_proxy, and sending both returns HTTP 400 while billing nothing.
Do not add render_js here out of habit. It would take the request from 1 credit to 5 and return the same ten results.
Plan tiers are on the pricing page.
Public DuckDuckGo search results. There is no account surface to scrape here, and scraping under login credentials is prohibited by ScrapingBee's terms of service in any case.
Search results contain other people's content, so treat snippets as quotations rather than as your own text, and follow the link if you need the full page. DuckDuckGo's Terms of Service govern use of the service, and DuckDuckGo publishes a free Instant Answer API which returns topic summaries rather than ranked web results, so it answers a different question from this one.
Reference: extraction rules, data extraction feature.
Adjacent search endpoints: DuckDuckGo news API, DuckDuckGo news results API, DuckDuckGo knowledge graph API, DuckDuckGo video player API, Bing search API, Bing organic results API, Brave search news API, Yahoo search API, Yandex search API.
Why is this so much cheaper than scraping Google?
Because DuckDuckGo publishes a no JavaScript HTML version and does not fight you for it. Google needs custom_google=true at 20 credits through the HTML API, or the dedicated endpoint at 10.
Should I use duckduckgo.com or html.duckduckgo.com? The HTML host. The main site renders client side and would need a browser for the same ten results.
How many results per request?
Ten. Go deeper with the s offset parameter, one credit per page.
Are these Google rankings? No. DuckDuckGo results are largely Bing sourced. Use this where coverage and volume matter more than matching Google exactly.
Do the selectors ever change?
The result__a, result__snippet and result__url classes are semantic rather than generated, so there is no build hash to rotate. That is the main durability advantage over scraping a modern SERP.
MIT. See LICENSE.