# Bing Search Scraper & Rank Tracker - SERP Results (`neverempty/bing-search-scraper`) Actor

Get Bing search results (top 10 organic results with title, URL, description and position) for many keywords and countries, and track where your domain ranks. Every page is checked to really answer your keyword. Monitoring mode returns a row only when a rank moves.

- **URL**: https://apify.com/neverempty/bing-search-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.44 / 1,000 search result returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bing Search Scraper & Rank Tracker - SERP Results

Get **Bing search results** for a list of keywords in as many countries as you like: the **top organic results** of Bing's results page with position, title, URL, displayed URL, domain, description and date. List your site and your competitors and you get **where each domain ranks** for each keyword and country, including a row that says `not-in-shown-results` when a domain is not on the page.

**Every page is checked before it is used.** Bing answers some automated visitors with the results of *another* search while keeping your keyword in the page title, so a scraper that does not check can hand you positions for the wrong search. This Actor only uses a page when Bing highlights your keyword's words in the results, and otherwise reads the search again through another connection. What could not be confirmed comes back as a free row that says so, never as a guessed position.

Turn on **monitoring mode**, schedule the Actor, and a run returns a row **only when a tracked domain's position changed**, with the previous position and the positions gained. A change is only reported when three reads through different connections agree.

### What you can use it for

- **Bing rank tracking**: follow your site's position for the keywords you target, per country, and see which keywords move after a content or technical change.
- **Competitor monitoring**: list rival domains next to yours and see who ranks above you on Bing for each keyword.
- **SERP research**: run a list of keywords with an empty domain list and see which pages hold Bing's first page, with titles, descriptions and dates.
- **Local and international SEO**: run the same keywords in `us`, `gb`, `de`, `fr`, `jp` and more in one run; each country is searched through a connection in that country.
- **Dashboards and spreadsheets**: flat rows, one per result or one per domain per search.

### How it works

1. For each keyword and country the Actor opens Bing's public results page, `https://www.bing.com/search?q=<keyword>&setlang=<language>&cc=<COUNTRY>`, through a residential connection in that country, using a new connection for every read.
2. It checks that the page really answers the keyword: Bing shows the words that matched your search in bold, and the page is used only if enough of your keyword's words are bold in the organic results (all of them for keywords of one or two words, at least 60% for longer keywords, because Bing does not always bold common words such as "recommended"). A word counts only as itself or its English plural. A page where only the first word of a longer keyword is bold across the results, and each other word is bold in at most two results, is not used: that is how Bing's answers for just the first word looked. If not, the search is read again through another connection, up to 8 times. `keywordWordsHighlighted` on every row says how many of the keyword's words were bold, and `readsUntilVerified` how many reads it took.
3. It reads the organic results (`li.b_algo`) in the order the page lists them. Answer boxes, video carousels and other blocks that are not organic results are not counted as positions.
4. Your tracked domains are looked up on that page. A domain matches its subdomains too (`apify.com` matches `console.apify.com`) and `www.` is ignored. A domain that is not on the page gets `rank: null` and `rankStatus: "not-in-shown-results"`, together with `resultsShown`, the number of results the page showed.

**Why the check matters (measured on 2026-09-16, UTC).** Read from datacenter connections, all 12 pages for four everyday keywords were the results of a different search (for "best running shoes", the results for just "best"). Read through one residential connection, 18 of 18 pages for six keywords were the results of unrelated searches. On a connection that Bing had started answering this way, a normal desktop browser got the same wrong pages, so a browser does not avoid it. Through a new US residential connection for each read, 8 of 8 keywords were confirmed, 7 of them on the first read, and two runs of the same four keywords gave the same positions.

**Positions can differ between reads.** Bing does not show every visitor exactly the same order. Reading the same keyword through new US connections, the order was not always the same: for "project management software" 4 of 8 confirmed reads differed from the most common order (2 only at positions 9 and 10, and 2 were a different order altogether, with forbes.com at position 6 instead of 1); for "best running shoes" 2 of 7 differed (one with positions 4 and 5 swapped); for "best crm software" none of 4 differed. Positions near the bottom of the page moved most often. Monitoring mode therefore reports a change only when three reads agree, and a single run with monitoring off shows one read of the page.

**Confirmation rate by country.** Some countries' connections get the results of other searches more often. In one run of 10 everyday English keywords per country (2026-09-16, UTC), the page was confirmed for 10 of 10 keywords in the US, 8 in the UK and Canada, 7 in Germany and India, and 3 in Australia. Changing how the market is specified (`mkt`, `setlang`, `cc`, language headers) did not help in Australia. Searches that could not be confirmed are free.

**Keywords that often cannot be confirmed.** In our tests no read could be confirmed for a misspelled keyword ("car insurence quote" in the US), a `site:` search, a Japanese keyword written without spaces, or a keyword in a language other than the one searched in that country (a German keyword in the US). For the first three, Bing answered only part of the keyword (for example just "car") with the same page on every connection; the `site:` search was answered with pages from other sites, so a `site:` search is used only when every result is on that site. These come back as a free `unverified-page` row. When the same wrong page comes back three times in a row, the Actor stops reading that search early. In the UK, Bing did answer "car insurence quote" with car insurance results (2 of 3 words bold), and those were returned.

#### How deep it looks

**The first results page only: about 10 organic results per search.** Bing's page 2 (`first=11`) returned exactly the same 10 results as page 1 on every connection we tried, including a browser clicking "Next", so positions beyond the first page are not offered rather than repeated. If your domain is not on the first page, you get `not-in-shown-results`, not a guessed position.

#### Things that show up in the rows

- **Results can be local to the connection's area.** In the US, "car insurance quote" returned insurers' pages for the area of the connection (for example Lockhart, Texas). `proxyCountry` says which country the connection was in.
- **`date` is written the way the page writes it**, in the page's language (`"Aug 3, 2026"`, `"2 days ago"`, `"19. März 2026"`).
- **`approxTotalResults`** is Bing's own "About 21,600 results" figure as a number; `totalResultsText` is the text as shown.
- Bing's click-tracking links are turned back into the real URLs, and Bing's `msockid` tracking parameter is removed.
- Email addresses, and some phone numbers written as one run of digits, are masked in titles and descriptions.

### Input

| Field | Default | What it does |
| --- | --- | --- |
| `keywords` | example keyword | Search terms, one per line. A repeated keyword is searched once and gets a free `duplicate` row. Up to 200 keywords per run. |
| `domains` | empty | Domains to track: a domain (`example.com`) or any URL on it. Empty = one row for every organic result. If the list has entries but none of them is a domain or URL, the run is rejected with a free `invalid-input` row and nothing is searched (so a mistyped list never turns into every result on the page). Up to 50 domains. |
| `countries` | `["us"]` | Two-letter ISO country codes (`uk` is read as `gb`). Every keyword is searched in every country, through a connection in that country. Up to 30 countries; at most 1,000 searches (keywords x countries) per run. |
| `language` | `en` | The language Bing is asked to show the page in (`en`, `de`, `fr`, `ja`, `pt-br`, ...). `pageLanguage` says which language the page was shown in. |
| `maxResults` | `1000` | With monitoring off, stop after this many rows and say what was left out. In monitoring mode it does not cut changes. |
| `monitoringMode` | `false` | Remember each tracked domain's position per keyword and country and return a row only when it changes. Needs at least one domain. |
| `resetMonitoringState` | `false` | Forget every remembered position and start a fresh baseline. Turn it off again after one run. |

```json
{
    "keywords": ["best running shoes", "running shoes for flat feet", "trail running shoes"],
    "domains": ["runnersworld.com", "https://www.wired.com/", "example.com"],
    "countries": ["us", "gb", "de"],
    "language": "en",
    "monitoringMode": true
}
```

If you leave `keywords` out with monitoring off, the example keyword "best running shoes" is searched (the run log says so). In monitoring mode `keywords` and `domains` are required, so a schedule never pays to watch the example. An empty list is rejected with a free `invalid-input` row.

### Output

Without tracked domains, one row per organic result (this one was read from Bing in the US on 2026-09-16, UTC):

```json
{
    "source": "bing-search",
    "status": "ok",
    "scrapedAt": "2026-09-17T09:00:00.000Z",
    "keyword": "best running shoes",
    "country": "us",
    "language": "en",
    "pageLanguage": "en",
    "searchUrl": "https://www.bing.com/search?q=best%20running%20shoes&setlang=en&cc=US",
    "resultsShown": 10,
    "approxTotalResults": 21600,
    "totalResultsText": "About 21,600 results",
    "keywordWordsHighlighted": "3/3",
    "readsUntilVerified": 1,
    "proxyCountry": "US",
    "position": 2,
    "title": "10 Best Running Shoes of 2026 | Tested & Ranked - GearLab",
    "url": "https://www.outdoorgearlab.com/topics/shoes-and-boots/best-running-shoes",
    "displayedUrl": "https://www.outdoorgearlab.com › ... › best-running-shoes",
    "domain": "outdoorgearlab.com",
    "description": "We run more than 50 miles in every pair of shoes we test, putting top models head to head to find the best from Nike, …",
    "date": "May 14, 2026"
}
```

With tracked domains, one row per domain per search. This one was built from the same page; the timestamps and monitoring fields show what a later monitoring run returns after the domain moved from position 7 to 5:

```json
{
    "source": "bing-search",
    "status": "ok",
    "scrapedAt": "2026-09-17T09:00:00.000Z",
    "keyword": "best running shoes",
    "country": "us",
    "language": "en",
    "pageLanguage": "en",
    "searchUrl": "https://www.bing.com/search?q=best%20running%20shoes&setlang=en&cc=US",
    "resultsShown": 10,
    "approxTotalResults": 21600,
    "totalResultsText": "About 21,600 results",
    "keywordWordsHighlighted": "3/3",
    "readsUntilVerified": 1,
    "proxyCountry": "US",
    "trackedDomain": "wired.com",
    "trackedDomainInput": "https://www.wired.com/",
    "rank": 5,
    "rankStatus": "ranked",
    "positionsOfDomain": [5],
    "urlsOfDomain": ["https://www.wired.com/gallery/best-running-shoes/"],
    "title": "The Best Running Shoes - WIRED",
    "url": "https://www.wired.com/gallery/best-running-shoes/",
    "displayedUrl": "https://www.wired.com › gallery › best-running-shoes",
    "description": "We logged thousands of test miles to bring you the best running shoes for speed workouts, long runs, trail races, and …",
    "rank1Url": "https://www.runnersworld.com/gear/a19663621/best-running-shoes/",
    "rank1Domain": "runnersworld.com",
    "change": "rank-changed",
    "isFirstCheck": false,
    "previousRank": 7,
    "positionsGained": 2,
    "previousCheckedAt": "2026-09-16T09:00:00.000Z",
    "previousRankReadAt": "2026-09-15T09:00:00.000Z",
    "confirmedByRereading": true
}
```

A tracked domain that is not on the page comes back like this (same search):

```json
{
    "trackedDomain": "example.com",
    "rank": null,
    "rankStatus": "not-in-shown-results",
    "positionsOfDomain": [],
    "url": null,
    "resultsShown": 10,
    "rank1Domain": "runnersworld.com"
}
```

What the columns mean:

- `keyword`, `country` and `searchUrl` say which search the row is from; `status` is `"ok"` on every charged row.
- `position` (result rows) and `rank` (domain rows) are positions among the organic results on the page, starting at 1. `resultsShown` is how many organic results the page showed.
- `rankStatus` is `"ranked"` or `"not-in-shown-results"`.
- `positionsOfDomain` lists every position the domain holds on the page (a domain can appear more than once); `rank` is the first of them, and `urlsOfDomain` are the URLs in the same order. `title`, `url`, `displayedUrl` and `description` on a domain row are those of the result at `rank`.
- `domain` is the result's host without `www.`. `trackedDomain` is your domain as matched; `trackedDomainInput` is what you typed.
- `rank1Url` / `rank1Domain` is the result at position 1 of that search.
- `keywordWordsHighlighted` is how many of the keyword's words Bing showed in bold in the results (`"3/3"`); `readsUntilVerified` is how many reads the search took until the page was accepted (including up to two extra reads that look for a page with every word in bold); `proxyCountry` is the country of the connection.
- `language` is the language you asked for; `pageLanguage` is the language of the page.
- Every row also has `source` and `scrapedAt`.

#### Monitoring fields

On domain rows, `change` is `null` with monitoring off, and in monitoring mode one of:

| `change` | Meaning |
| --- | --- |
| `"first-check"` | The domain had no remembered position for this keyword and country. Every pair is returned once to set the baseline. |
| `"rank-changed"` | The position is different from the one in the row last returned for the pair. |
| `"entered-results"` | The domain was not on the results page last time and is now. |
| `"left-results"` | The domain was on the results page last time and is not now (`rank: null`). |

`isFirstCheck` is `true` on a first check. `previousRank` is the position in the row last returned for the pair, `positionsGained` is `previousRank - rank` (positive = moved up), `previousCheckedAt` is when the pair was last checked and `previousRankReadAt` when that position was read. `confirmedByRereading` is `true` when the search was read two more times a few seconds later, each through another connection, and all reads showed the same position. If either of those reads shows a different position, or cannot be confirmed, nothing is returned for the pair and the remembered position stays as it was (a pair with no remembered position yet stays unremembered and is a first check again next time). Because of these extra reads, a run that finds changes or first checks reads those searches three times.

#### Free rows

These rows are never charged and always say why:

| `status` | When |
| --- | --- |
| `no-change` | Monitoring mode: none of the tracked domains changed position. Only the check fee applies. |
| `unverified-page` | No read of the search showed enough of the keyword's words in bold, so no page could be confirmed as the results for the keyword. Nothing is guessed. |
| `no-results` | Bing said there are no results for the keyword. |
| `blocked` | Bing answered with a bot check or nothing, even after reading again. Nothing is guessed. |
| `unreadable` | The results page could not be read (for example the page layout changed). Nothing is guessed. |
| `invalid-input` | A keyword, domain or country could not be used, or the input as a whole was rejected. |
| `duplicate` | A keyword, domain or country repeated an earlier one. |
| `not-checked` | Rows or searches left out because of `maxResults` or the per-run limits. |
| `budget-reached` | Rows or searches left out because the run's maximum total charge was reached. |

### Pricing

- **Result rows (no tracked domains): $0.60 per 1,000 results**, so a full first page of 10 results costs $0.006. Charged only for rows with `status: "ok"`.
- **Domain rank rows: $3.00 per 1,000 rows**, one per tracked domain per search. A tracked domain that is not on the page is a charged row too: `not-in-shown-results` is the answer to "where does my domain rank".
- **Monitoring mode: $1.00 per 1,000 domain checks**, charged for every tracked domain on every search that was confirmed, changed or not. Example: 20 keywords for one domain, checked once a day, is 600 checks a month, which is **$0.60**, plus $3.00 per 1,000 for the changes returned.
- Searches that could not be confirmed, free rows and extra reads are never charged. The result and rank row prices can be lower on higher Apify plans; the monitoring check fee is the same on every plan.
- The run's maximum total charge is respected: the Actor stops reading before a row or a check would go over it and says what was left out.

### Scheduling and monitoring tips

- Save your input as a task, turn on `monitoringMode` and schedule it (daily is enough for most keywords).
- Positions are remembered per domain, keyword, country and language, for this Actor across all your runs. Do not put the same pairs in two schedules that can run at the same time (Apify's key-value store has no atomic update, so overlapping runs can overwrite each other's records).
- Use `resetMonitoringState` once to start a fresh baseline, then turn it off again.

### Limits

- Positions are those of Bing's public web results page (www.bing.com), first page only. Bing's apps, Copilot answers and signed-in personalization are not seen.
- Ads, answer boxes, "People also ask", videos and images are not returned; ads did not appear on the pages we read.
- Search volume, keyword difficulty and click estimates are not provided.
- Up to 200 keywords, 50 domains, 30 countries and 1,000 searches per run.

# Actor input Schema

## `keywords` (type: `array`):

Search terms, one per line. Each keyword is searched on Bing in every country you pick. A repeated keyword (same words, any letter case) is searched once and gets a free 'duplicate' row. If you leave this field out with monitoring off, the example keyword 'best running shoes' is searched; with monitoring on it is required. An empty list is rejected. Up to 200 keywords per run.

## `domains` (type: `array`):

Your site and competitors: a domain (example.com) or any URL on it (https://www.example.com/page). With domains listed you get one row per domain per search with its position in Bing's top results (a domain also matches its subdomains, and www. is ignored), including a row that says 'not-in-shown-results' when the domain is not on the page. With the list empty you get one row for every organic result on the page. If the list has entries but none of them is a domain or URL, the run stops without searching. Up to 50 domains.

## `countries` (type: `array`):

Two-letter ISO country codes (us, gb, de, fr, jp, br, ...; uk is read as gb). Every keyword is searched in every country listed, in one run, through a connection in that country. Results differ by country. Up to 30 countries.

## `language` (type: `string`):

The language Bing is asked to show the page in (en, de, fr, ja, pt-br, ...). Every row says which language the page was shown in (pageLanguage).

## `maxResults` (type: `integer`):

With monitoring off, the run stops once this many charged rows have been returned, and a free row says how many rows and searches were left out. In monitoring mode it does not limit the changes returned: every search is checked, as far as the run's maximum total charge allows.

## `monitoringMode` (type: `boolean`):

Needs at least one domain in 'Domains to track'. Off = every tracked domain comes back with its current position, charged per row. On = the Actor remembers each domain's position for each keyword and country and, on later runs, returns a row only when the position changed, the domain entered the top results or left them, with the previous position and the positions gained. A change is only reported, and a first position only remembered, if two more reads of the search, each through another connection a few seconds later, show the same position. The first run returns every pair once to set the baseline. **In monitoring mode every domain checked on a search costs $1.00 per 1,000 checks, changed or not** (searches that could not be verified are free), plus $3.00 per 1,000 for the rows returned. Example: 20 keywords for one domain, checked once a day = 600 checks a month = $0.60. Positions are remembered per domain, keyword, country and language; do not put the same pairs in two schedules that can run at the same time.

## `resetMonitoringState` (type: `boolean`):

Clears every remembered position for this Actor, so the next monitoring run returns each pair once again as a first check. This affects all your monitoring runs. Turn it off again after one run: left on in a schedule, every run returns every pair as a first check and charges the row price for it.

## Actor input object example

```json
{
  "keywords": [
    "best running shoes"
  ],
  "domains": [
    "runnersworld.com",
    "https://www.wired.com/"
  ],
  "countries": [
    "us"
  ],
  "language": "en",
  "maxResults": 1000,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

## `results` (type: `string`):

Without tracked domains: one row per organic result on Bing's first results page (position, title, URL, displayed URL, domain, description, date). With tracked domains: one row per domain per keyword and country, with the domain's position or not-in-shown-results, the page's result at position 1, and in monitoring mode the previous position and the positions gained. Searches that could not be verified or read and invalid input come back as free rows that say why.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "best running shoes"
    ],
    "domains": [
        "runnersworld.com",
        "https://www.wired.com/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/bing-search-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "keywords": ["best running shoes"],
    "domains": [
        "runnersworld.com",
        "https://www.wired.com/",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/bing-search-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "best running shoes"
  ],
  "domains": [
    "runnersworld.com",
    "https://www.wired.com/"
  ]
}' |
apify call neverempty/bing-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/bing-search-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/XBgbmO8GS4qXnP9UE/builds/9Vh3WKPbxzgaz47r7/openapi.json
