Bing Webmaster Tools: SOAP/POX → JSON migration pack

View on GitHub

Microsoft retires the Bing Webmaster Tools SOAP and POX APIs on 31 August 2026; no official migration tooling exists. Made by apinae.

Hard sunset: 31 August 2026. A correct migration starts with facts from Microsoft's own docs and first-party captures (read 2026-08-12).

Unofficial. Microsoft has not endorsed, reviewed, or participated in this. "Bing" and "Bing Webmaster Tools" are trademarks of Microsoft Corporation.

Harness shape adapted from openai/completions-responses-migration-pack (MIT). Not affiliated with OpenAI.

Envelope and tick-date behaviour corroborated against merj/bing-webmaster-tools (MIT, © 2024 Merj Ltd). Not affiliated with Merj Ltd. Its date parser matches the timezone offset then discards it into datetime.fromtimestamp(), resolving to a different wall clock per machine; this pack does not inherit that.

Layout

Path What it is
scripts/detect.sh scans a repo for Bing SOAP/POX usage; exit 0 clean, exit 1 affected call sites, credentials redacted
scripts/migrate.sh the harness: detect, codemod, differential gate in one command
src/bing_migration/adapter.py pure-function adapter; POX/JSON responses → canonical Python objects
src/bing_migration/codemod.py AST codemod for the two provable edits (URL swap, parse boundary)
src/bing_migration/contract_facts.json 55 types, 12 enums, 23 dateTime fields, 1 byte[], extracted from Microsoft's WSDL/XSD
change-spec.json machine-readable change spec (16 concepts, classification)
change-spec.schema.json JSON schema for the change spec
tests/ differential conformance gate and unit tests; paired captures in tests/fixtures/captured/
src/apps/ POX and JSON example apps
pyproject.toml packaging and [dev] extras
NOTICE attribution
LICENSE MIT

The one thing to know

Swapping /pox/ for /json/ compiles, deploys, returns HTTP 200 — and silently changes your data. Same call, both protocols, captured seconds apart on 2026-08-12:

POX   <Role>Administrator</Role>
JSON  "Role": 0

Enums are the member name in XML, the member ordinal in JSON. if role == "Administrator" becomes 0 == "Administrator"False, and every matching row silently disappears.

Microsoft's retirement notice says this does not happen:

Enum values — No change — enums remain as integers

True of JSON alone, false as migration guidance. Follow the notice literally and you write the bug.

Mechanical share

Callers Mechanical Denominator
POX (URL-string) 87% 13 of 15 concepts
SOAP / WCF 0% transport and parse layer rebuilt by hand

Concept share is not effort share. change-spec.json: 16 concepts — 13 mechanical, 2 judgment (datetime-offset-absent, unordered-results), 1 not_automatable (soap-client). 13 backed by a first-party capture.

The naive fix, failing

tests/fixtures/naive/naive_app.py follows Microsoft's notice step by step: swap URL, parse JSON, unwrap d, handle /Date(ticks)/ — and trusts it about enums.

correct : {'admins': [<1 administrator>], 'crawl_rate': [5, 5, ... 5], 'last_crawled_year': 2026}
naive   : {'admins': [],                  'crawl_rate': [5, 5, ... 5], 'last_crawled_year': 2026}

No exception. HTTP 200 throughout. Two of three fields perfect — which is why it survives review. It passes the source gate; the behaviour gate catches it.

Why this is grounded

Rules are proven against paired captures — the same method over both protocols, seconds apart, in tests/fixtures/captured/. The gate is a differential test over 30 operations:

canonical_from_json(captured_json[method]) == canonical_from_pox(captured_pox[method])

Neither side is authored here, so the test cannot be satisfied by encoding an assumption. Conversion is driven by contract_facts.json, extracted from Microsoft's WSDL/XSD: 55 types, 12 enums, 23 dateTime fields, 1 byte[] field.

No language model in the mechanical path. The adapter is a pure function — no network, clock or randomness.

Use

from bing_migration import from_json_response

roles = from_json_response(response.json())
assert roles[0]["Role"] == "Administrator"   # name, as POX gave you

Returns what your XML code already consumed: enum names, aware UTC datetimes, bytes, None, no d, no __type. Downstream logic does not move.

Codemod

python -m bing_migration.codemod --repo /path/to/your/code           # dry run
python -m bing_migration.codemod --repo /path/to/your/code --write

Makes only the two edits provable from syntax — swap the URL segment, wrap the parse boundary — and refuses the rest with a reason:

5 site(s) NOT rewritten - a human must decide:
  client.py:25: .findtext() is XML element access; the migrated response is a
                dict. Rewrite by hand - the correct key depends on the document
                shape here.

It will not rewrite element access into dict access, will not touch a parser applied to a variable, and never edits config or .env. Edits are exact AST spans; running it twice is a no-op. scripts/migrate.sh runs it by default.

The codemod does not finish the migration, and says so. Afterwards the source gate passes while the app may still be broken. Refusals are not failures — they are what a machine cannot prove.

This is a deadline instrument, not an end state. It keeps POX-shaped data alive so you stay serving past 31 August.

Not covered

A green suite is necessary, not sufficient. It proves nothing about judgment or not_automatable concepts.

Gotcha

Per-site methods return {"ErrorCode":14,"Message":"ERROR!!! NotAuthorized"} for a site with IsVerified: false. Looks exactly like a broken API key. It isn't.

Tests

python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest -q

No network, no API key. Verified on Python 3.12, 3.13, 3.14.

Sources

License

MIT. See LICENSE and NOTICE.