How to Detect jQuery Migrate on Any Website (API Guide)
jQuery Migrate is the compatibility shim that keeps ten-year-old jQuery code running on a modern jQuery build. It is loaded by default on every WordPress front end, by a long tail of legacy themes, and by enterprise portals that were "temporarily" bridged to a newer jQuery years ago and never went back. Its presence tells you something jQuery alone does not: the site is carrying code written for a jQuery API that no longer exists.
This guide covers the fingerprints jQuery Migrate leaves in HTML and in the browser, a console check for a single page, and the DetectZeStack API for detecting it across thousands of domains with the version parsed out. Every JSON example uses the fields the API actually returns, and the two worked examples are real responses from live sites.
What jQuery Migrate Is and Why Sites Still Ship It
The jQuery team removed a batch of deprecated APIs in jQuery 1.9 (2013) and another batch in jQuery 3.0 (2016). To stop those releases from breaking the web, they published jquery-migrate: a plugin that patches the removed methods back in and, in its development build, logs a JQMIGRATE warning to the console every time old code calls one. Two major lines exist:
- jQuery Migrate 1.x restores APIs removed in 1.9 and is meant to sit on top of jQuery 1.9 through 2.x. Its last release was 1.4.1.
- jQuery Migrate 3.x restores APIs removed in 3.0 and sits on top of jQuery 3.x. This is the line you will see on almost every site today.
The intended workflow is: upgrade jQuery, load Migrate, fix everything it warns about, remove Migrate. In practice step three rarely finishes. WordPress core enqueues Migrate on every front-end page under the jquery-migrate script handle, so most of the WordPress web ships it without anyone deciding to. Themes and plugins that were written against jQuery 1.x keep working because of it, and nothing breaks loudly enough to force a cleanup.
Why Detect jQuery Migrate
jQuery is on a majority of the public web, so "uses jQuery" is a weak filter. "Uses jQuery Migrate" is sharper, because Migrate exists only to keep obsolete code alive. The teams who ask for it:
- Migration and modernization leads use it to size the problem. A site loading Migrate has code that will break the moment the shim is removed, so it cannot simply drop jQuery. The inventory of Migrate-loading pages is the real scope of a de-jQuery project.
- Agencies and consultancies selling WordPress maintenance, theme rebuilds, or headless migrations use it as a prospecting signal. A site that still needs Migrate 3.x in 2026 has a theme or plugin nobody has touched in years.
- Security and compliance teams use it as a proxy for stale front-end dependencies. Migrate itself is not the vulnerability; it is the marker that the rest of the jQuery stack was frozen at whatever version first needed the bridge.
- Performance engineers flag it because it is one more render-blocking script in the head, typically loaded on every page whether or not anything on that page needs the patched APIs.
Each of those wants the same two answers: is Migrate present? and which version? The version matters because 1.x and 3.x mean different things, which the pitfalls section covers.
How jQuery Migrate Leaves Fingerprints on a Page
Migrate advertises itself in two independent ways: a script tag in the HTML, and a set of properties it attaches to the jQuery object at runtime. The first is visible to any HTTP client; the second needs a browser.
The jquery-migrate.min.js Script Tag and the WordPress ?ver= Parameter
Migrate is nearly always loaded as its own file, immediately after jQuery core, and the filename says what it is. The common shapes:
| Where it loads from | Typical src | Version location |
|---|---|---|
| WordPress core | /wp-includes/js/jquery/jquery-migrate.min.js?ver=3.4.1 | ?ver= query string |
| Official jQuery CDN | code.jquery.com/jquery-migrate-3.4.1.min.js | filename |
| Public CDNs (cdnjs, jsDelivr) | .../jquery-migrate/3.4.1/jquery-migrate.min.js | URL path |
| Self-hosted, unversioned | /assets/js/jquery-migrate.min.js | none |
WordPress adds two extra tells. Since WordPress 5.5 every enqueued script gets an id attribute derived from its handle, so the tag reads <script id="jquery-migrate-js" src="...">. And WordPress appends the version of the enqueued asset as ?ver=, which is why the Migrate version on a WordPress site lives in the query string rather than the filename. A plain grep over the HTML catches all of the above:
curl -sL https://gridpane.com | grep -oE '<script[^>]*jquery-migrate[^>]*>'
On that site the result is a single tag:
<script id="jquery-migrate-js" src="https://gridpane.com/wp-includes/js/jquery/jquery-migrate.min.js?ver=3.4.1" ...>
The jQuery.migrateVersion and jQuery.migrateWarnings Globals
Once it executes, Migrate decorates the jQuery object. The properties that matter for detection:
jQuery.migrateVersionis the version string, for example"3.4.1". This is the cleanest runtime signal.jQuery.migrateWarningsis an array of every deprecation warning Migrate has generated on the page so far. An empty array means Migrate is loaded but nothing on the page has hit a patched API yet; a non-empty array is a list of exactly which removed APIs the site still depends on.jQuery.migrateMuteandjQuery.migrateTracecontrol whether warnings print and whether they include a stack trace.
Because these are runtime properties, they only exist inside a browser that has executed the page's scripts. An HTTP fetch of the HTML cannot see them, which is why the API-side detection in the next sections works from the script tag instead.
Manual Detection in the Browser Console
For a single page, open DevTools and paste this in the console. It reports presence, version, and whether the page has already tripped any compatibility patches:
(function () {
var jq = window.jQuery;
if (typeof jq !== 'function') { console.log({ jquery: false }); return; }
var migrate = typeof jq.migrateVersion === 'string';
console.log({
jquery: jq.fn.jquery,
migrate: migrate,
migrate_version: migrate ? jq.migrateVersion : null,
warnings: migrate && jq.migrateWarnings ? jq.migrateWarnings.length : 0
});
})();
A WordPress site on current core prints something like:
{ jquery: "3.7.1", migrate: true, migrate_version: "3.4.1", warnings: 0 }
If warnings is greater than zero, run jQuery.migrateWarnings on its own to read the list. Each entry names the removed API that was called, such as jQuery.fn.load() is deprecated or jQuery.browser is deprecated. That list is the to-do list for removing Migrate from the site.
Migrate without jQuery is impossible. Migrate is a plugin, not a standalone library, so a positive Migrate detection always implies jQuery is present even if the jQuery script itself was bundled somewhere you did not spot. The DetectZeStack fingerprint encodes this: a jQuery Migrate match adds jQuery to the technologies array if it was not already there.
Detect jQuery Migrate at Scale With the DetectZeStack API
The console approach does not survive contact with a domain list. The DetectZeStack /analyze endpoint fetches the page over HTTP, runs the fingerprint set against the headers and HTML, and returns every detected technology as JSON. jQuery Migrate is reported as its own entry in the technologies array, in the JavaScript libraries category, with the version parsed from the script src when one is present.
Try It Free With GET /demo Before Getting a Key
The public /demo endpoint runs the same detector with no authentication. It is rate-limited per IP address, so it is for confirming the response shape rather than for scanning, but it needs nothing beyond curl:
curl -s "https://detectzestack.com/demo?url=https://gridpane.com" | python3 -m json.tool
The response is one JSON object. Trimmed to the entries this article cares about, the live response for that domain looks like this:
{
"url": "https://gridpane.com",
"domain": "gridpane.com",
"technologies": [
{
"name": "WordPress",
"categories": ["CMS", "Blogs"],
"confidence": 100,
"description": "WordPress is a free and open-source content management system written in PHP and paired with a MySQL or MariaDB database. Features include a plugin architecture and a template system.",
"website": "https://wordpress.org",
"icon": "WordPress.svg",
"source": "http",
"version": "",
"cpe": "cpe:2.3:a:wordpress:wordpress:*:*:*:*:*:*:*:*"
},
{
"name": "jQuery",
"categories": ["JavaScript libraries"],
"confidence": 100,
"description": "jQuery is a JavaScript library which is a free, open-source software designed to simplify HTML DOM tree traversal and manipulation, as well as event handling, CSS animation, and Ajax.",
"website": "https://jquery.com",
"icon": "jQuery.svg",
"source": "http",
"version": "",
"cpe": "cpe:2.3:a:jquery:jquery:*:*:*:*:*:*:*:*"
},
{
"name": "jQuery Migrate",
"categories": ["JavaScript libraries"],
"confidence": 100,
"description": "Query Migrate is a javascript library that allows you to preserve the compatibility of your jQuery code developed for versions of jQuery older than 1.9.",
"website": "https://github.com/jquery/jquery-migrate",
"icon": "jQuery.svg",
"source": "http",
"version": "3.4.1",
"cpe": ""
}
],
"categories": {
"CMS": ["WordPress"],
"Blogs": ["WordPress"],
"JavaScript libraries": ["jQuery", "jQuery Migrate"]
},
"meta": { "status_code": 200, "tech_count": 22, "scan_depth": "full" },
"cached": false,
"response_ms": 1423
}
Three things to notice. The Migrate entry is separate from the jQuery entry. Its version is 3.4.1, lifted from the ?ver= query string shown in the grep earlier. And source is http, meaning the match came from the fetched HTML rather than from DNS or TLS signals.
curl Example Against GET /analyze and the technologies Array
For real usage, sign up on RapidAPI and call /analyze with your key. The shape is identical to /demo; the difference is that requests count against your monthly plan instead of the per-IP demo limit. Piping through jq isolates the Migrate entry:
curl -s "https://detectzestack.p.rapidapi.com/analyze?url=https://gridpane.com" \
-H "x-rapidapi-key: $RAPIDAPI_KEY" \
-H "x-rapidapi-host: detectzestack.p.rapidapi.com" \
| jq '.technologies[] | select(.name == "jQuery Migrate")'
When Migrate is present that prints the single matching object:
{
"name": "jQuery Migrate",
"categories": ["JavaScript libraries"],
"confidence": 100,
"description": "Query Migrate is a javascript library that allows you to preserve the compatibility of your jQuery code developed for versions of jQuery older than 1.9.",
"website": "https://github.com/jquery/jquery-migrate",
"icon": "jQuery.svg",
"source": "http",
"version": "3.4.1",
"cpe": ""
}
When it is absent, jq prints nothing and exits zero, which makes the command safe to drop into a shell loop. If you only need a yes/no answer for one domain, the /check endpoint does the filtering server-side and returns a flat object:
curl -s "https://detectzestack.p.rapidapi.com/check?url=gridpane.com&tech=jQuery%20Migrate" \
-H "x-rapidapi-key: $RAPIDAPI_KEY" \
-H "x-rapidapi-host: detectzestack.p.rapidapi.com"
{
"domain": "gridpane.com",
"technology": "jQuery Migrate",
"detected": true,
"confidence": 100,
"version": "3.4.1",
"categories": ["JavaScript libraries"],
"response_ms": 1380,
"cached": false
}
Reading the version Field: Script Filename vs ?ver= Query String
The Migrate fingerprint matches a script src of the form jquery-migrate (or jquery.migrate), optionally followed by -<version>, optionally .min, then .js, optionally followed by ?ver=<version>. Two capture groups feed the version field:
- The version embedded in the filename, as in
jquery-migrate-3.4.1.min.js. - The version in a trailing
?ver=query string, as injquery-migrate.min.js?ver=3.4.1.
If both are present the filename wins. If neither is present, version is an empty string and only name tells you Migrate is there. The ?ver= fallback is what makes WordPress sites report a version, because WordPress never puts the version in the filename.
Filtering for a specific major line in saved responses is one jq expression. This flags every saved scan still on the 1.x series, and treats a missing version as unknown rather than as 1.x:
jq -r '
.technologies[]
| select(.name == "jQuery Migrate")
| (.version // "") as $v
| if $v == "" then "\(input_filename): jQuery Migrate (version unknown)"
elif ($v | split(".")[0] | tonumber) < 3 then "\(input_filename): jQuery Migrate \($v) (1.x line)"
else empty end
' analyses/*.json
Batch Detection Across Thousands of Domains With POST /analyze/batch
For lists, /analyze/batch accepts up to 10 URLs per request and scans up to 5 of them concurrently on the server. Each URL still counts as one request against your plan, so the batch endpoint is about wall-clock time, not quota. The request body is a JSON object with a urls array:
curl -s -X POST "https://detectzestack.p.rapidapi.com/analyze/batch" \
-H "x-rapidapi-key: $RAPIDAPI_KEY" \
-H "x-rapidapi-host: detectzestack.p.rapidapi.com" \
-H "Content-Type: application/json" \
-d '{"urls": ["gridpane.com", "wptavern.com", "example.com"]}' \
| jq -r '
.results[]
| select(.result != null)
| . as $r
| ($r.result.technologies[] | select(.name == "jQuery Migrate")) as $m
| "\($r.url)\t\($m.version // "")"
'
The response has a results array with one item per submitted URL. Each item carries either a result object in the same shape as a single /analyze response or an error string, plus top-level successful, failed, and total_ms counters. The jq above prints one tab-separated line per domain where Migrate was found, with the version or an empty column. For the three domains above the output is two lines, because example.com loads no JavaScript at all:
gridpane.com 3.4.1
wptavern.com
To run a file of a thousand domains, chunk it into tens and feed each chunk to the same call. A short bash loop that writes one JSON line per batch:
#!/usr/bin/env bash
# migrate-batch.sh — usage: migrate-batch.sh domains.txt > results.jsonl
set -euo pipefail
KEY="${RAPIDAPI_KEY:?set RAPIDAPI_KEY}"
HOST="detectzestack.p.rapidapi.com"
split -l 10 "$1" chunk_
for f in chunk_*; do
body=$(jq -R -s -c 'split("\n") | map(select(length > 0)) | {urls: .}' "$f")
curl -s -X POST "https://${HOST}/analyze/batch" \
-H "x-rapidapi-key: ${KEY}" \
-H "x-rapidapi-host: ${HOST}" \
-H "Content-Type: application/json" \
-d "$body" \
| jq -c '.results[] | {
url: .url,
error: (.error // null),
migrate: ((.result.technologies // [])[] | select(.name == "jQuery Migrate")) // null
}'
rm -f "$f"
done
Every output line is {"url":...,"error":null,"migrate":{...}} when Migrate was found, "migrate":null when the scan succeeded without it, or a non-null error when the fetch failed. Keep the error rows; a domain behind aggressive bot protection returns no HTML, and dropping it silently would turn "could not scan" into "does not use Migrate." For concurrency, retries, and quota planning across a larger list, see Batch Scan 1,000 Websites for Tech Stack.
Common Pitfalls
Four situations regularly produce a wrong count. Each one is visible in the data if you know to look.
Migrate Has a Version but jQuery Does Not
Look back at the gridpane.com response: Migrate reports 3.4.1 while the jQuery entry's version is empty, even though the page loads jquery.min.js?ver=3.7.1. The jQuery fingerprint reads the version from the filename or URL path, not from a ?ver= query string, so WordPress-style jQuery tags yield a name without a version. Do not treat the missing jQuery version as a scan failure, and do not infer the jQuery version from the Migrate version. They are separate packages on separate release schedules.
Migrate Without a Version
Sites that serve WordPress core assets through a CDN can strip the query string. wptavern.com loads https://c0.wp.com/c/7.1/wp-includes/js/jquery/jquery-migrate.min.js, where 7.1 in the path is the WordPress version, not the Migrate version. The API correctly reports jQuery Migrate with an empty version. An empty version means "present, version not visible from this signal," never "absent."
Migrate 1.x vs 3.x Mean Different Things
Migrate 3.x on top of jQuery 3.x is the default state of a maintained WordPress site and says little about the theme. Migrate 1.x is a much stronger signal: it only makes sense with jQuery 1.9 through 2.x, so its presence means the site never moved to jQuery 3 at all. WordPress stopped shipping Migrate 1.4.1 in version 5.5 (2020) and moved to the 3.x line in 5.6, so a WordPress site reporting Migrate 1.x is either years behind on core or has a theme that enqueues its own copy. Filter on the major version, not just on presence.
Bundled Builds
A build pipeline that concatenates jQuery and Migrate into one vendor.js leaves no jquery-migrate in any script src. The HTTP-layer detector cannot see inside the bundle, so the API will not report Migrate for those sites. The runtime jQuery.migrateVersion check in the browser still works, because Migrate attaches its properties regardless of how it was delivered. If a specific site matters, confirm it in the console before concluding it is clean.
Related JavaScript Detection
Migrate rarely appears alone, and the same /analyze response that surfaces it names everything else on the page. Follow-ups worth checking in the same result:
- The jQuery entry itself, and how its version is parsed, is covered in How to Detect jQuery on Any Website.
- Migrate almost always rides on WordPress. Check if a Website Uses WordPress walks the header and path signals that confirm it.
- If the same response also lists React, Vue, or Next.js, the site is mid-migration and Migrate is the part they have not removed yet. Detect the JavaScript Framework on Any Website covers those entries.
- For how often Migrate is being added and dropped across a scanned corpus week to week, see Weekly Issue #1: jQuery Is Still Spreading, where jQuery Migrate appears in both the top-added and top-removed tables.
Get Your API Key and Start Detecting
- Confirm the response shape with
curl "https://detectzestack.com/demo?url=https://gridpane.com". No key required. - Sign up at rapidapi.com/mlugoapx/api/detectzestack and copy your
x-rapidapi-key. The free tier is 100 requests per month with no credit card. - Run the
/analyzeexample above against a domain you care about and read thejQuery Migrateentry'sversion. - Move to
/analyze/batchwhen the list grows past a handful of domains.
Conclusion
jQuery Migrate is a compatibility shim, so detecting it is detecting technical debt directly. On one page, jQuery.migrateVersion in the console answers both questions in a second. Across a portfolio, the jQuery Migrate entry in the DetectZeStack technologies array answers them per domain, with the version parsed from either the filename or the WordPress ?ver= string. Read the major version, treat an empty version as unknown rather than absent, and remember that a Migrate hit always means jQuery is there too.
Related Reading
- How to Detect jQuery on Any Website — The library Migrate patches; console globals, script-tag fingerprints, and version parsing
- Weekly Issue #1: jQuery Is Still Spreading — First-party scan data on jQuery and jQuery Migrate adoption week over week
- Check if a Website Uses WordPress — The CMS that enqueues Migrate by default, detected from headers and asset paths
- Batch Scan 1,000 Websites for Tech Stack — Concurrency, retries, and quota patterns for /analyze/batch
- Detect the JavaScript Framework on Any Website — React, Vue, Angular, Svelte, and the rest in one call
- Find Companies Using imagesLoaded — Another jQuery-era library that travels with Migrate on image-heavy WordPress themes
- How to Detect Slick on Any Website — The jQuery carousel plugin; a Slick detection always implies jQuery
Detect jQuery Migrate and Every Other Library in One API Call
One HTTP request returns every framework, library, CDN, CMS, and analytics tag on a page, with version strings where the source reveals them. 100 requests per month free. No credit card.
Get your free API key