Scrape DuckDuckGo Shopping Prices in Python
DuckDuckGo Shopping product data — title, merchant, price, currency, URL, thumbnail, rating, and a sponsored flag — comes back as clean JSON through a shopping API. There is no official DuckDuckGo Shopping search API, so the practical path for a Python script is a shopping API that returns the product listings as structured data.
This guide shows you exactly how, with a tested Python example, the real response shape, and the honest limits you'll hit (DuckDuckGo does not return everything a Google Shopping scraper would).
TL;DR: DuckDuckGo exposes no public API for its Shopping search results. The fastest reliable way to scrape DuckDuckGo Shopping in Python is one HTTPS call to the DuckDuckGo Shopping API with an X-API-Key header, then parse results.shopping. Below is the exact script — query, normalize prices, handle null, store to CSV or SQLite, and alert on drops.
Table of Contents
- Why scrape DuckDuckGo Shopping at all
- What DuckDuckGo actually exposes
- The fields the API returns
- Working Python: one call to a product table
- Store to CSV or SQLite
- Alerting on price drops
- Honest limits: what DuckDuckGo won't give you
- What it costs
- FAQ
Why scrape DuckDuckGo Shopping at all
Google Shopping gets nearly all the attention. Every SERP provider, scraper library, and tutorial covers Google Shopping prices. DuckDuckGo product data is the quieter sibling — and that is exactly why it is worth a look.
Fewer people track it means less competition for the data you collect. If you run a price-monitoring or price-intelligence workflow, adding a second product source gives you a cross-market check on what a shopper actually sees on another engine. DuckDuckGo's product coverage is broad across everyday consumer queries, which makes it a useful complement rather than a low-quality mirror.
What DuckDuckGo actually exposes
First, the honest picture. DuckDuckGo does not have a public API for reading Shopping search results. Its product search returns a compact set of product-listing cards, and those placements are paid slots. That means two practical consequences for a scraper:
- You get a short list per query — the endpoint accepts
numup to 20, but the response is as long as the source surfaces for that query. - Every listing is effectively sponsored, so there is no meaningful organic-vs-ads split the way there is on a general web search results page.
Those aren't bugs in an API — they are properties of the source. Any tool that scrapes DuckDuckGo Shopping runs into the same ceiling, because the page itself only shows that much.
The fields the API returns
The DuckDuckGo Shopping API returns each product with the fields below. Here is an example item from an iphone 15 case query:
{
"position": 1,
"title": "Spigen Mag Armor MagFit for iPhone 15",
"price": "$49.99",
"currency": "USD",
"merchant": "spigen.com",
"store": "spigen.com",
"url": "https://spigen.com/products/...",
"thumbnail": "https://.../product.jpg",
"rating": 4.5,
"reviews": 342,
"sponsored": true,
"snippet": "Shockproof military-grade case with built-in MagSafe..."
}
| Field | What it is |
|---|---|
position | 1-based slot in the DuckDuckGo product listings. |
title | Product name. |
price | Displayed price as text, e.g. $49.99. May be null — see limits below. |
currency | ISO currency code, e.g. USD. A separate field. |
merchant / store | Seller name, e.g. spigen.com. |
url | Direct product URL on the merchant's site. |
thumbnail | Product image. |
rating / reviews | Review score and count when the source shows them (often null). |
sponsored | true when the listing is a paid placement. |
snippet | A short product description. |
Working Python: one call to a product table
Here is a complete, runnable script. It queries DuckDuckGo Shopping, prints a table, and handles the two things that trip people up: missing prices and the short result list.
Replace sk_live_your_key with your key from the dashboard. Authentication is the X-API-Key header.
import re
import requests
API_KEY = "sk_live_your_key"
BASE_URL = "https://apiserpent.com/api/shopping"
def ddg_shopping(query, num=10):
resp = requests.get(
BASE_URL,
params={"q": query, "engine": "ddg", "num": num},
headers={"X-API-Key": API_KEY},
timeout=60,
)
resp.raise_for_status()
return resp.json()["results"]["shopping"]
def to_number(price):
"""'$49.99' -> 49.99 ; None -> None"""
if not price:
return None
m = re.search(r"\d+[\.,]?\d*", price)
return float(m.group().replace(",", "")) if m else None
products = ddg_shopping("iphone 15 case")
print(f"{'#':>2} {'title':38} {'price':10} {'merchant':22}")
print("-" * 76)
for p in products:
print(f"{p['position']:>2} {p['title'][:38]:38} {p.get('price') or 'n/a':10} {p.get('merchant','')[:22]:22}")
priced = sum(1 for p in products if to_number(p.get("price")) is not None)
print(f"\n{len(products)} products, {priced} with a numeric price, "
f"{len(products) - priced} without.")
Run it and you get a table like this (trimmed):
# title price merchant
1 Spigen Mag Armor MagFit for iPhone 15 $49.99 spigen.com
2 iPhone 15 Clear Case n/a casetify.com
3 OtterBox Defender for iPhone 15 $44.95 otterbox.com
10 products, 8 with a numeric price, 2 without.
Notice two things. First, the result list is short — that is DuckDuckGo's own product surface, not a missing feature. Second, one product came back without a price. The script treats that honestly instead of pretending a number exists.
Store to CSV or SQLite
Once you have parsed JSON, persisting it is a few lines. To CSV:
import csv
with open("ddg_shopping.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["position", "title", "price", "currency", "merchant", "url", "rating", "reviews"])
for p in products:
w.writerow([
p["position"], p["title"], p.get("price"), p.get("currency"),
p.get("merchant"), p.get("url"), p.get("rating"), p.get("reviews"),
])
Or to SQLite, which scales better for a long-running history:
import sqlite3
conn = sqlite3.connect("prices.db")
conn.execute("""CREATE TABLE IF NOT EXISTS shopping (
position INTEGER, title TEXT, price TEXT, currency TEXT,
merchant TEXT, url TEXT, rating REAL, reviews INTEGER,
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP)""")
for p in products:
conn.execute(
"INSERT INTO shopping (position, title, price, currency, merchant, url, rating, reviews) VALUES (?,?,?,?,?,?,?,?)",
(p["position"], p["title"], p.get("price"), p.get("currency"),
p.get("merchant"), p.get("url"), p.get("rating"), p.get("reviews")),
)
conn.commit()
Alerting on price drops
With a history table, price-drop detection is a comparison against yesterday's lowest offer per product. Every run, convert the price string with to_number(), skip None values, and compare:
# daily job - assume prev holds the previous lowest per product title
for p in products:
num = to_number(p.get("price"))
if num is None:
continue # no price shown - nothing to compare
if num < prev.get(p["title"], float("inf")):
print(f"PRICE DROP on {p['title']}: now {p.get('price')}")
For the full schedule-and-diff pattern, see our shopping price-monitoring playbook; to turn a drop into an email or webhook notification, the price-drop alerts guide has the details.
Honest limits: what DuckDuckGo won't give you
Scraping DuckDuckGo Shopping has hard ceilings, and a good scraper plans around them:
- Depth. One query returns up to
num(max 20), and in practice only as many as the source surfaces. Requesting more does not conjure extra listings. - No delivery or condition fields. DuckDuckGo's product cards do not expose shipping speed, product condition, or an internal product ID. We do not fabricate them.
- No old-price / discount data. There is no original-price field in the card markup, so a true “was / now” discount split is not available from this source.
- Price can be missing. Some placements render without a visible price, so
pricecan benullat read time. - Mostly sponsored. Expect
sponsored: trueacross the product cards — these are paid placements.
If you need deep catalogs, delivery data, or discount history, a dedicated product feed or a marketplace's own API is the right tool. For a fast, clean read of what DuckDuckGo Shopping shows for a query — which is the realistic use case — a shopping API is the sweet spot. See the Shopping API documentation for the full parameter reference.
What it costs
DuckDuckGo Shopping searches with Serpent are billed per 1,000 calls. New accounts get 10 shared free calls on every endpoint, then pay-as-you-go. Current tiers:
| Tier | Per 1,000 calls | Per call |
|---|---|---|
| Default | $0.60 | $0.0006 |
| Growth | $0.06 | $0.00006 |
| Scale | $0.03 | $0.00003 |
There is no per-page multiplier on shopping and no subscription — credits never expire. See the pricing page for the full table. If you want the Google-shaped view of the same product data instead, the Google Shopping API engine returns link/source/product_rating fields, and the Shopping API hub compares all four engines.
Next step: pull your key from the dashboard, hit the live playground to see the raw response for your own query, then wire the script above into your daily price monitoring job. Start at $0.03 per 1,000 at the Scale tier.
FAQ
Is there an official DuckDuckGo Shopping API?
No. DuckDuckGo does not offer a public API that reads its product search listings. A shopping API that returns the product listings as JSON is the standard workaround.
Are DuckDuckGo Shopping results organic or paid?
They are paid placements. Every product card in the results is a paid slot, so the sponsored field is true across the board rather than an organic/ads split.
Why is my result list so short?
DuckDuckGo shows a compact set of product listings per query. The endpoint accepts num up to 20 but returns as many as the source shows; it cannot exceed what DuckDuckGo surfaces.
Why is the price sometimes null?
Some placements render without a visible price at read time. The API returns price: null rather than inventing a number.
Can I scrape DuckDuckGo Shopping without a browser?
Yes — that is the whole point of the API. One HTTPS request with an X-API-Key header returns parsed JSON. You never run a headless browser or consent flow yourself.



