B&Q scraper API for AI agents and code
datawebot returns B&Q search results and product pages as structured JSON, with the same fields for every shop: title, price, original price, availability, rating, image and link, plus details such as brand, variants and identifiers when a product page shows them. It runs from your AI agent over MCP or from code over REST on diy.com in United States, at 5 credits per search page and 1 per product page, and failed requests are free.
Fields you get
Every B&Q product comes back in these fields; a field is null when the page doesn't show it. Each endpoint below also lists its raw record schema.
| Field | Type | Description |
|---|---|---|
product_id | string or null | Opaque id. Pass it to get_product (or capability "product") to read the product again. |
title | string | Product title as the shop shows it. |
url | string or null | The product page on the shop. |
shop | string or null | The shop’s hostname, without www. |
price | object or null | amount (number), currency (ISO 4217, or null when the shop doesn’t say) and text (the shop’s own wording when it is the same amount, else null). |
original_price | object or null | The price before a discount, in the same shape as price. |
rating | number or null | Average rating, on the shop’s own scale. |
review_count | integer or null | Number of reviews or ratings. |
image_url | string or null | Main product image. |
availability | string or null | in_stock, out_of_stock, or the shop’s own stock wording. |
sponsored | boolean or null | true for an ad the shop marks as sponsored; null when the page doesn’t say. |
How to run it
From your agent, after connecting it once with the setup prompt (agent setup), ask in plain words:
Use datawebot to extract https://www.diy.com/search?term=cordless+drill and give me each product's title, price and link as JSON.From code, send one request with your API key. The answer has the same shape for every shop; if it says "status": "working", read it again with GET /v1/requests/{id}.
curl https://api.datawebot.com/v1/requests \
-H "Authorization: Bearer $DATAWEBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"capability":"endpoint","endpoint":"bandq.search_items","url":"https://www.diy.com/search?term=cordless+drill","format":"structured"}'Search
B&Q GB (www.diy.com) keyword search (https://www.diy.com/search?term=<keyword>): the first 1-5 card occurrences of results page 1 in grid order (sponsored duplicates kept), after the whole page-1 grid is matched to its ordered JSON-LD ItemList. URL ID, SKU, title, GBP item price or options range (never a selected amount), "Was" price, sponsored label and fulfillment text. No seller, stock, pagination, filters or sorting.
- ID
bandq.search_items- Version
1- Price
- 5 credits / page
- State
- Available
- Typical time
- About 30 s (estimate)
What it does
Reads one search results page (or up to the page limit) and returns one record per product card, in page order. Works on diy.com; other hosts are rejected before anything runs.
Input
One page URL per request, shaped like https://www.diy.com/search?term=cordless+drill, plus these bounded parameters.
| Parameter | Type | Allowed | Default | Set in /v1/requests |
|---|---|---|---|---|
max_products | integer | 1–5 | 5 | POST /v1/jobs only |
Price
5 credits / page. Failed requests are free; a page that loaded but had nothing on it still costs credits. Set max_credits to refuse anything dearer before it runs.
Output
The default, "format": "structured". Every shop answers in the same fields, mapped from the page to the product_summary@1 schema. Fields are null when the page doesn't show them, and a record that can't be mapped is read best-effort from its raw fields. Products are in products, up to 50 a page; follow next_cursor with GET /v1/requests/{id} (MCP: get_results).
| Field | Type | Description |
|---|---|---|
product_id | string or null | Opaque id. Pass it to get_product (or capability "product") to read the product again. |
title | string | Product title as the shop shows it. |
url | string or null | The product page on the shop. |
shop | string or null | The shop’s hostname, without www. |
price | object or null | amount (number), currency (ISO 4217, or null when the shop doesn’t say) and text (the shop’s own wording when it is the same amount, else null). |
original_price | object or null | The price before a discount, in the same shape as price. |
rating | number or null | Average rating, on the shop’s own scale. |
review_count | integer or null | Number of reviews or ratings. |
image_url | string or null | Main product image. |
availability | string or null | in_stock, out_of_stock, or the shop’s own stock wording. |
sponsored | boolean or null | true for an ad the shop marks as sponsored; null when the page doesn’t say. |
{
"request_id": "string",
"status": "done",
"products": [
{
"product_id": "string or null",
"title": "string",
"url": "string or null",
"shop": "string or null",
"price": {
"amount": "number",
"currency": "string or null",
"text": "string or null"
},
"original_price": {
"amount": "number",
"currency": "string or null",
"text": "string or null"
},
"rating": "number or null",
"review_count": "integer or null",
"image_url": "string or null",
"availability": "string or null",
"sponsored": "boolean or null"
}
],
"next_cursor": "string or null",
"meta": {
"as_of": "date-time",
"country": "string",
"credits_used": "integer",
"credits_left": "integer"
}
}Over the Jobs API, GET /v1/jobs/{id}/results?format=structured returns the full product_summary@1 records.
With "format": "raw", the answer keeps the structured fields and adds raw: this endpoint's own records, unchanged, one per item, in the schema below. It is the data the endpoint extracted from the page, not the page's HTML. Same price as structured.
| Field | Type | Description |
|---|---|---|
product_id | string required | URL ID: the 13-digit SKU, or a 7-digit legacy route ID kept apart from the SKU |
sku | string required | ItemList SKU as sent |
url | string (uri) required | Pattern ^https://www\.diy\.com/departments/[^/?#]+/([0-9]{7}|[0-9]{13})_BQ\.prd$ |
title | string required | Card title as shown |
structured_title | string required | ItemList name (may repeat a phrase the card abbreviates) |
title_presentation_difference | boolean required | — |
price | string or null required | Shown GBP item price; null on an options range |
price_min | string or null required | Options range minimum (not a selected price) |
price_max | string or null required | Options range maximum (not a selected price) |
price_kind | "current_item" | "options_range" required | — |
price_text | string required | Price as shown |
currency | "GBP" required | — |
structured_offer_price | string required | ItemList Offer price: the item price, or the range maximum |
comparison_price | string or null required | "Was" price when shown |
sponsored_label | string or null required | Card sponsorship label as shown |
fulfillment_text | string or null required | Card fulfillment summary as shown (not stock at a store) |
seller_name | null required | — |
seller_id | null required | — |
variant_id | null required | — |
offer_id | null required | — |
shipping_price | null required | — |
availability | null required | — |
page | 1 required | — |
position | integer required | Minimum 1 |
{
"product_id": "string",
"sku": "string",
"url": "string (uri)",
"title": "string",
"structured_title": "string",
"title_presentation_difference": "boolean",
"price": "string or null",
"price_min": "string or null",
"price_max": "string or null",
"price_kind": "current_item | options_range",
"price_text": "string",
"currency": "GBP",
"structured_offer_price": "string",
"comparison_price": "string or null",
"sponsored_label": "string or null",
"fulfillment_text": "string or null",
"seller_name": null,
"seller_id": null,
"variant_id": null,
"offer_id": null,
"shipping_price": null,
"availability": null,
"page": 1,
"position": "integer"
}Raw needs this endpoint: a search with format: "raw" doesn't fall back to shopping search, and answers raw_unavailable where no endpoint can run. Over the Jobs API, results are raw by default; format=both gives each raw record with its structured form.
Request
Run exactly this endpoint with capability: "endpoint"; there is no fallback, and a failed run is free. Set format to "raw" for the raw records too. For a keyword instead of a URL, search_products with this shop as site builds the search URL, tries the shop's endpoint first and then shopping search.
curl https://api.datawebot.com/v1/requests \
-H "Authorization: Bearer $DATAWEBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"capability":"endpoint","endpoint":"bandq.search_items","url":"https://www.diy.com/search?term=cordless+drill","format":"structured"}'submit_job {
"endpoint": "bandq.search_items",
"url": "https://www.diy.com/search?term=cordless+drill",
"format": "structured"
}If the answer says "status": "working", read it again with GET /v1/requests/{id} (MCP: get_results) after check_after_seconds.
Product details
One exact B&Q GB product page (www.diy.com/departments/<slug>/<13-digit id>_BQ.prd) with the seller it shows: title (kit/battery wording kept), brand, gtin13, description, specification table, seller name and ID, GBP price, "Was" price, and each fulfillment offer (Click and Collect, In-Store Purchase, Home Delivery) with its own source availability. No global stock, store, postcode, shipping price or variant selection.
- ID
bandq.product_details- Version
1- Price
- 1 credit / page
- State
- Available
- Typical time
- About 30 s (estimate)
What it does
Reads one product page and returns that product. Works on diy.com; other hosts are rejected before anything runs.
Input
One page URL per request, shaped like https://www.diy.com/departments/terratek-18v-cordless-drill-and-screwdriver-set-drill-bit-13-piece-kit-li-ion-battery-included-lightweight-power-battery-drill/5060452746685_BQ.prd.
Price
1 credit / page. Failed requests are free; a page that loaded but had nothing on it still costs credits. Set max_credits to refuse anything dearer before it runs.
Output
The default, "format": "structured". Every shop answers in the same fields, mapped from the page to the product@1 schema. Fields are null when the page doesn't show them, and a record that can't be mapped is read best-effort from its raw fields. The product is in product.
| Field | Type | Description |
|---|---|---|
product_id | string or null | Opaque id. Pass it to get_product (or capability "product") to read the product again. |
title | string | Product title as the shop shows it. |
url | string or null | The product page on the shop. |
shop | string or null | The shop’s hostname, without www. |
price | object or null | amount (number), currency (ISO 4217, or null when the shop doesn’t say) and text (the shop’s own wording when it is the same amount, else null). |
original_price | object or null | The price before a discount, in the same shape as price. |
rating | number or null | Average rating, on the shop’s own scale. |
review_count | integer or null | Number of reviews or ratings. |
image_url | string or null | Main product image. |
availability | string or null | in_stock, out_of_stock, or the shop’s own stock wording. |
sponsored | boolean or null | true for an ad the shop marks as sponsored; null when the page doesn’t say. |
product.details, when the shop's page maps to product@1:
| Field | Type | Description |
|---|---|---|
brand | string or null | Brand as the page names it. |
category_path | array of strings | Breadcrumb categories, top first. |
description | string or null | Plain-text description. |
feature_bullets | array of strings | Listed features. |
price_min, price_max | number or null | Lowest and highest variant price. |
discount_label | string or null | Discount wording, such as "-20%". |
stock | integer or null | Units in stock, when the page shows a number. |
sold_count, sold_count_basis | integer or null; "total", "monthly" or null | Units sold, and whether that is a total or per month. |
identifiers | array of {type, value} | asin, parent_asin, walmart_item_id, gtin, mpn, isbn or source_sku. |
specs_raw | array of {name, value} | The specification table as shown. |
images, image_count | array of {url, alt}; integer | Every product image. |
option_names | array of strings | Option names such as Size or Colour. |
variants, variant_count | array (up to 100); integer or null | Each variant: id, name, options, sku, final_price, price_before_discount, discount_label, available, stock, image_url. |
seller | object or null | id, name, url and type (official or marketplace). |
rating_scale | number or null | The top of the rating scale, such as 5. |
warnings | array of strings | Fields the page didn’t give, such as currency_missing. |
{
"request_id": "string",
"status": "done",
"product": {
"product_id": "string or null",
"title": "string",
"url": "string or null",
"shop": "string or null",
"price": {
"amount": "number",
"currency": "string or null",
"text": "string or null"
},
"original_price": {
"amount": "number",
"currency": "string or null",
"text": "string or null"
},
"rating": "number or null",
"review_count": "integer or null",
"image_url": "string or null",
"availability": "string or null",
"sponsored": "boolean or null",
"details": {
"brand": "string or null",
"variants": [
"…"
],
"…": "…"
}
},
"meta": {
"as_of": "date-time",
"country": "string",
"credits_used": "integer",
"credits_left": "integer"
}
}Over the Jobs API, GET /v1/jobs/{id}/results?format=structured returns the full product@1 records.
With "format": "raw", the answer keeps the structured fields and adds raw: this endpoint's own records, unchanged, one per item, in the schema below. It is the data the endpoint extracted from the page, not the page's HTML. Same price as structured.
| Field | Type | Description |
|---|---|---|
url | string (uri) required | Pattern ^https://www\.diy\.com/departments/[^/?#]+/[0-9]{13}_BQ\.prd$ |
canonical_url | string (uri) required | Pattern ^https://www\.diy\.com/departments/[^/?#]+/[0-9]{13}_BQ\.prd$ |
product_id | string required | Pattern ^[0-9]{13}$ |
gtin13 | string required | Product JSON-LD gtin13 (= URL ID = DOM Product code) |
title | string required | h1 = Product JSON-LD name (kit/battery wording kept) |
brand | string or null required | — |
model | string or null required | Model name/number specification when shown |
description | string required | Product JSON-LD description as sent |
specifications | object required | Specification table name/value text as shown |
seller_name | string required | Seller the page shows (company marketplace seller) |
seller_id | string required | Literal seller ID from the seller link |
price | string required | Shown GBP price, equal to every fulfillment Offer |
price_text | string required | — |
currency | "GBP" required | — |
comparison_price | string or null required | "Was" price when shown |
fulfillment_offers | array required | Each fulfillment method with its own source availability; no global stock |
variant_id | null required | — |
offer_id | null required | — |
shipping_price | null required | — |
availability | null required | — |
{
"url": "string (uri)",
"canonical_url": "string (uri)",
"product_id": "string",
"gtin13": "string",
"title": "string",
"brand": "string or null",
"model": "string or null",
"description": "string",
"specifications": "object",
"seller_name": "string",
"seller_id": "string",
"price": "string",
"price_text": "string",
"currency": "GBP",
"comparison_price": "string or null",
"fulfillment_offers": "array",
"variant_id": null,
"offer_id": null,
"shipping_price": null,
"availability": null
}Over the Jobs API, results are raw by default; format=both gives each raw record with its structured form.
Request
Read a product page with capability: "product"; it runs this endpoint for the page's shop. Set format to "raw" for the raw records too.
curl https://api.datawebot.com/v1/requests \
-H "Authorization: Bearer $DATAWEBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"capability":"product","url":"https://www.diy.com/departments/terratek-18v-cordless-drill-and-screwdriver-set-drill-bit-13-piece-kit-li-ion-battery-included-lightweight-power-battery-drill/5060452746685_BQ.prd","format":"structured"}'get_product {
"url": "https://www.diy.com/departments/terratek-18v-cordless-drill-and-screwdriver-set-drill-bit-13-piece-kit-li-ion-battery-included-lightweight-power-battery-drill/5060452746685_BQ.prd",
"format": "structured"
}If the answer says "status": "working", read it again with GET /v1/requests/{id} (MCP: get_results) after check_after_seconds.
Limits
- One page URL per request, on diy.com only; other hosts are rejected before anything runs.
- Prices and stock are read when the request runs (
meta.as_of); datawebot keeps no price history, so schedule and store repeat reads yourself. - Fields the page doesn't show come back null; nothing is filled in or guessed.
- Up to 30 requests a minute per account.
Questions about the B&Q scraper API
Does B&Q have an official API?
datawebot does not use one. It reads B&Q's pages and returns them as structured JSON, so you need no B&Q developer account or partner key. Whether B&Q offers its own API, and on what terms, is for B&Q to say.
Can I scrape B&Q with Python?
Yes. Send the POST /v1/requests call shown on this page from Python with any HTTP client, such as requests or httpx: the same JSON body, with your API key in the Authorization header. The products come back in the answer's products field (product for a product page).
Is it legal to scrape B&Q?
It depends on where you are, B&Q's terms and what you do with the data. You stay responsible for how you use what datawebot returns. This is not legal advice; read our guide to robots.txt and legal considerations, and ask a lawyer about your case.
What does a B&Q scraper API call cost?
5 credits per search page and 1 per product page. Failed requests are free; a page that loaded but had nothing on it still costs credits. Every account starts with 200 free credits, and credit packs start at 10,000 credits for $10, with no subscription.
Start with 200 free credits
Connect your agent with one setup prompt, or create an API key, and run B&Q requests right away. No card and no subscription.
Connect your agent See pricing