Bing Webmaster Tools: SOAP/POX → JSON migration pack
View on GitHubMicrosoft 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
soap-client— SOAP/WCF callers arenot_automatableand unverified. The endpoint ishttps://www.bing.com/webmasterapi/api.svc/soap(fromsoap:addressin the WSDL; Microsoft's notice gives a URL that 400s and claims the WSDL is gone when it served 27,949 bytes). No SOAP request or response was ever captured.xml-body-on-post— unverified deliberately. Every POST-capable operation mutates the account; we will not run one against a live property to close a checkbox.- Request shapes for all 29 mutating operations. Permanently ungrounded.
- 18 of 35 read operations returned empty payloads; their field shapes are unconfirmed and reported as unknown, not fine.
- Sub-millisecond date precision is destroyed by the vendor. Canonical form is millisecond-truncated on both sides.
- Types outside the contract pass through unconverted.
- Generated WCF proxy classes; a JS/TS adapter (detection anchors cover JS/TS).
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
| Ref | URL | Read |
|---|---|---|
| S1 | https://learn.microsoft.com/en-us/bingwebmaster/api-protocols | 2026-08-12 |
| S2 | https://www.bing.com/webmasters/help/soap-pox-api-deprecation-s0appox01 | 2026-08-12 |
| S3 | https://ssl.bing.com/webmaster/api.svc?wsdl | 2026-08-12 |
| C1 | tests/fixtures/captured/ |
2026-08-12 |