Getting started
Five minutes from nothing to a verified identification.
1. Get your keys
Sign in, open API keys. Every organization starts with two:
| Key | Prefix | Where it goes |
|---|---|---|
| Public | pk_live_… |
Browsers and mobile apps. It ships inside your client and is not a secret. |
| Secret | sk_live_… |
Your servers only. It reads identification data. |
The secret key is shown once, at creation. Only a hash of it is stored, so it cannot be displayed again. If you lose one, revoke it and make another.
Because the public key is not a secret, list your sites under Settings →
Allowed origins before you go live: with none listed, any site may identify
with your key, and every identification it makes is billed to you and recorded
in your data. Browsers send their origin with every identification; one not on
the list is refused (403 OriginNotAllowed). The traffic page's setup checklist
says so while the list is empty.
2. Identify a visitor
<script src="https://cdn.antibot.dev/agent/v1/antibot.js"></script>
<script>
Antibot.load({ apiKey: "pk_live_…" })
.then((agent) => agent.get())
.then((result) => {
console.log(result.visitorId, result.confidence.score)
})
</script>
Or from a bundler:
npm install @antibot/browser
import { load } from "@antibot/browser"
const agent = await load({ apiKey: "pk_live_…" })
const result = await agent.get()
React, Vue, Angular and Svelte have their own packages — see Browser agent.
3. Send the request id to your backend
The browser result carries a requestId. Send that to your server along with
whatever action the user is taking.
// Identified as the action happens: a result the agent kept from the page's
// load (half an hour by default) would be too old for the check in step 4.
const { requestId } = await agent.get({ ignoreCache: true })
await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ requestId, cart }),
})
4. Re-read it server-side, and decide there
This is the step that matters. The browser's copy of the result can be edited by whoever owns the browser; the copy you fetch with your secret key cannot.
const res = await fetch(
`https://api.antibot.dev/api/v4/events/${requestId}`,
{ headers: { Authorization: `Bearer ${process.env.ANTIBOT_SECRET_KEY}` } }
)
if (!res.ok) throw new Error("unknown event")
const event = await res.json()
// One identification is one pass. Its id, kept and sent again — by a script
// that had a real browser identify once — reads as that browser: hold it to
// moments ago, and to once.
if (Date.now() - event.timestamp > 2 * 60_000) return deny()
if (!(await claimOnce(requestId))) return deny() // e.g. Redis SET NX with a TTL
if (event.products.bot.result === "bad") return deny()
if (event.suspectScore >= 70) return challenge()
if (event.confidence.score < 0.9) return challenge()
return allow(event.visitorId)
There are server SDKs for Node, Python, Go, Java, PHP and .NET if you would rather not hand-roll the HTTP.
5. Check your work
Open Events in the dashboard. Your identification is at the top, with every signal that was computed for it. Click through to see the full payload.
Test what your code does with a refusal. You do not need a bot to see a block or a challenge: a rule in your development environment forces one whenever a request says so. Rules belong to their environment, so production never reads them.
# Requests without the agent (an edge, a guard, POST /api/v4/assess): any carrying the header
curl -X POST "https://api.antibot.dev/api/management/v1/rules?environment=development" \
-H "Authorization: Bearer $ANTIBOT_MANAGEMENT_KEY" -H "Content-Type: application/json" \
-d '{"name":"test: block on request","scope":"assessment","condition":"headers.x-antibot-test == \"block\"","action":"block","priority":0}'
# Identifications: any tagged so
curl -X POST "https://api.antibot.dev/api/management/v1/rules?environment=development" \
-H "Authorization: Bearer $ANTIBOT_MANAGEMENT_KEY" -H "Content-Type: application/json" \
-d '{"name":"test: block on tag","scope":"identification","condition":"tag.test == \"block\"","action":"block","priority":0}'
Then send a request with X-Antibot-Test: block, or identify with
agent.get({ tag: { test: "block" } }), and your code gets the block; a second
pair with challenge gives you a challenge. Only ever force a refusal this way:
a rule that allows on a header or a tag the client sets is a way past every
check for anyone who reads your docs.
What to do next
- Do not block on the client. LayerSift never blocks your visitor and neither should the browser — only your server knows what the request was trying to do.
- Read Smart Signals to understand what you are looking at.
suspectScoreis a reasonable first cut, but the individual signals tell you why, and that is what you want in a support conversation. - Consider rules so thresholds live in the product instead of in every service you own.
- Before you go live, set up first-party serving, the CDN proxy if you can. Content blockers drop requests to known device-intelligence domains, which silently removes identification from exactly the traffic most worth identifying; and Safari restricts what it knows as fingerprinting scripts, keeps a custom subdomain's cookies a week, and deletes what scripts wrote after seven days away — behind the CDN proxy, a returning Safari keeps its visitor hint.
Enforcing: where the decision runs
Identifying tells you what a visitor is. To refuse a bot, challenge it, or slow a credential-stuffing run, something has to act on each request. Pick the place closest to the edge that you already run. All of these places make the same decisions by the same rules:
- A refusal is a 403 with a reference the visitor can quote.
- A challenge is the interstitial that identifies the browser and reloads.
- A sign-in's verdict reaches your code, so it can ask for a second factor.
| What is in front of your site | Use | |
|---|---|---|
| Cloudflare, Fastly, Azure Front Door/Functions, CloudFront | The edge adapter for it | Sees the TLS handshake, and on Cloudflare, Fastly and Azure places the agent on every page |
| Your own nginx, Traefik, Caddy or Envoy — on servers or in Kubernetes | The forward-auth service | The same edge, asked by your proxy before each request |
| WordPress, with or without WooCommerce | The plugin | Sign-ins, comments, XML-RPC, REST and every WooCommerce checkout, with nothing to configure but keys |
| Laravel | The package | A middleware: sign-ins, with the starter kits' forms covered by default, and the address Laravel reads through your trusted proxies |
| Symfony | The bundle | An event subscriber: sign-ins read from form_login's redirects, and the address Symfony reads through framework.trusted_proxies |
| Spring Boot | The starter | Registers the client, guard and servlet filter from antibot.* properties, ordered ahead of Spring Security; the address Spring reads through server.forward-headers-strategy |
| Nothing: your application answers directly | The middleware for your stack | Express, Fastify, NestJS (on either adapter) and node:http; Next.js, SvelteKit, Remix and Hono; Flask, Django (antibot.django), FastAPI; Go net/http; ASP.NET Core; plain PHP and PSR-15; Jakarta Servlet and Spring Boot; Rails, Sinatra and any Rack app |
Whichever you pick, configure the forms that matter — the sign-in, a gift card's redemption, a comment box — by path and field. Their field is read from the body and the query string as your framework reads it, every way it could be spelled, and a body that cannot be read is assessed as such rather than let through. Point the address at the header your CDN or load balancer sets; without one, the connection's address is used, and a request with none is still assessed.
With none of these, the assess call is the same decision as one HTTP request from your code.
A note on freshness
agent.get() caches for 30 minutes by default. That is the right trade for
personalisation and the wrong one for a fraud decision: a cached result carries
the bot and VPN state from when it was made. Pass ignoreCache: true at any
decision point.
const result = await agent.get({ ignoreCache: true })