October 14, 2026
How to Discover Hidden API Endpoints Using JavaScript Analysis
Hi, I’m Vipul — the human behind TheHackersLog.

By Vipul Sonule
11 min read
I spend a lot of time exploring cybersecurity, ethical hacking, reconnaissance, and web security. One lesson keeps coming back: the application often tells you what it can do. You just have to read what it sends you.
Every modern web app ships a pile of JavaScript to your browser. That JavaScript has to know where the backend lives, so it contains URLs, paths, parameter names, and sometimes feature flags for things that aren't visible in the UI. Reading it is one of the most underrated recon skills.
This guide is for beginners and intermediate researchers who already know what an API is and want a repeatable way to find endpoints that normal crawling misses. By the end you'll know:
- Why JavaScript leaks API structure
- How to collect JS files properly
- How to extract endpoints with manual methods, tools, and scripts
- How to validate what you find without being noisy
- What defenders can do about it
⚠️ Ethical Testing Note: Only test these techniques against systems you own, lab environments, or bug bounty targets that are explicitly in scope. Always follow the program's rules. Everything below uses
example.comand localhost as placeholders.
🧠 Why JavaScript Reveals Hidden Endpoints
Think about a single-page application (SPA) built with React, Vue, or Angular. The HTML you receive is almost empty. The real application lives in bundled JavaScript, which runs in your browser and calls the backend with fetch(), XMLHttpRequest, or libraries like Axios.
For those calls to work, the code must contain the endpoint paths. They end up in the bundle in forms like:
javascript
fetch("/api/v2/users/profile")
axios.get(`${BASE_URL}/orders/${id}`)
xhr.open("POST", "/internal/export/csv")fetch("/api/v2/users/profile")
axios.get(`${BASE_URL}/orders/${id}`)
xhr.open("POST", "/internal/export/csv")Several things make this interesting for researchers:
- Code for unused features gets shipped anyway. A bundle may contain admin panel calls even if you never see an admin UI.
- Old versions linger. You might see
/api/v1/references that the current UI doesn't use. - Lazy-loaded chunks hide things. Many apps only load certain JS files when you visit particular pages or roles.
- Staging and internal URLs leak. Developers sometimes leave
staging-api.example.comin a config object.
One important point: an endpoint appearing in JavaScript doesn't mean it's vulnerable, accessible, or even alive. It's a lead. Your job is to validate it.
🗺️ The Workflow
text
Scope Confirmation
↓
Find the Main Pages / Entry Points
↓
Collect JavaScript Files (live + archived)
↓
Download & Deduplicate
↓
Extract Endpoints, Hosts, Parameters
↓
Check Source Maps & Comments
↓
Clean and Normalize Results
↓
Validate Carefully (status, method, auth behavior)
↓
Manual Analysis in Burp / DevTools
↓
Document FindingsScope Confirmation
↓
Find the Main Pages / Entry Points
↓
Collect JavaScript Files (live + archived)
↓
Download & Deduplicate
↓
Extract Endpoints, Hosts, Parameters
↓
Check Source Maps & Comments
↓
Clean and Normalize Results
↓
Validate Carefully (status, method, auth behavior)
↓
Manual Analysis in Burp / DevTools
↓
Document Findings
Step 1: Start With Scope and the Browser
Before any tool, confirm scope. Check that the domain and its subdomains are in scope, and read the rules about automated requests and rate limits. Some programs ban or restrict automated scanning.
Then do something tools can't do: use the app like a real user. Open Chrome DevTools (F12), go to the Network tab, and filter by Fetch/XHR. Click around, log in with your own test account, and trigger features. You'll see the API calls the app makes.
Next, switch the filter to JS and note the script files that load. Look for:
main.[hash].js,app.[hash].js,bundle.jsvendorandruntimefiles- Numbered chunks like
123.abc456.chunk.js - Files from other hosts like
cdn.example.comorstatic.example.com
A DevTools trick I like
In the Sources tab, press Ctrl+Shift+F (Cmd+Option+F on Mac) to search across all loaded scripts. Try searching for:
/api/fetch(axiosgraphqltokenadmin
It's fast, requires no tools, and generates good leads. Chrome's built-in pretty-print button ({ }) makes minified code readable enough to follow.
Step 2: Collect JavaScript Files at Scale
Manual browsing is great for one app. For larger scopes you want automation.
Crawling with Katana
Katana is a crawler from ProjectDiscovery.
bash
katana -u https://example.com -jc -d 3 -silent -o crawl.txtkatana -u https://example.com -jc -d 3 -silent -o crawl.txtWhat this does:
-usets the target URL.-jcenables JavaScript crawling, so Katana parses JS files for additional endpoints and URLs.-d 3sets crawl depth. Higher means more coverage and more requests, so start small.-silentprints only results.-owrites output to a file.
Filter the output to JS files:
bash
grep -E "\.js(\?|$)" crawl.txt | sort -u > js_files.txtgrep -E "\.js(\?|$)" crawl.txt | sort -u > js_files.txtThis keeps lines ending in .js (optionally followed by a query string) and removes duplicates.
Using archived URLs
Archived sources can reveal JS files that are no longer linked from the live site, which sometimes still contain older endpoint references. Two common tools:
- gau (GetAllUrls)
- waybackurls
bash
echo "example.com" | gau | grep -E "\.js(\?|$)" | sort -u >> js_files.txtecho "example.com" | gau | grep -E "\.js(\?|$)" | sort -u >> js_files.txtArchived files may no longer exist, and older code might not reflect the current application. Treat them as historical clues and validate before relying on them.
Verify the JS files are still alive
bash
httpx -l js_files.txt -mc 200 -silent > live_js.txthttpx -l js_files.txt -mc 200 -silent > live_js.txthttpx probes each URL, and -mc 200 keeps only those that return HTTP 200. Flag names can change between versions, so check httpx -h if something errors.
If you're building a bigger recon pipeline around this, I keep a more structured version of the surrounding workflow in my Advanced Bug Bounty Recon Mastery resource. It's an optional extension, and everything in this article works without it.
Step 3: Download and Deduplicate
Pull the files down so you can search them offline, which is faster, and doesn't hammer the server repeatedly.
bash
mkdir -p js && cd js
while read -r url; do
name=$(echo "$url" | md5sum | cut -d' ' -f1)
curl -s -L --max-time 15 "$url" -o "$name.js"
echo "$url -> $name.js" >> ../js_map.txt
done < ../live_js.txtmkdir -p js && cd js
while read -r url; do
name=$(echo "$url" | md5sum | cut -d' ' -f1)
curl -s -L --max-time 15 "$url" -o "$name.js"
echo "$url -> $name.js" >> ../js_map.txt
done < ../live_js.txtWhat's happening:
- We loop through each URL in
live_js.txt. md5sumof the URL gives a safe, unique filename, because URLs contain characters that break filenames.curl -sis silent,-Lfollows redirects,--max-time 15prevents hanging.js_map.txtrecords which filename came from which URL. Don't skip this, because later you'll want to know where a finding came from.
You can also deduplicate by content, since the same file is sometimes served from several URLs:
bash
md5sum *.js | sort | awk '!seen[$1]++ {print $2}' > unique_js.txtmd5sum *.js | sort | awk '!seen[$1]++ {print $2}' > unique_js.txtStep 4: Extract Endpoints
There are three levels of extraction, from simple to thorough.
Level 1: grep
Grep is crude, but it's quick and shows you the shape of the target.
bash
grep -ohE "[\"'\`](/[a-zA-Z0-9_\-\./\{\}\$]+)[\"'\`]" *.js | sort -u | head -50grep -ohE "[\"'\`](/[a-zA-Z0-9_\-\./\{\}\$]+)[\"'\`]" *.js | sort -u | head -50This matches quoted strings that start with /, which captures most relative API paths. -o prints only the match, -h hides filenames, and -E enables extended regex. You'll get noise (CSS paths, image paths), so narrow it:
bash
grep -ohE "[\"'\`]/(api|v[0-9]+|graphql|rest|internal|admin)[a-zA-Z0-9_\-\./\{\}\$]*" *.js | sort -ugrep -ohE "[\"'\`]/(api|v[0-9]+|graphql|rest|internal|admin)[a-zA-Z0-9_\-\./\{\}\$]*" *.js | sort -uFind full URLs too:
bash
grep -ohE "https?://[a-zA-Z0-9\.\-]+(:[0-9]+)?(/[a-zA-Z0-9_\-\./\?=&%]*)?" *.js | sort -ugrep -ohE "https?://[a-zA-Z0-9\.\-]+(:[0-9]+)?(/[a-zA-Z0-9_\-\./\?=&%]*)?" *.js | sort -uLevel 2: LinkFinder
LinkFinder is built for exactly this. It uses a larger set of regexes to pull endpoints from JavaScript.
bash
python3 linkfinder.py -i https://example.com/static/app.js -o clipython3 linkfinder.py -i https://example.com/static/app.js -o cli-iis the input (a URL, a file, or a folder with a wildcard)-o cliprints results to the terminal instead of generating an HTML report
Check the project README for current usage, since forks and versions differ.
Level 3: Custom Python script
Tools are good, but a small script of your own gives you control over cleaning and output. Here's one that reads all downloaded JS files, extracts likely endpoints and absolute URLs, and writes a deduplicated report.
python
#!/usr/bin/env python3
import re
import sys
import json
from pathlib import Path
# Quoted relative paths like "/api/v1/users"
PATH_RE = re.compile(r"""["'`](/[a-zA-Z0-9_\-./{}$:?=&%]{2,200})["'`]""")
# Absolute URLs
URL_RE = re.compile(r"""https?://[a-zA-Z0-9.\-]+(?::\d+)?(?:/[a-zA-Z0-9_\-./?=&%{}$:]*)?""")
# Paths that are usually noise
NOISE_EXT = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".css", ".woff", ".woff2", ".ico", ".map")
def is_interesting(path: str) -> bool:
if path.lower().endswith(NOISE_EXT):
return False
return path.count("/") >= 1 and len(path) > 3
def scan(folder: Path):
paths, urls = {}, {}
for f in folder.glob("*.js"):
text = f.read_text(errors="ignore")
for m in PATH_RE.findall(text):
if is_interesting(m):
paths.setdefault(m, set()).add(f.name)
for u in URL_RE.findall(text):
urls.setdefault(u, set()).add(f.name)
return paths, urls
if __name__ == "__main__":
folder = Path(sys.argv[1] if len(sys.argv) > 1 else "js")
paths, urls = scan(folder)
report = {
"paths": {p: sorted(files) for p, files in sorted(paths.items())},
"urls": {u: sorted(files) for u, files in sorted(urls.items())},
}
Path("endpoints.json").write_text(json.dumps(report, indent=2))
print(f"[+] {len(paths)} paths, {len(urls)} URLs written to endpoints.json")#!/usr/bin/env python3
import re
import sys
import json
from pathlib import Path
# Quoted relative paths like "/api/v1/users"
PATH_RE = re.compile(r"""["'`](/[a-zA-Z0-9_\-./{}$:?=&%]{2,200})["'`]""")
# Absolute URLs
URL_RE = re.compile(r"""https?://[a-zA-Z0-9.\-]+(?::\d+)?(?:/[a-zA-Z0-9_\-./?=&%{}$:]*)?""")
# Paths that are usually noise
NOISE_EXT = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".css", ".woff", ".woff2", ".ico", ".map")
def is_interesting(path: str) -> bool:
if path.lower().endswith(NOISE_EXT):
return False
return path.count("/") >= 1 and len(path) > 3
def scan(folder: Path):
paths, urls = {}, {}
for f in folder.glob("*.js"):
text = f.read_text(errors="ignore")
for m in PATH_RE.findall(text):
if is_interesting(m):
paths.setdefault(m, set()).add(f.name)
for u in URL_RE.findall(text):
urls.setdefault(u, set()).add(f.name)
return paths, urls
if __name__ == "__main__":
folder = Path(sys.argv[1] if len(sys.argv) > 1 else "js")
paths, urls = scan(folder)
report = {
"paths": {p: sorted(files) for p, files in sorted(paths.items())},
"urls": {u: sorted(files) for u, files in sorted(urls.items())},
}
Path("endpoints.json").write_text(json.dumps(report, indent=2))
print(f"[+] {len(paths)} paths, {len(urls)} URLs written to endpoints.json")How to use it: save it as extract.py, then run python3 extract.py js.
What it does: it reads every .js file in the folder, applies two regexes, filters out static assets, and records which file each endpoint came from. That source mapping is useful when you later need to understand context.
Limitations:
- Regexes can't understand code. Paths built dynamically, like
"/api/" + version + "/users", will be missed or partial. - Heavily obfuscated code may need a different approach.
- You'll still get false positives. Always validate.
For a more advanced option, jsluice from Bishop Fox parses JavaScript properly instead of using regex alone, which helps with trickier cases.
If you like building this kind of tooling, my Pro Recon & Automation Pack collects recon commands and automation ideas in one place, but the script above is a solid start by itself.
Step 5: Look Beyond Obvious Paths
Endpoints are only part of what JavaScript leaks. While reading, also look for:
Base URLs and config objects
javascript
const config = {
apiBase: "https://api.example.com",
stagingApi: "https://staging-api.example.com",
graphqlUrl: "/graphql"
}const config = {
apiBase: "https://api.example.com",
stagingApi: "https://staging-api.example.com",
graphqlUrl: "/graphql"
}New hostnames here are excellent recon leads. Add them to your asset list (if in scope).
Parameter names
javascript
fetch(`/api/search?q=${q}&sort=${sort}&include_deleted=${flag}`)fetch(`/api/search?q=${q}&sort=${sort}&include_deleted=${flag}`)A parameter like include_deleted tells you what the backend supports, even if the UI never exposes it. Whether it's meaningful depends on how the server enforces authorization, which is something you test manually.
HTTP methods
javascript
fetch("/api/orders/" + id, { method: "DELETE" })fetch("/api/orders/" + id, { method: "DELETE" })Knowing which methods the frontend uses helps you understand the API's design, and later which methods deserve careful authorization testing.
Source maps
Source maps (.map files) map minified code back to the original source. Check the end of a JS file:
bash
tail -n 3 app.jstail -n 3 app.jsIf you see a line like //# sourceMappingURL=app.js.map, the map may be accessible. Production apps shouldn't generally expose source maps publicly, because they reveal original file structure, comments, and sometimes internal logic. If one is exposed on an in-scope target, it's worth a note.
Comments and leftovers
Search for TODO, FIXME, debug, test, beta, and deprecated. Developers leave breadcrumbs.
Secrets: handle carefully
JavaScript sometimes contains API keys or tokens. Many are intentionally public (such as map or analytics keys). Don't assume every key is a vulnerability. Check what the key actually grants, and don't use credentials beyond what the program's rules allow. SecretFinder can help you spot candidates, but classification is a human job.
Step 6: Validate Without Being Reckless
You now have a list of candidate endpoints. Validation is where beginners either rush or panic.
Build full URLs first (combine your base host with the paths), then probe gently:
bash
httpx -l candidates.txt -sc -cl -title -silenthttpx -l candidates.txt -sc -cl -title -silent-scshows the status code-clshows content length-titleshows the page title if any
Interpreting status codes sensibly:
ResponseWhat it may suggest200Endpoint exists and responded. Check what it returned.401Exists, requires authentication. Worth understanding.403Exists, access denied. Not the same as non-existent.404Probably not there (or hidden behind a different method/host).405Endpoint exists, but this HTTP method isn't allowed. Useful clue.500Server error. Be careful, and don't keep pushing it.
For a single endpoint, use curl to examine the details:
bash
curl -i https://example.com/api/v1/userscurl -i https://example.com/api/v1/users-i includes response headers, which tell you about content type, caching, server software, CORS headers, and rate-limit headers.
To see which methods an endpoint advertises:
bash
curl -i -X OPTIONS https://example.com/api/v1/userscurl -i -X OPTIONS https://example.com/api/v1/usersLook at the Allow header, but remember servers don't always report this accurately.
If you want to pretty-print JSON responses:
bash
curl -s https://example.com/api/v1/status | jq .curl -s https://example.com/api/v1/status | jq .Then move to Burp Suite
Once you've found interesting endpoints, send them through Burp Suite so you can inspect and modify requests with authentication context. This is where real API testing begins, such as checking object-level authorization, parameter handling, and role boundaries. Our JS analysis only gave us the map.
🧪 Lab Example
Lab Example (localhost / intentionally vulnerable app):
A good way to practice is with a deliberately vulnerable application like OWASP Juice Shop, which is a modern SPA built for learning.
bash
docker run --rm -p 3000:3000 bkimminich/juice-shopdocker run --rm -p 3000:3000 bkimminich/juice-shopThen:
- Open
http://localhost:3000in your browser. - Use DevTools to list the JS files (you'll see something like
main.js). - Download them with
curland run the grep and Python extraction steps above. - Compare the endpoints you extracted with the XHR calls you saw in the Network tab.
Notice which endpoints appear in the code but weren't triggered when you clicked around. That gap between "what the app does" and "what the code knows about" is the core idea of this technique. I haven't printed exact output here because it varies by version, and I'd rather you generate your own.
🧠 Questions I Ask While Reading JavaScript
- Which hosts does this code talk to? Any I haven't seen before?
- Are there multiple API versions? What happened to the older ones?
- Are there paths containing
admin,internal,debug,export, orupload? - Which parameters look like object identifiers (
userId,orderId,accountId)? - Does the frontend enforce a restriction that the backend might not?
- Are different chunks loaded for different user roles?
- Does a GraphQL endpoint exist? Is introspection enabled? (Test within scope only.)
Tools produce leads. Your judgment decides which ones matter.
⚠️ Common Mistakes
1. Treating every found path as a vulnerability. A path in JavaScript is a clue, not a finding. Validate behavior and impact.
2. Ignoring lazy-loaded chunks.
If you only analyze main.js, you miss code that loads on other routes. Browse the whole app, and crawl with JS parsing enabled.
3. Skipping the scope check. Third-party scripts (analytics, chat widgets, CDNs) contain lots of URLs that are not yours to test. Filter to in-scope hosts only.
4. Running aggressive fuzzing straight away. Start by reading and validating. Heavy wordlist fuzzing against production APIs is noisy and may violate program rules.
5. Forgetting authentication. Many endpoints only respond sensibly when logged in. A 401 on an unauthenticated request tells you less than a request with a valid test session.
6. Not recording the source. Always keep the mapping from endpoint to JS file. It helps with reporting and re-testing.
7. Assuming a 200 means something is wrong. A 200 can be a perfectly normal public endpoint. Look at the data returned and ask whether the user should be able to see it.
8. Not re-checking after deployments. JS bundles change when the app updates. New hashes often mean new endpoints.
🛡️ Defensive Perspective
JavaScript is delivered to the client, so you can't hide endpoint paths in it. Security can't depend on secrecy. Defenders should focus on:
- Enforce authorization on the server for every endpoint. Hidden UI isn't access control.
- Remove unused endpoints and old API versions instead of leaving them deployed.
- Don't ship internal or staging URLs in production bundles. Use build-time environment separation.
- Disable public source maps in production, or restrict them to authenticated internal access.
- Never embed secrets in frontend code. Anything in the browser is visible to the user.
- Maintain an API inventory. You can't protect endpoints you don't know exist. Compare what your gateway sees against what your frontend references.
- Monitor and rate limit. Log unusual patterns, like one client requesting many nonexistent or never-used paths.
- Review bundles during code review or CI. Simple pipeline checks can flag internal hostnames or key-like strings.
The OWASP API Security Project is an excellent reference for the most common API risks, such as broken object-level authorization and improper inventory management.
🛠️ Tools Worth Exploring
Chrome DevTools — Built into your browser. Use the Network tab for live API calls and Sources for global code search. Start here before anything else.
Katana — A fast crawler with JavaScript parsing. Use it to discover pages and JS files at scale. Example: katana -u https://example.com -jc -d 2.
gau / waybackurls — Pull historical URLs from archives. Useful for finding retired JS files. Example: echo example.com | gau.
httpx — Probes URLs for status, titles, and more. Use it to separate live from dead. Example: httpx -l urls.txt -sc.
LinkFinder — Extracts endpoints from JS using regex. Good for quick wins. Example: python3 linkfinder.py -i app.js -o cli.
jsluice — Parses JavaScript to extract URLs and secrets more intelligently than regex. Check its README for current usage.
Burp Suite — Intercepts and modifies traffic. Use it after discovery for manual testing.
jq — Command-line JSON processor, handy for parsing API responses and tool output.
📦 If You Want to Go Deeper
Hidden API discovery goes well beyond JavaScript: documentation files, mobile apps, wordlist-based discovery, and versioning patterns. I wrote Hidden API Endpoints: The Hacker's Secret Weapon to cover those techniques in a more structured way.
And if you want a broader reference for the tools around this workflow, the Advanced Hacker Pack collects resources for ethical hackers and security researchers.
Both are optional. The workflow in this article is enough to start practicing today.
👋 Keep Exploring With TheHackersLog
TheHackersLog covers cybersecurity, ethical hacking, bug bounty, reconnaissance, web and network security, AI security, OSINT, programming, technology, cybersecurity news, and practical tutorials.
- ✉️ Substack: https://thehackerslog.substack.com/
- ✍️ Medium: https://medium.com/@vipulsonule71
- 💼 LinkedIn: https://www.linkedin.com/company/thehackerslog/
- 🐦 X: https://x.com/VipulSonule
- 🛒 Gumroad: https://thehackerslog.gumroad.com/