# Bot, VPN and Tor detection

> Detect bots, VPNs, proxies and Tor with one identification: which fields on the event, signal groups and webhooks tell you, and how to act on them.

Last updated: 2026-11-12

Every Fingerly identification doubles as a **bot detection API** and a **VPN detection API**. The same call that returns the visitor ID also reports whether the visitor looks automated and whether the connection comes through Tor, a VPN, a proxy or a hosting provider. You read the answer from your backend, on the stored event or in a webhook.

These are signals for your own policy, not verdicts. Fingerly never blocks anyone on its own, and a VPN is not evidence of fraud by itself, which is why you set what each one is worth.

## Where the answer is

| What you want to know | Where to read it | Values |
| --- | --- | --- |
| Is the visitor a person, an automation or a crawler? | `visitor_kind` on the [event](https://docs.fingerly.io/reference/get-event#response) | `human`, `ai_bot`, `automation` or `search_crawler` |
| Does the connection come through an anonymising network? | `anonymity_network` on the event | `tor`, `vpn`, `proxy` or `hosting`. Omitted for an ordinary connection. |
| Whose network is it? | `asn`, `asn_name` and `country_code` on the event | The autonomous system number, its name and the ISO country |
| Which signals raised the score? | `triggers` on the event | The [signal groups](https://docs.fingerly.io/docs/signals) that fired, heaviest first, each with `weight` and `confidence` |
| How suspicious is the session overall? | `suspect_score` and `suspect_level` | A [score](https://docs.fingerly.io/docs/suspect-score), and `low`, `medium` or `high` |

## Signal groups for bots and anonymised networks

| Group | Key in `triggers` | Meaning |
| --- | --- | --- |
| Bot | `bot` | The client is automated, or says it is a bot. |
| Tor exit node | `tor` | The connection arrives from the Tor network. |
| VPN | `vpn` | The connection arrives over a VPN. |
| Datacenter proxy | `datacenter_proxy` | The address belongs to a proxy or a hosting provider rather than a consumer connection. |
| Residential proxy | `residential_proxy` | Traffic is relayed through someone else's home connection. |
| IP reputation | `ip_reputation` | The address has a recent history of abuse. |

Each group has a default weight per platform, listed in the [signals reference](https://docs.fingerly.io/docs/signals), and you can change them with [risk weights](https://docs.fingerly.io/docs/risk-weights). VPN and residential proxy findings can be weighed by how they were recognised or by confidence. The `bot` group applies to the web. Whether the visitor is a crawler or an AI agent is told by `visitor_kind`, not by a signal group.

## Read it on your server

```ts Node.js
const event = await fingerly.events.get(requestId)

if (event.visitor_kind === 'automation') return refuse()
if (event.anonymity_network === 'tor') return challenge()
if (event.anonymity_network === 'vpn' && event.suspect_level !== 'low') return challenge()
```

An event that is not recorded yet comes back as pending, and an identification that could not be scored has no score, which is not a score of `0`. See [server-side verification](https://docs.fingerly.io/docs/server-side-verification) before you act on a result.

## Get it pushed to you

[`identification.completed`](https://docs.fingerly.io/reference/webhooks/identification-completed) is sent for every identification. [`visitor.suspect`](https://docs.fingerly.io/reference/webhooks/visitor-suspect) is sent when one reaches the `high` level, with every signal that fired in `triggers`, each carrying its `signal`, its `group`, its `weight` and its `confidence`. Subscribe to it to react to automation or Tor traffic without polling. See [webhooks](https://docs.fingerly.io/docs/webhooks).

## What a browser or an app is told

A page or an app can read `suspect_score` and `suspect_level` by default, never the signals, and not at all if you [hide them on the key](https://docs.fingerly.io/docs/api-keys#hide-the-score). The groups and fields above are told only to your backend and the dashboard, because knowing which check tripped teaches a fraudster what to hide.

> **Warning:** Do not decide in the browser. Read the event with a secret key on your server, and check it belongs to the action you are protecting.

## Recipes

- [Credential stuffing](https://docs.fingerly.io/docs/use-cases/credential-stuffing): Refuse automation and count failed logins per device.
- [Sign-up abuse](https://docs.fingerly.io/docs/use-cases/sign-up-abuse): Send automated and farmed sign-ups to review.
- [Signals reference](https://docs.fingerly.io/docs/signals): Every signal group and its default weight.
- [Risk weights](https://docs.fingerly.io/docs/risk-weights): Change what each signal is worth.
