@nytka/plugin-bing
v0.1.0
Published
Bing Webmaster Tools connector for nytka projects. Collects traffic, queries, pages, inbound links and crawl health into datasets/ and registers them with provenance.
Readme
@nytka/plugin-bing
Bing Webmaster Tools connector for nytka projects. Collects traffic, top queries, top pages,
inbound link counts and crawl health for a verified site into the project's datasets/ and
registers each in datasets/index.json with provenance.
Read-only, by construction. The same API carries SubmitUrl, SubmitUrlBatch,
SubmitContent, SubmitFeed, AddBlockedUrl, SaveCrawlSettings, AddSiteRoles and
RemoveSite — some of them destructive against a client's property. This package cannot
reach any of them: the client issues only GET, and only to the six methods in a frozen
allowlist. A method outside the list is refused before a URL is built. Writing to Bing is a
separate decision under 0008,
and this connector is not where it gets made.
Why a second search engine
Every other search-side connector reports on Google. Bing Webmaster Tools is the same class of first-party data for the other engine, free with a verified account and a single API key — no OAuth, no service account, no Cloud project. Two things it gives that nothing else in the line does:
- Inbound links, free.
linksreturns the pages of your site Bing's index has links to, with counts. One engine, your own sites only, no competitor coverage — it is not a backlink tool — but it is link data nobody is billing you for. - A first-party signal for what AI assistants see. Bing's index is what Copilot reads, and its traffic figures count the Chat vertical alongside web results (see the comparison caveat below).
Install
npm install @nytka/plugin-bingOne dependency, @nytka/core, which has none. The API is JSON over HTTP and Node's built-in
fetch speaks it; there is no client library to carry.
Setup
About five minutes, once per user. There is no key file and nothing to put in private/.
1. Verify the site
https://www.bing.com/webmasters → sign in (any Microsoft, Google or Facebook account) → add the site and verify it, if not already done. A site that is not verified returns nothing.
If the site is already in Google Search Console, Import from there is the fastest route: Bing reads the verified properties from your Google account and verifies them in one step.
2. Generate the API key
Settings (top right) → API access → accept the terms on first use → Generate API Key.
The key belongs to the user, not to a site: one key reads every site verified under that
account. Bing issues one key per user, and generating a new one deletes the old one, so
rotating it means updating .env in every project that carries it.
3. Configure
One .env at the project root. Every key for the project lives there and nowhere else.
The names below ship with the package too, so recovering them later never means re-reading this guide:
cat node_modules/@nytka/plugin-bing/.env.example >> .envBING_WEBMASTER_API_KEY=<the key>
BING_SITE=https://example.com/BING_WEBMASTER_API_KEY is the key itself — the value, not a path. Named for Webmaster
Tools specifically because Microsoft issues other Bing keys (the Search APIs) that this
connector cannot use.
4. Verify, and get the site string right
npx nytka-bing sitesListing your site as [verified] means every step above worked.
Copy BING_SITE from this output — do not compose it by hand. Bing keys a site by its
full URL, scheme and trailing slash included, and a spelling that differs from its own is
refused rather than matched. https://example.com/ and http://example.com are two sites to
Bing. sites prints the exact string.
Use
npx nytka-bing sites # auth smoke test — lists readable sites
npx nytka-bing monthly # monthly clicks/impressions, same columns as nytka-gsc monthly
npx nytka-bing traffic # daily impressions and clicks, every day Bing retains
npx nytka-bing queries # top queries: clicks, impressions, average positions
npx nytka-bing pages # top pages, same columns
npx nytka-bing links # pages with inbound links in Bing's index, with counts
npx nytka-bing crawl # crawl and index health, day by day
npx nytka-bing monthly --no-register # write the payload, leave the registry aloneNone of these takes a date range, because the API has none. Each method returns whatever
window Bing retains — the reference says six months for crawl statistics and does not say for
the others — and the connector stores exactly that. There is no --from/--to on purpose: a
client-side filter on a payload the agent never reads belongs in the script that reads it.
The surface, and the question each part answers
| Command | API method | Question | Refreshed by Bing |
|---|---|---|---|
| sites | GetUserSites | Which sites can this key read, and are they verified? | — |
| monthly | GetRankAndTrafficStats | How much Bing traffic did the site get, month by month? | daily |
| traffic | GetRankAndTrafficStats | …and day by day? | daily |
| queries | GetQueryStats | Which queries put the site in front of Bing users, and did they click? | weekly |
| pages | GetPageStats | Which pages does Bing show, and how do they perform? | weekly |
| links | GetLinkCounts | Which pages of the site does Bing's index have links to, and how many? | — (snapshot) |
| crawl | GetCrawlStats | Is Bing crawling and indexing cleanly — pages crawled, in index, response codes, robots blocks, malware flags? | daily |
monthly and traffic are one request each — the same one — bucketed and not bucketed.
Not collected, and why. GetKeyword / GetKeywordStats / GetRelatedKeywords are
keyword research over any term, not data about your site; that is a different question and a
different dataset shape, left for a later release. GetUrlLinks (who links to a given page)
is the per-URL drill-down of links; the page-level counts answer the site-level question
first. GetCrawlIssues, GetUrlInfo, GetUrlTrafficInfo, GetChildrenUrlInfo, GetFeeds
are per-URL and per-feed detail behind crawl. All are reads, and any of them can be added
to the allowlist when a project asks the question.
Several sites
A project may span domains. Comma-separate them, and each becomes its own dataset:
BING_SITE=https://example.com/, https://other.example/One key reads all of them, provided each is verified under the same account. The site is part of every dataset id, so two sites never collide.
Programmatic:
import { run, collect, listSites } from '@nytka/plugin-bing'
const { id, rowCount, rawPath } = await run({ kind: 'monthly' })run() returns counts and paths, never rows.
Bing and gsc side by side: not like-for-like
monthly writes the columns @nytka/plugin-gsc's monthly writes — Month, Clicks,
Impr. — and its id, bing-monthly-example-com-2026-03-to-2026-09, sorts beside
gsc-monthly-example-com-… in the registry. That is so the two series can be put on one chart
on one column. It is not so they can be summed or compared as equals:
- Bing counts every vertical. From 2023-03-24 on,
GetRankAndTrafficStatsincludes impressions and clicks from Web, Chat, News, Images, Videos and Knowledge Panel. gsc'smonthlyistype: webonly. A Bing click may be a Copilot citation. - Different windows. The Search Console API retains 16 months; Bing returns what it holds, and the reference only commits to six months for crawl statistics.
- Different lag. gsc ends its range three days back because Google finalises late. Bing says its traffic data updates daily and its query and page data weekly; this connector stores every row Bing returns and does not trim. Expect the last day or two to move on re-collection.
The registry summary of every monthly entry says "All verticals, not web only" for this
reason.
Troubleshooting
| What you see | What it means |
|---|---|
| BING_WEBMASTER_API_KEY is not set … and exit 0 | The project has not configured Bing — step 3. Not an error: capability is configuration presence |
| Bing rejected the API key (InvalidApiKey) | The key is wrong or was regenerated in the portal since it was copied — step 2 |
| Bing refused GetRankAndTrafficStats (HTTP 400): … | Almost always the site string. Compare BING_SITE against sites output character for character — step 4. Otherwise the site is not verified under the account that owns the key |
| sites lists the site as NOT verified | Step 1 was not finished. Nothing collects until it is |
| no project.yaml found walking up from cwd | Not inside a nytka project. cd to the project root |
| unexpected date from Bing: … | Bing changed its date serialisation. The collection fails rather than bucketing rows under a guess — file it |
| Runs fine, monthly returns 0 rows | Bing has no traffic data for the site yet. A newly verified site takes days to fill |
| Bing rate-limited … (HTTP 429) | Retry later. Microsoft publishes no limit for the read methods, so none is claimed here |
What it writes
| Path | Committed? |
|---|---|
| datasets/payloads/<id>.json | no — the directory ships its own .gitignore |
| datasets/index.json | yes — one entry, added or replaced |
The dataset id
monthly bing-monthly-example-com-2026-03-to-2026-09
traffic bing-traffic-example-com-2026-03-to-2026-09
queries bing-queries-example-com-2026-03-to-2026-09 the months the rows span
pages bing-pages-example-com-2026-03-to-2026-09
crawl bing-crawl-example-com-2026-03-to-2026-09
links bing-links-example-com-2026-09 the month the snapshot was takenEvery id names a period at month granularity, and dateRange keeps the exact span the rows
covered. Bing's methods take no parameters beyond the site, so every collection is the
recurring kind: the same site collected on two days of one month is one series. The id must
not move between them — otherwise a scheduled daily run leaves thirty near-identical entries
for one series, which is the failure @nytka/plugin-gsc 0.3.0 fixed and this package inherits
the fix for. Re-collecting within a month replaces the entry and its payload; a new month at
either end of Bing's window is a new entry.
This is the opposite of gsc's query, which keys on the exact window, and the difference is
the same rule applied to a different case: there the user chooses a window and dimensions, so
two runs can be two different questions and a shared id would destroy one. Here there is
nothing to choose.
links has no dates — a count is Bing's index as of now — so its entry carries
dateRange: null and period: "2026-09", and re-taking the snapshot within a month replaces
it. An empty series (a site with no data yet) is keyed to the month it was collected in:
"nothing here as of September" is one fact, and October's nothing is another.
Every date it writes — collectedAt, both ends of dateRange, the months inside the id — is a
calendar date. The rows' dates are the days Bing meant: the API serialises each as
/Date(1316156400000-0700)/, an instant plus the offset Bing reported it in, and the
connector reads the day in that offset rather than in UTC or in the local zone. A value that
is not that shape fails the collection instead of being bucketed under a guess.
The registry writer preserves the file's existing formatting: adding one dataset produces a one-entry diff.
Rules it follows
- Payloads never enter agent context. Query them with a script; write conclusions to
research/. The CLI prints row counts and paths for this reason. - The key never appears in output. It travels as a query parameter, which is how the API takes it; no URL is printed, and any response body is redacted before it can reach an error message.
sitesprints URLs and verification status only.GetUserSitesalso returns each site'sAuthenticationCodeandDnsVerificationCode; they are dropped, because the smoke test prints to a terminal an agent reads.- The project is found by walking up for
project.yaml, so it works at any install depth. - No YAML parsing, no config file. Secrets from
.env, everything else from flags.
Limits worth knowing
- What has and has not been run against the live API. Confirmed on 2026-09-19 with a
made-up key: the JSON URL format, the query-parameter auth and the 400 fault envelope
(
{"ErrorCode":3,"Message":"ERROR!!! InvalidApiKey"}— theERROR!!!prefix is not in Microsoft's sample, which is why the match is on the substring). Not yet confirmed: a successful response from any method. The row shapes are Microsoft's documented JSON samples, read the same day. The first run with a real key is the check. - SOAP and POX are gone. Microsoft retired both on 2026-08-31; the POX URL answers 404 today. JSON/HTTP is what this package uses and what remains, under the same key.
queriesandpagesare "top" lists. Bing decides how many; the connector stores what it is given. A page or query below Bing's cut is not in the payload and not in the totals.- Data is Bing's index, not the web. Link counts and crawl figures describe what Bingbot has seen, which is a subset of what exists.
Tests
npm testnode --test against recorded responses. No credential, no network, no client data. Sixty-one
tests, including: the reference's own /Date(…)/ sample decoding to the day its XML twin
spells, the DST change moving the offset and not the day, month bucketing losing and inventing
nothing, the id holding still across a month of daily runs, every write method refused before
fetch is reached, and the key absent from every error the package can produce.
