{
  "openapi": "3.0.1",
  "info": {
    "title": "Which? Trusted Traders Scraper — Vetted UK Trade Leads",
    "description": "Scrape Which?-endorsed UK tradespeople from Which? Trusted Traders: business name, trades and services, areas covered, phone, email, website, postcode, geo, rating, review count, endorsement date and award badges. 100% phone and email fill measured on 198 live rows. No login.",
    "version": "0.1",
    "x-build-id": "8umF9UwcKIhiw9m5q"
  },
  "servers": [
    {
      "url": "https://api.apify.com/v2"
    }
  ],
  "paths": {
    "/acts/scrapersdelight~which-trusted-traders-scraper/run-sync-get-dataset-items": {
      "post": {
        "operationId": "run-sync-get-dataset-items-scrapersdelight-which-trusted-traders-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/acts/scrapersdelight~which-trusted-traders-scraper/runs": {
      "post": {
        "operationId": "runs-sync-scrapersdelight-which-trusted-traders-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor and returns information about the initiated run in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/runsResponseSchema"
                }
              }
            }
          }
        }
      }
    },
    "/acts/scrapersdelight~which-trusted-traders-scraper/run-sync": {
      "post": {
        "operationId": "run-sync-scrapersdelight-which-trusted-traders-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "inputSchema": {
        "type": "object",
        "properties": {
          "source": {
            "title": "Where to get traders from",
            "enum": [
              "search",
              "sitemap",
              "profileUrls"
            ],
            "type": "string",
            "description": "SEARCH walks the trade + location listing pages (e.g. plumbers in London) — the usual choice. SITEMAP takes the WHOLE directory from Which?'s own gzipped sitemap (3,674 endorsed businesses, counted live on 2026-08-12). PROFILE URLS scrapes only the business pages you paste in. All three produce identical rows, and all three cost the same per trader.",
            "default": "search"
          },
          "trades": {
            "title": "Trades",
            "type": "array",
            "description": "Trades to search, as they appear in a Which? URL. Free text is slugified, so \"Painters & Decorators\" becomes \"painters-and-decorators\" — you can type the trade as a person would say it. The 23 trades Which? itself lists as popular: plumbers · electricians · builders · roofers · gardeners · carpenters · plasterers · glaziers · painters-and-decorators · boiler-installation · central-heating-installation-and-servicing- (note the trailing hyphen, it is Which?'s own slug) · bathroom-fitters-and-designers · kitchen-installation · heat-pump-installers · burglar-alarms · carpet-cleaners · computer-repairs · damp-proofing-and-timber-preservation · drain-and-sewer-services · home-removals · aerial-and-satellite-installers · motor-vehicle-mechanics · tyre-fitters. Also verified live on 2026-08-12: locksmiths, tilers, fencing, driveways, flooring, pest-control, tree-surgeons, scaffolding, solar-panel-installers, window-cleaners, garages, interior-designers, landscape-designers, renewable-energy, gas-installers, heating-contractors. A slug Which? does not run returns an empty page rather than a 404, so the run stops with a list of live slugs pulled from the sitemap instead of quietly returning nothing. Used only when Source is SEARCH.",
            "default": [
              "plumbers"
            ],
            "items": {
              "type": "string"
            }
          },
          "locations": {
            "title": "Locations",
            "type": "array",
            "description": "UK places, combined with every trade above — 2 trades × 3 locations = 6 listings walked. Three granularities all work: a city (london, manchester, birmingham, leeds, glasgow, edinburgh, cardiff, bristol, sheffield, liverpool), a county (hertfordshire, west-sussex, fife, leicestershire, pembrokeshire) or a region (greater-manchester, west-midlands, west-yorkshire, merseyside). Which? publishes 67 area slugs in its own sitemap (london, manchester, leeds and greater-manchester are all among them) and accepts many towns beyond that list — york, norwich, coventry, luton and milton-keynes each returned a full page when checked live on 2026-08-12. If a slug does not exist the run stops with an error listing live slugs to use instead. Used only when Source is SEARCH.",
            "default": [
              "london"
            ],
            "items": {
              "type": "string"
            }
          },
          "startUrls": {
            "title": "Listing URLs (optional)",
            "type": "array",
            "description": "Paste Which? listing pages directly instead of, or alongside, trades + locations — e.g. https://trustedtraders.which.co.uk/electricians-in-manchester/. This is the escape hatch for any trade Which? runs that is not in the list above: find the page on the site, paste it here. Page numbers are added automatically; do not include one.",
            "default": [],
            "items": {
              "type": "object",
              "required": [
                "url"
              ],
              "properties": {
                "url": {
                  "type": "string",
                  "title": "URL of a web page",
                  "format": "uri"
                }
              }
            }
          },
          "profileUrls": {
            "title": "Business profile URLs (optional)",
            "type": "array",
            "description": "Specific business pages to scrape, e.g. https://trustedtraders.which.co.uk/businesses/az-plumbing-contractors/. Anything pasted here is scraped whatever Source is set to — useful for re-checking a list of traders you already hold. Note that rows scraped this way carry endorsedSinceSource: \"profile-created\" (see the Actor's README): the endorsement month only exists on the listing card, so this route falls back to the profile creation date, which runs a month or two earlier.",
            "default": [],
            "items": {
              "type": "string"
            }
          },
          "onlyWithPhone": {
            "title": "Only traders with a phone number",
            "type": "boolean",
            "description": "Measured fill is 100% on 198 live London rows, so this normally removes nothing. Leave it off unless you are pasting profile URLs of unknown quality.",
            "default": false
          },
          "onlyWithEmail": {
            "title": "Only traders with an email address",
            "type": "boolean",
            "description": "Measured fill is 100% on 198 live London rows — Which? publishes a real contact mailbox on essentially every endorsed business, which is what makes this directory unusually good for outbound. Like the phone filter, this normally removes nothing.",
            "default": false
          },
          "onlyWithWebsite": {
            "title": "Only traders with a website",
            "type": "boolean",
            "description": "89.4% of Which? traders publish a website (measured on 198 rows), so this discards roughly one in nine. The website only exists on the profile page, so this is the ONE filter that cannot be applied from the card: switching it on forces profile fetching even if \"Open each business profile page\" is off, and the Actor queues about 3× the candidates to still hit your row target.",
            "default": false
          },
          "minRating": {
            "title": "Minimum rating",
            "enum": [
              "0",
              "4",
              "4.5",
              "4.8",
              "4.9"
            ],
            "type": "string",
            "description": "Which? ratings are out of 5 and vetted traders cluster above 4.5, so this filter only bites at the very top of the scale. Traders with no reviews yet have no rating at all and are removed by any value above 0 — that is about 5% of the directory (rating fill measured 94.9% on 198 rows).",
            "default": "0"
          },
          "minReviewCount": {
            "title": "Minimum number of reviews",
            "minimum": 0,
            "type": "integer",
            "description": "Social-proof floor, applied to reviewCount. Newly endorsed traders often sit at 0, and 0 here is a real zero rather than a missing value. Set 10 for firms with a visible review history.",
            "default": 0
          },
          "minYearsEndorsed": {
            "title": "Minimum years endorsed by Which?",
            "minimum": 0,
            "type": "integer",
            "description": "Which? publishes NO company founding year — the only tenure signal on the site is the \"ENDORSED SINCE MMM YYYY\" date on the listing card, which this Actor converts to whole years. This is years endorsed by Which?, not years in business. 64% of the 198-row London sample had been endorsed 5+ years, so 3 is a mild filter and 8 is a strong one.",
            "default": 0
          },
          "fetchProfileDetails": {
            "title": "Open each business profile page (recommended)",
            "type": "boolean",
            "description": "ON (default) opens every profile and returns the full record — website, street address, county, the complete trade and service lists, areas covered, award badges, opening hours and the description. OFF returns ONLY what the listing card publishes (name, phone, email, postcode, geo, rating, review count, endorsed-since) and never opens a profile: about 12× fewer requests. Measured fill in OFF mode over 117 Manchester rows: phone 100%, email 99.1%, rating 94.9%, postcode 100%, geo 100%. The price per trader is identical either way — this buys speed, not money.",
            "default": true
          },
          "includeDescription": {
            "title": "Include the business description",
            "type": "boolean",
            "description": "The trader's own \"About\" text — often 100+ words, which is what makes a CSV heavy. Turn it off for a slim lead file. Costs no extra requests either way.",
            "default": true
          },
          "includeServices": {
            "title": "Include the full service list",
            "type": "boolean",
            "description": "The individual jobs under each trade heading (\"Gas safety testing and inspection\", \"Underfloor heating\", \"Emergency plumbing services\" …). Averages 16.2 per trader on the 198-row London sample, with 97.5% of traders carrying at least one. This is the field to segment on if you sell to a specific job type.",
            "default": true
          },
          "includeOpeningHours": {
            "title": "Include opening hours",
            "type": "boolean",
            "description": "Schema.org opening-hours strings, e.g. \"Mo-Fr: 08:00-18:00\" or \"Mo-Su: 24 hours\". Present on 82.8% of the 198-row sample.",
            "default": true
          },
          "maxItems": {
            "title": "Max traders",
            "minimum": 1,
            "type": "integer",
            "description": "Stop after this many unique traders. You are billed $0.004 per trader RETURNED, so this is your hard cost ceiling: 100 = $0.40, 1,000 = $4.00, the whole 3,674-business directory = $14.70. Duplicates are dropped before billing and never count against it, and rows your filters remove are never charged.",
            "default": 100
          },
          "maxPagesPerSearch": {
            "title": "Max listing pages per search",
            "minimum": 1,
            "maximum": 300,
            "type": "integer",
            "description": "How deep to walk each trade + location listing. 12 traders per page, no promoted repeats, so a page adds 12 new traders — 10 pages ≈ 120 traders per trade × town. Listings also end on their own: plumbers-in-london runs out at page 17 with 198 traders, and Which? marks the end with a redirect back to the last page.",
            "default": 10
          },
          "skip": {
            "title": "Skip the first N traders",
            "minimum": 0,
            "type": "integer",
            "description": "Resume where the last run stopped instead of re-buying rows you already have: run 1 with maxItems 500, run 2 with skip 500. Applies to the sitemap and search routes in the order the site returns them.",
            "default": 0
          },
          "maxConcurrency": {
            "title": "Concurrent requests",
            "minimum": 1,
            "maximum": 5,
            "type": "integer",
            "description": "Kept low on purpose — Which? Trusted Traders is a small site and there is nothing to gain from hammering it. Measured sustained load at concurrency 4 was 215/215 = 100% success through the Apify datacenter proxy. Values above 5 are rejected by the schema.",
            "default": 3
          },
          "requestDelayMs": {
            "title": "Delay between requests (ms)",
            "minimum": 0,
            "maximum": 30000,
            "type": "integer",
            "description": "Pause each worker waits after every request. The default 500 ms at concurrency 3 works out at roughly 2 requests/second. Which? declares no crawl-delay in robots.txt; this is politeness, not a requirement.",
            "default": 500
          },
          "proxyConfiguration": {
            "title": "Proxy",
            "type": "object",
            "description": "Which? is NOT walled. Measured 2026-08-12: direct, Apify datacenter and Apify RESIDENTIAL (with and without country GB) every rung returned HTTP 200 with a full 12-card page — so the default is the CHEAP datacenter rung, which held 100% over a 215-request sustained load, rather than billing you for residential IPs you do not need. If you ever do see failures, switch to RESIDENTIAL + country GB: measured equally reliable (29/30) but slower, 1.63 req/s against 2.22.",
            "default": {
              "useApifyProxy": true
            }
          }
        }
      },
      "runsResponseSchema": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "actId": {
                "type": "string"
              },
              "userId": {
                "type": "string"
              },
              "startedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "finishedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "status": {
                "type": "string",
                "example": "READY"
              },
              "meta": {
                "type": "object",
                "properties": {
                  "origin": {
                    "type": "string",
                    "example": "API"
                  },
                  "userAgent": {
                    "type": "string"
                  }
                }
              },
              "stats": {
                "type": "object",
                "properties": {
                  "inputBodyLen": {
                    "type": "integer",
                    "example": 2000
                  },
                  "rebootCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "restartCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "resurrectCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "computeUnits": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "options": {
                "type": "object",
                "properties": {
                  "build": {
                    "type": "string",
                    "example": "latest"
                  },
                  "timeoutSecs": {
                    "type": "integer",
                    "example": 300
                  },
                  "memoryMbytes": {
                    "type": "integer",
                    "example": 1024
                  },
                  "diskMbytes": {
                    "type": "integer",
                    "example": 2048
                  }
                }
              },
              "buildId": {
                "type": "string"
              },
              "defaultKeyValueStoreId": {
                "type": "string"
              },
              "defaultDatasetId": {
                "type": "string"
              },
              "defaultRequestQueueId": {
                "type": "string"
              },
              "buildNumber": {
                "type": "string",
                "example": "1.0.0"
              },
              "containerUrl": {
                "type": "string"
              },
              "usage": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "integer",
                    "example": 1
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "usageTotalUsd": {
                "type": "number",
                "example": 0.00005
              },
              "usageUsd": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "number",
                    "example": 0.00005
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}