Which BrowserLeaks API Can You Actually Call From Code?
There is one documented REST interface: the browserleaks api at api.browserleaks.io/v1, which takes a Bearer token and accepts JSON posts for fingerprints, IP reputation, DNS leaks and WebRTC leaks. BrowserLeaks.com, the free test site, has no public API at all. You collect signals in a browser you control, post them, and get back a hash, per-component uniqueness and a privacy score you can store and compare over time.

📌 TL;DR Executive Summary
- Core Takeaway: Five documented calls — auth, fingerprint, IP detect, DNS leak and WebRTC leak — all under https://api.browserleaks.io/v1, authenticated with a token from POST /v1/auth/token.
- Key Risk/Challenge: The reference lives in a third-party GitHub repository rather than on the site, and the free tier allows 100 requests a day, so a careless loop burns the quota in an afternoon.
- Recommended Solution: One small client with re-auth on 401, backoff on 429, raw JSON logged, and one saved document per profile per day so results stay comparable.
Two Sites Share a Name — Only One Has an API
BrowserLeaks.com is the free test suite most people know: IP and headers, WebRTC, DNS, Canvas, WebGL, fonts, geolocation, TLS with JA3 and JA4. It runs in your browser, needs no account, and states that it does not store fingerprints or set non-essential cookies. You can read it by hand all day, but there is no endpoint to call.
BrowserLeaks.io is a separate service. Its homepage says fingerprint signals are computed locally in the browser while IP and WebRTC checks contact its API. That split explains why an API exists at all: the network-side checks need a server that sees your request. If you want the raw material those checks measure, the breakdown of every telemetry signal in your browser is a useful primer before you post JSON.
The reference is a GitHub file, not a vendor page
The API reference is a Markdown file in the residentialproxies repository, marked v1.0 and last updated on 15 November 2025. It describes RESTful JSON, Bearer authentication, rate limiting, and three paid-tier features: webhook callbacks, batch checks and history queries. Read it as the best available description, because the site itself does not publish a mirrored contract.
The same repository’s README tells a second story. It lists IP, DNS and WebRTC leak tests plus the privacy score dashboard as done, and Canvas, WebGL, Audio, Fonts, Timezone and full browser fingerprinting as planned. A separate README marks the live demo and the API documentation page as Coming Soon. Build accordingly: log raw bodies, validate every field, and never assume a component you sent comes back populated.
Prerequisites Before the First Request
Nothing here needs a vendor SDK. The service is JSON over HTTPS, so a standard HTTP client is enough, and the helpers on GitHub are hobby projects rather than supported libraries.
- Python 3.9 or newer with
requests(pip install requests). Node 18+ with the built-infetchworks equally well. - An email address to exchange for a token, kept in an environment variable rather than in the script.
- The signals for a fingerprint run: user agent, screen metrics, timezone, languages, plugins, font list, a canvas data URL, WebGL vendor and renderer, and an audio hash.
- The quota in mind: 100 requests per day free, 10,000 per month on Starter, 100,000 per month on Pro, unlimited on Enterprise, per the docs table. No prices are attached to those tiers anywhere in the reference.
- A storage decision: browserleaks.io caches IP results by address and keeps server request logs for up to 7 days, so anything you need long-term belongs in your own database.
One ecosystem warning: the wiz64/browserleaks-api repository from 2024 is a canvas helper, not an official client. The monthly prices scattered through project READMEs ($9.99, $29, $99) contradict one another and appear nowhere in the API reference, so check your own account page for what you are actually on.
Endpoint map
| Method and path | What it does | What comes back |
|---|---|---|
| POST /v1/auth/token | Exchanges an email for a Bearer token | Token to send in the Authorization header |
| POST /v1/fingerprint | Scores a set of browser signals (30+ detection items) | Hash, confidence, uniqueness, privacyScore with risk level and a four-part breakdown |
| POST /v1/detect/ip | Classifies an IP address | Geo, ISP/ASN, isProxy, isVPN, isDatacenter, isTor, reputation score, blacklist flag |
| POST /v1/detect/dns-leak | Evaluates resolver data against the connection | isLeak, leakType, servers list with country and ISP per server |
| POST /v1/detect/webrtc-leak | Checks whether a real IP appears in ICE candidates | Leak verdict for the publicIp, localIPs and candidates you submit |
Every path sits under /v1. Set the base once and append only the segment; calling https://api.browserleaks.io/v1/v1/detect/ip is the most common mistake in first drafts.
Step 1 — Exchange an Email for a Bearer Token
The token call is the only request you make without a header. Post an email, read the token, attach it to everything else as Authorization: Bearer <token>, and keep it in memory or an environment variable.
import os
import requests
BASE = "https://api.browserleaks.io/v1"
TIMEOUT = 30
def get_token(email: str) -> str:
resp = requests.post(f"{BASE}/auth/token", json={"email": email}, timeout=TIMEOUT)
resp.raise_for_status()
data = resp.json()
# The docs show a token but do not pin the key name; probe a few and fail loudly.
token = data.get("token") or data.get("access_token") or (data.get("data") or {}).get("token")
if not token:
raise RuntimeError(f"No token field in response: {data}")
return token
if __name__ == "__main__":
token = get_token(os.environ["BROWSERLEAKS_EMAIL"])
print("token acquired:", token[:6] + "...")
The docs show a token in the response but do not pin the key name, which is why the helper probes three plausible keys before failing. Print the raw JSON on your first run, note the real field, then simplify the function. Do not share one token across parallel workers on the free tier — 100 requests a day disappears fast when two processes race, and you will spend the afternoon reading 429s.
Step 2 — Classify an IP, Proxy Exit or Datacenter Range
The IP endpoint answers the question that decides everything else: does the address you browse from look like a residential connection, or does it announce itself as a proxy, a VPN, a hosting range or a Tor exit? The response keeps those four booleans separate and adds a reputation score and a blacklist flag.
def headers_for(token: str) -> dict:
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
def check_ip(token: str, address: str) -> dict:
resp = requests.post(f"{BASE}/detect/ip", json={"ip": address},
headers=headers_for(token), timeout=TIMEOUT)
if resp.status_code == 429:
raise RuntimeError(f"Rate limited, retry after {resp.headers.get('Retry-After', 'unknown')}s")
resp.raise_for_status()
return resp.json()
report = check_ip(token, "203.0.113.10") # TEST-NET-3, safe to hard-code in examples
print(report.get("country"), report.get("isp"), report.get("isDatacenter"))
Read all five values together. isProxy, isVPN, isDatacenter and isTor are independent signals, so a residential address can still carry a weak reputation score if it has history. Log them per run and you can spot drift in a pool over weeks instead of guessing from one bad day.
The geo block is where consistency checking starts. Compare the returned country with the timezone and language list in the fingerprint you posted. A profile reporting an IP in Helsinki, a timezone of Asia/Shanghai and a language list of zh-CN describes three different machines, and any site that correlates those fields will notice. Fix the profile settings, not the report.
Step 3 — Post a Fingerprint and Read the Score
The fingerprint call takes what you gathered and returns a hash plus a score. The documented inputs are the user agent, a canvas data URL, WebGL vendor and renderer, screen metrics, timezone, languages, plugins, fonts and an audio hash, and the endpoint is described as covering more than 30 detection items.
SIGNALS = {
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"canvas": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...", # paste the real data URL
"webgl": {"vendor": "Google Inc. (Apple)",
"renderer": "ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version)"},
"screen": {"width": 1512, "height": 982, "colorDepth": 30, "pixelRatio": 2},
"timezone": "Europe/Helsinki",
"languages": ["en-GB", "en"],
"plugins": [],
"fonts": ["Arial", "Helvetica", "Menlo", "Times New Roman"],
"audioHash": "4c0a1e9b8f2d7c3a",
}
def check_fingerprint(token: str, signals: dict) -> dict:
resp = requests.post(f"{BASE}/fingerprint", json=signals,
headers=headers_for(token), timeout=60)
resp.raise_for_status()
return resp.json()
Reading the response is the step people skip. You get a hash identifying the combination, a confidence value, an overall uniqueness figure, and a privacyScore object with a total, a risk level and a four-part breakdown. Each component returns its own uniqueness value and a boolean consistency field that reports whether the submitted value agrees with the rest of the fingerprint. The sample shows canvas at 0.89 and WebGL at 0.92 on a GeForce RTX 3080.
def summarise(result: dict) -> None:
score = result.get("privacyScore", {})
print("hash:", result.get("hash"), "| confidence:", result.get("confidence"))
print("uniqueness:", result.get("uniqueness"),
"| privacy:", score.get("total"), score.get("riskLevel"))
for name, item in (result.get("components") or {}).items():
if isinstance(item, dict):
flags = {k: v for k, v in item.items() if isinstance(v, bool)}
print(f" {name:12} uniqueness={item.get('uniqueness')} flags={flags}")
Two readings follow. High uniqueness on one component means that component alone narrows you down, so it deserves attention when you standardise a profile. A raised consistency flag says the submitted value does not agree with the rest of the fingerprint, which is a signal to align the profile’s settings rather than replacing the data you sent.
If you have not chosen a scoring service yet, weigh the options for a browser fingerprinting API before writing a client you may throw away. Field names differ between vendors, and the nested component objects here are only partly illustrated, so print the raw body on your first successful call and adapt the parser to what you actually receive.
Step 4 — Submit DNS and WebRTC Leak Evidence
These two calls do not open a browser for you. You submit what your session observed and the service evaluates it, which makes them the fastest to wire up and the easiest to get wrong. Note that the reference documents the response shapes more fully than the request bodies, so confirm the payload keys against your first accepted call.
def check_dns_leak(token: str, resolvers: list) -> dict:
resp = requests.post(f"{BASE}/detect/dns-leak", json={"servers": resolvers},
headers=headers_for(token), timeout=TIMEOUT)
resp.raise_for_status()
return resp.json()
def check_webrtc_leak(token: str, public_ip: str, local_ips: list, candidates: list) -> dict:
payload = {"publicIp": public_ip, "localIPs": local_ips, "candidates": candidates}
resp = requests.post(f"{BASE}/detect/webrtc-leak", json=payload,
headers=headers_for(token), timeout=TIMEOUT)
resp.raise_for_status()
return resp.json()
The DNS response returns isLeak, a leakType and a servers list where each entry carries a country, an ISP and risk entries. The check that matters is whether those countries match the IP you browse from. An exit node in Finland with resolvers answering from three other countries is a split route, and any test that compares resolver geography to connection geography will show it.
The WebRTC payload is the most explicit: a public IP, a list of local IPs, and the ICE candidate objects themselves. Sending candidates rather than a summary lets the service inspect host candidates for private ranges and srflx candidates for addresses that should not appear. If you want the neighbouring browser surfaces that expose state without a prompt, the Permissions API fingerprinting signals cover them.
Step 5 — Stay Inside the Rate Limits
The free tier allows 100 requests per day, which is 25 full four-call runs. Paid tiers raise the ceiling, and the reference marks batch checks, webhook callbacks and history queries as Pro and above. Nothing in the documentation promises those features are live today, so build the sequential path first and switch batching on after testing.
| Tier (as documented) | Allowance | Roughly what that buys |
|---|---|---|
| Free | 100 requests per day | 25 four-call runs — a smoke test, not a sweep |
| Starter | 10,000 per month | About 2,500 runs a month, roughly 80 a day |
| Pro | 100,000 per month | About 25,000 runs a month, plus batch and webhooks |
| Enterprise | Unlimited | Scheduled runs across a fleet, history kept on your side |
Whatever tier you are on, handle 429 explicitly. Honour a Retry-After header when one arrives; otherwise use exponential backoff capped at about a minute so a queue keeps moving without hammering the endpoint.
import time
def post_with_retry(token: str, path: str, payload: dict, attempts: int = 4) -> dict:
for attempt in range(attempts):
resp = requests.post(f"{BASE}{path}", json=payload,
headers=headers_for(token), timeout=TIMEOUT)
if resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", 2 ** (attempt + 1)))
time.sleep(min(wait, 60))
continue
resp.raise_for_status()
return resp.json()
raise RuntimeError(f"{path} still rate limited after {attempts} attempts")
Step 6 — One Saved Document per Profile
Pull the four calls into a single function so a row in your database always has an owner. Run it once per profile, write the result to disk, and keep the request bodies next to the responses.
import json
from datetime import date
def check_profile(token: str, label: str, address: str, signals: dict,
resolvers: list, candidates: list) -> dict:
document = {
"profile": label,
"checked": date.today().isoformat(),
"ip": post_with_retry(token, "/detect/ip", {"ip": address}),
"fingerprint": post_with_retry(token, "/fingerprint", signals),
"dns": post_with_retry(token, "/detect/dns-leak", {"servers": resolvers}),
"webrtc": post_with_retry(token, "/detect/webrtc-leak", {
"publicIp": address,
"localIPs": ["192.168.1.24"],
"candidates": candidates,
}),
}
with open(f"leaks-{label}-{document['checked']}.json", "w") as fh:
json.dump(document, fh, indent=2)
return document
Four calls per profile is also your quota arithmetic: divide your allowance by four before you schedule anything. The reference is descriptive rather than exhaustive, so the documents you save become your real schema documentation. When a field disappears after a service update, you get a diff instead of an argument.
Common Errors and How to Fix Them
Most failures in the first hour are structural rather than subtle. Work down this list before you change your payload.
- 404 on every call. You doubled the version segment. The base already ends in
/v1, so append/detect/ip, not/v1/detect/ip. - 401 right after a successful token call. The header is
Authorization: Bearer <token>, capital B, single space. A missing space or a lowercase scheme fails quietly in some clients. - Rejected field names. Component keys are camelCase in the samples (
userAgent,publicIp,localIPs). Snake-case versions are not documented. - 429 during a loop. You are at the free cap, or another process shares the token. Log the remaining quota instead of retrying blindly.
- Missing components in the response. Not every component is scored yet. The README still lists Canvas, WebGL, Audio and Fonts as planned work, so an absent component is expected behaviour, not a parser bug.
- Timezone rejected. Send an IANA name such as
Europe/Helsinki, not an offset likeUTC+2. - Score looks wrong in your dashboard. The sample response uses Chinese risk labels and an Asia/Shanghai timezone. Map the numeric fields yourself and never pattern-match on label text.
If you plan to call more than one fingerprint service, keep a single adapter layer per vendor. The differences are big enough that comparing fingerprinting APIs side by side before you wire a second one saves a refactor later.
🏆 Send.win Verdict
Calling the checks from code is the easy half. The hard half is making sure the signals you send belong to one coherent browser, which is what a profile manager is for: each Send.win profile keeps its own cookies, storage, sessions, consistent fingerprint settings and proxy, and timezone, locale, WebRTC and geolocation follow the proxy’s exit IP automatically, so profiles do not share one browser identity. Run the same four checks inside each profile and you get comparable rows instead of a pile of numbers with no owner. The desktop app runs on Windows, macOS and Linux; the cloud browser runs profiles on EU servers in Helsinki with nothing to install. Plan on doing the checks by hand: open the profile, run the calls yourself, and file the result next to the profile name. Send.win is a hands-on browser for people who manage accounts, so treat each run as a manual step rather than a pipeline stage.
See Send.win plans — Pro is $19/mo with 150 saved profiles and 5 GB of proxy bandwidth, or start with the free cloud demo, a three-minute preview that needs no signup.
Frequently Asked Questions
Is there an official BrowserLeaks API?
BrowserLeaks.com has no public API; it is a free set of browser-based test pages. A separate service, browserleaks.io, documents a REST API at api.browserleaks.io/v1. That reference currently lives in a third-party GitHub repository rather than on the site itself, so verify details against your own account before you depend on them.
How do I authenticate to api.browserleaks.io?
Post your email to /v1/auth/token and read the token from the JSON response. Send it on every later call as an Authorization header with the Bearer scheme, and re-authenticate on a 401 rather than retrying with the same token.
What are the browserleaks.io rate limits?
The documented table lists 100 requests per day on the free tier, 10,000 per month on Starter, 100,000 per month on Pro and unlimited on Enterprise. Prices and tier names in project READMEs contradict each other, so treat your account page as the source of truth.
Which signals does the fingerprint endpoint accept?
The documented components are the user agent, a canvas data URL, WebGL vendor and renderer, screen metrics, timezone, languages, plugins, fonts and an audio hash. The reference describes the endpoint as covering more than 30 detection items and returns a hash plus per-component uniqueness and a boolean consistency field.
Can I batch fingerprint checks and receive webhooks?
Both appear in the API reference as Pro-tier features, alongside history queries. The project roadmap still marks parts of the fingerprint stack as planned, so test batch and webhook behaviour on your plan before building a pipeline that depends on it.
How do I detect a WebRTC leak from code?
Collect the ICE candidates and local IPs your session produces, then post them with your public IP to /v1/detect/webrtc-leak. The response tells you whether an address appears that should not be there, which is the signal that traffic is not fully routed through the connection you intended.
Does BrowserLeaks store my results?
browserleaks.io states that it caches IP results by address and keeps server request logs for up to 7 days. BrowserLeaks.com says separately that it does not store fingerprints or set non-essential cookies. Neither is a substitute for your own record keeping.
How do I test a proxy’s IP reputation from a script?
Call /v1/detect/ip with the exit address and read isProxy, isVPN, isDatacenter, isTor, the reputation score and the blacklist flag together. Then compare the returned country with the timezone and languages in your fingerprint payload, because a mismatch there tells you more than any single flag.
How Send.win Helps With Browserleaks Api
Send.win is a browser profile manager for professionals and teams: every account gets its own isolated browser profile.
- Isolated profiles – separate cookies, storage and sessions per profile
- Fingerprint protection – each profile keeps its own consistent browser fingerprint, so profiles never share one browser identity
- Desktop app + cloud browser – native app for Windows, macOS and Linux, or run profiles in the cloud with no install
- Residential proxies included – on Pro and Team, with timezone and locale matched to the proxy
- Team sharing – share a signed-in profile with a teammate without sharing the password
Try the free cloud demo — no install — or download the desktop app. Paid plans start at $19/month, and your first card purchase is covered by a 7-day money-back guarantee (see pricing).