Headless browser detection: what it catches and how to use it
Updated
A headless browser is a real browser engine run by a program with no visible window. Attackers use it with tools such as Puppeteer, Playwright and Selenium to automate sign-ups, scraping and card testing. Each event carries signals.bot.result, which says good, bad or not_detected, and the kinds that were seen. Read it on your server and add a step on risky actions.
What a headless browser is
A headless browser runs the same engine as a normal one but is controlled by code. That makes it good for testing and just as good for abuse, because it runs your page scripts and can look much like a person.
Simple bots that send plain requests are easy to see. Headless browsers are the harder case, which is why a result from the browser itself is useful.
What the bot signal reports
signals.bot.result is good, bad or not_detected. For a bad bot, signals.bot.types names the kind that was seen.
- headless: a browser run without a window.
- webdriver, playwright and selenium: common automation frameworks.
- inhuman_timing and inconsistent_environment: behaviour or a setup that does not look like a person.
- A bot we recognise, such as a search crawler, is reported as good with its operator and category.
Read it on your server
The browser never sees the result. Fetch the event after identify and decide on the server.
if (!/^ev_[0-9A-HJKMNP-TV-Z]{20}$/.test(eventId)) throw new Error('bad eventId'); // fail closed
const res = await fetch(`https://api.fpchecker.com/v1/events/${eventId}`, {
headers: { 'X-Secret-Key': process.env.FPCHECKER_SECRET_KEY },
});
if (!res.ok) throw new Error('event lookup failed'); // fail closed
const event = await res.json();
if (event.eventId !== eventId || !(Date.now() - Date.parse(event.createdAt) <= 5 * 60_000)) throw new Error('stale or reused event'); // wrong or old event
if (!(await markEventUsed(eventId))) throw new Error('stale or reused event'); // your store: true only the first time
const { signals } = event;
if (signals?.bot?.result === 'bad') {
refuseOrChallenge(); // your code
}What to do when it fires
For a bad bot on a sign-up, a claim or a checkout, refuse or ask for a human check. For not_detected, carry on, but remember that no detection is not a guarantee. A determined attacker can try to look ordinary, so combine the result with the network signals and the device history.
If you only have a plain HTTP request and no browser script, the bot check endpoint classifies the request from your backend.
Do not block the good ones
Search engines and other crawlers you want are labelled good, and one that pretends to be a confirmed bot is flagged. Look at the result and the identity before you block automated traffic.
Frequently asked questions
Can a headless browser always be caught?
No. We do not promise to catch every case. Use the result as one input next to network and device evidence.
Where is the result?
On each event at signals.bot.result, through the events API, the console and webhooks. The browser never receives it.
Is every automated browser bad?
No. Your own tests use them too. The result tells you what was seen; you decide what it means for your page.
Does it change the allow or deny answer?
No. Signals are evidence for your own rules. The decision API works as before.
Try FingerPrint Checker free
Every new account gets 2,000 free checks. No card required to start.
Create a free account