API Keys Hidden in JS Bundles
Many sites embed API keys in their minified JavaScript. Find them with the right grep, the right DevTools workflow, and the right respect for what you're allowed to use them for.
What you’ll learn
- Locate API keys embedded in JS bundles.
- Use DevTools Sources, Search, and Pretty-Print to navigate minified code.
- Combine with HAR captures to find runtime values.
- Recognise the ethical and legal limits of using discovered keys.
A surprisingly large fraction of "secret" API keys aren't secret. They're shipped in the JavaScript bundle of the site you're scraping, downloadable by anyone who hits the page.
This isn't always wrong, public API keys for third-party services (Mapbox, Stripe publishable keys, Algolia search-only keys) are meant to ship to the browser. But they're often the keys you need.
This lesson is how to find them, and the ethics of using them.
Why keys end up in JS
Three reasons:
- Public keys by design. Algolia search-only keys, Mapbox public tokens, Stripe publishable keys. The provider intends them to be public.
- Build-time substitution. The site uses
process.env.API_KEYat build time, which becomes a literal string in the bundle. Sometimes intended public; sometimes accidentally so. - Lazy security. A key was supposed to be server-side only but ended up client-side because someone took a shortcut. These are the "found" keys, and the morally ambiguous ones.
Where to look
Three places, in order:
- The main bundle, usually
app.js,main.[hash].js, or_next/static/chunks/.... Often the biggest JS file on the page. - Inline
<script>tags in HTML, server-rendered config blobs likewindow.__INITIAL_STATE__ = {...}often contain keys. - HTML
<meta>tags,<meta name="apiKey" content="...">(rare, but happens).
The grep workflow
Step 1: download the JS bundle (right-click in Sources → Save).
Step 2: search for likely patterns:
# Common API key formats
grep -oE '"[A-Za-z0-9_-]{20,}"' bundle.js | head
grep -oE 'pk_(live|test)_[A-Za-z0-9]+' bundle.js # Stripe
grep -oE 'sk_(live|test)_[A-Za-z0-9]+' bundle.js # Stripe secret (shouldn't be here!)
grep -oE 'AIza[A-Za-z0-9_-]{35}' bundle.js # Google API
grep -oE 'pk\.eyJ[A-Za-z0-9._-]+' bundle.js # Mapbox
grep -E '(apiKey|API_KEY|api_key|secret|token)' bundle.js | head -20
Step 3: cross-reference. For each candidate, check Network → Fetch/XHR for requests carrying that value. If the site sends it as X-API-Key, Authorization, or in a query string, you've found a real one.
DevTools workflow
Faster than wget+grep:
- Sources tab → ⌘P → search file name (e.g.
main.) to find the bundle. - Click the file →
{}(Pretty print) to deminify. - Search inside (
⌘F):apiKey,API_KEY,secret,token. - For each match, right-click → "Reveal in symbol pane" or note line number.
- Set a logpoint (right-click in gutter → Add logpoint) printing the value, so you see it on every code path that touches it.
Catalog108 example
The /challenges/api/auth/api-key-in-js lab embeds an API key in a JS file that the page loads. The key is required on protected requests as X-API-Key.
Workflow:
# 1. Find the JS bundle
curl -s https://practice.scrapingcentral.com/challenges/api/auth/api-key-in-js \
| grep -oE 'src="[^"]+\.js[^"]*"'
# 2. Download it
curl -s https://practice.scrapingcentral.com/static/js/api-key-app.js > app.js
# 3. Grep
grep -oE 'apiKey[^;]+' app.js
# → apiKey: "ck108-pub-7e8d2f3a..."
# 4. Use it
curl -H "X-API-Key: ck108-pub-7e8d2f3a..." \
https://practice.scrapingcentral.com/api/products
Python equivalent:
import requests, re
# 1. Get the page, find the bundle
page = requests.get("https://practice.scrapingcentral.com/challenges/api/auth/api-key-in-js").text
bundle_path = re.search(r'src="([^"]+app[^"]+\.js)"', page).group(1)
# 2. Download the bundle
bundle = requests.get(f"https://practice.scrapingcentral.com{bundle_path}").text
# 3. Extract the key
key = re.search(r'apiKey\s*[:=]\s*["\']([^"\']+)', bundle).group(1)
print("Key:", key)
# 4. Use it
r = requests.get(
"https://practice.scrapingcentral.com/api/products",
headers={"X-API-Key": key},
)
print(r.json()["products"][:3])
Heuristics for "this looks like a key"
- 20+ char alphanumeric strings, often with
_or-. - Prefixed:
pk_,sk_,AIza,xoxb-,ghp_,sl.,Bearer, well-known provider prefixes. - Quoted in JS:
apiKey: "...",API_KEY = "...",Authorization: \Bearer ${"..."}``. - Hex strings of length 32/40/64 (MD5/SHA-1/SHA-256 hashes, sometimes used as keys).
- Base64-ish strings ending in
==or=.
Patterns that aren't keys:
- Class names:
bg-blue-500,text-gray-700. - Hashed file names:
main.7f9d2.js. - React/Vue keys:
key="user-1". - UUIDs in dev (sometimes are keys, sometimes test data).
Eyeball context. A bare 32-char string in the middle of JSX is usually a className; the same string assigned to a property called apiKey is a key.
The "watch it in DevTools" technique
Sometimes the key is constructed dynamically:
const k = "ck108-pub-" + Math.random().toString(36).substring(2);
Grepping won't find it. Use:
- Set an XHR breakpoint (Sources → XHR/fetch Breakpoints → URL contains).
- Trigger the request.
- When paused, inspect the headers object, see the key as it's about to be sent.
Or set a regular breakpoint inside fetch overrides if the app uses an axios/ky-style interceptor.
Ethics and legality, a serious aside
Just because a key is in the bundle doesn't mean you're allowed to use it.
- Public-by-design keys (Algolia search-only, Mapbox public, Stripe publishable): yes, fine.
- Keys clearly marked private that leaked: legally murky. The site's ToS often forbids "automated access." Use can constitute unauthorized access in some jurisdictions (CFAA in the US, Computer Misuse Act in UK).
- High-privilege keys (write/admin/secret): never. Even reading them is dicey; using them is asking for trouble.
A useful test: would the site's security team be angry if they knew? If yes, don't. Treat the bundle search as a discovery tool, not a license.
When the key has rate limits
The site's frontend uses the key under polite load. Your scraper uses the same key at 100x the rate. The key gets rate-limited or revoked. Plan for that:
- Throttle to roughly what the frontend would naturally produce.
- Don't issue 10k calls/sec from a single IP with a key obviously belonging to a small site.
- If the site rotates the key, you'll need to re-scrape the bundle. Build it into your flow.
Hands-on lab
Hit /challenges/api/auth/api-key-in-js in your browser. Find the API key in the JS bundle using all three methods: page-source view, DevTools Sources search, and a Python regex over the downloaded bundle. Use the key to call /api/products with X-API-Key. Then deliberately use the WRONG key and observe the 401, confirming the key matters. You've now reverse-engineered a real auth mechanism.
Hands-on lab
Practice this lesson on Catalog108, our first-party scraping sandbox.
Open lab target →/challenges/api/auth/api-key-in-jsQuiz, check your understanding
Pass mark is 70%. Pick the best answer; you’ll see the explanation right after.