Skip to content

omnireach/bing-webmaster

Omnireach's Bing Webmaster Tools, under any PHP host: a site's search queries and pages in Bing with their clicks, impressions, CTR and average position, week by week or over a period, through the Bing Webmaster API (JSON, API key).

Floor
Provider · Omnireach
Version
1.x
Conformance suite
not wired
Verified for real
Verified in part
Licence
MIT
composer require omnireach/bing-webmaster:1.x-dev

Bing Webmaster Tools for omnisense/omnireach: a site's search queries and pages in Bing with their clicks, impressions, CTR and average position, period by period or summed over the query's days. The provider calls the Bing Webmaster API's JSON endpoint (its reference: https://learn.microsoft.com/en-us/dotnet/api/microsoft.bing.webmaster.api.interfaces.iwebmasterapi):

Report Method Request
by query GetQueryStats GET {url}/GetQueryStats?siteUrl=...&apikey=...
by page GetPageStats GET {url}/GetPageStats?siteUrl=...&apikey=...
the whole site GetRankAndTrafficStats GET {url}/GetRankAndTrafficStats?siteUrl=...&apikey=...

{url} is https://ssl.bing.com/webmaster/api.svc/json by default; there is no sandbox. No maintained PHP client of this API was found to reuse.

Installation

composer require omnireach/bing-webmaster symfony/http-client

Requires PHP 8.2, omnisense/omnireach (1.x) and symfony/http-client-contracts (3).

Credentials

An API key: Bing Webmaster Tools, Settings, API access. The site is given as verified there (https://example.org/); ReachQuery::$site reports on another site of the same account.

Options

Option Type Default Required
api_key string - yes the API key
site string - yes the site's URL as verified in Bing Webmaster Tools
url string https://ssl.bing.com/webmaster/api.svc/json no the JSON endpoint's base URL

Per query: ReachQuery::$site replaces site for one report.

Capabilities

contract SearchPerformanceInterface
dimensions query, page, date - one at a time besides the date (combined: false)
metrics clicks, impressions, ctr, position (all four by default)
filters only on the dimension the report is broken down by
country, device no
AudienceInterface no

Behaviour

The call. One GET per report, chosen by the dimension besides the date: GetQueryStats for query, GetPageStats for page, GetRankAndTrafficStats for none. The site and the key go in the query string, which is how Bing's API takes them:

GET https://ssl.bing.com/webmaster/api.svc/json/GetQueryStats
  query: {"siteUrl":"https://atelier.example.org/","apikey":"example-key"}

(seen on a recorded client in the harness, 2026-10-26). The answer's rows are under d.

Bing's periods. Bing chooses the periods, not the query: one row per week ("the data will be updated every week"), about six months back, dated /Date(1316156400000-0700)/ (milliseconds, an offset). Each row's date is read in the offset it names; the rows whose date falls inside the query's days are kept, the others dropped. So a query of three days may hold no Bing row at all, and a month holds four or five.

Grouping. Rows are grouped by the dimension's value (a page is in Bing's Query field too), and by the date when the date is asked: without it, a query's weeks are summed into one row; with it, each week is a row dated by Bing. Clicks and impressions are summed; the CTR is clicks over impressions (0 when there is no impression), rounded to 4 decimals; the position is Bing's AvgImpressionPosition averaged weighted by impressions, rounded to 1 - left out when Bing gave none (the whole site's report has no position).

Order. Without the date, rows are sorted by clicks then impressions, most first; with it, by the dimension's value then the date. Then the query's filters and limit are applied.

Filters. A filter on the dimension of the report (['query' => 'cours de poterie'] in a report by query) is applied to Bing's rows. Any other filter - a page in a report by query, any filter on the whole site's report, a date - is refused with NotSupportedException (Bing Webmaster filters on the dimension "page" only in a report broken down by it.), since Bing cannot filter.

Note. Every report's note: Bing's own periods (weekly rows) inside the query's days; positions weighted by impressions., with ; no position for the whole site for the whole site.

row: {"dimensions":{"query":"atelier poterie strasbourg"},"metrics":{"clicks":16,"impressions":300,"ctr":0.0533,"position":4.3}}
row: {"dimensions":{"query":"cours de poterie"},"metrics":{"clicks":0,"impressions":50,"ctr":0,"position":12}}
note: Bing's own periods (weekly rows) inside the query's days; positions weighted by impressions.

(queries from 2026-09-01 to 2026-10-31 on the package's test answers, in the harness, 2026-10-26)

Limits: no quota figure in what was read of Bing's reference; keep calls few and cache the answers.

Example

<?php

require __DIR__.'/vendor/autoload.php';

use Omnireach\BingWebmaster\BingWebmasterFactory;
use Omnireach\Model\Dimension;
use Omnireach\Model\Metric;
use Omnireach\Model\ReachQuery;
use Omnireach\Registry;
use Symfony\Component\HttpClient\HttpClient;

$registry = new Registry([new BingWebmasterFactory(HttpClient::create())], [
    'bing' => ['factory' => 'bing-webmaster', 'options' => ['api_key' => getenv('BING_WEBMASTER_KEY'), 'site' => 'https://example.org/']],
]);
$bing = $registry->search('bing');

// The queries of the last three months, summed.
foreach ($bing->searchPerformance(ReachQuery::lastDays(90, [Dimension::Query]))->rows as $row) {
    printf("%-40s %4d clicks %6d impressions\n", $row->dimension(Dimension::Query), $row->metric(Metric::Clicks), $row->metric(Metric::Impressions));
}

// One page, week by week.
$weeks = $bing->searchPerformance(new ReachQuery(
    new DateTimeImmutable('-180 days'), new DateTimeImmutable('yesterday'),
    [Dimension::Page, Dimension::Date], [Metric::Clicks, Metric::Position],
    ['page' => 'https://example.org/old-page'],
));

In Symfony:

services:
    Omnireach\BingWebmaster\BingWebmasterFactory:
        arguments: ['@http_client']
    Omnireach\Registry:
        arguments:
            $factories: ['@Omnireach\BingWebmaster\BingWebmasterFactory']
            $config:
                bing: { factory: bing-webmaster, options: { api_key: '%env(BING_WEBMASTER_KEY)%', site: 'https://example.org/' } }

The example needs a key: it was not run live.

Errors

The provider's calls are named Bing Webmaster in the messages, and the method called is the "what": 401 and 403 an InvalidKeyException (Bing Webmaster refused the credentials (401) for GetQueryStats: ..., Bing's Message after the colon), 404 a NotFoundException, any other status of 400 or more or a body that is not JSON a ProviderException, a network error an UnreachableException. A missing api_key or site is an InvalidConfigException when the provider is built; a country, device, source or an analytics metric is a NotSupportedException before any call.

The key travels in the query string, as Bing's API takes it; an UnreachableException's message names the method and the path only, with the HTTP client's reason without the URL (Bing Webmaster could not be reached for GetQueryStats (GET /json/GetQueryStats): Could not resolve host: bing.invalid.). The exception's previous, the HTTP client's, still quotes the URL: keep a log that writes the whole chain private.

Verified, and not

Tested offline (Tests/BingWebmasterProviderTest.php, in the core's harness, PHP 8.4): queries summed over a period, week by week, the whole site, a filter, what Bing cannot answer, an error message, the key kept out of an unreachable Bing's message, the factory through a Registry - against the JSON sample of GetQueryStats in Bing's reference and rows composed in its shape. Tests/BareTest.php loads every class with the package's requirements alone.

Not verified: no Bing Webmaster key was used; no call was made to Bing. Whether AvgImpressionPosition is the position a site owner sees in Bing Webmaster Tools is unchecked: positions are passed as Bing gives them.

Responsibility

An independent, unofficial client, affiliated with Microsoft in no way. Whoever runs it complies with Bing Webmaster Tools' terms (https://www.bing.com/webmasters/): see the README.

Propose a change docs/index.md