Skip to content

Node.js SDK

The WebDecoy Node SDK (@webdecoy/node) brings bot detection into your own backend: an in-process detection engine, deterministic tripwire rules, a self-hosted proof-of-work captcha, and JA3/JA4 fingerprint matching when your proxy supplies TLS details (the SDK does not read them from the socket; pass them as tls_info in the request metadata). Framework packages wrap it for Express, Fastify, and Next.js, and the Browser Client is its client-side counterpart.

Create a free account → · Compare all install methods

For the client-and-server architecture and package overview, see the Bot Detection SDK product page.

Terminal window
npm install @webdecoy/node

The core method is protect(). Pass request metadata, get a decision:

import { WebDecoy } from '@webdecoy/node';
const webdecoy = new WebDecoy({
apiKey: process.env.WEBDECOY_API_KEY
});
app.post('/api/submit', async (req, res) => {
const { allowed, detection } = await webdecoy.protect({
method: req.method,
path: req.path,
ip: req.ip, // required
user_agent: req.get('user-agent'),
headers: req.headers,
timestamp: Date.now()
});
if (!allowed) {
return res.status(403).json({ error: 'Bot detected' });
}
// Process legitimate request
});

The core protect() call has no monitor mode of its own: it returns a decision and your code chooses what to do with it. The framework packages (Express, Fastify, Next.js) default to monitor mode and only block when you set mode: 'enforce'.

Send one request with the reserved test User-Agent. It always fires a detection through the real pipeline, works from localhost before you deploy, and shows up labeled Test in the dashboard (excluded from stats and billing):

Terminal window
curl -A "WebDecoy-Test/1.0" http://localhost:3000/

Within a few seconds a detection with the Test category chip appears on the Detections page. If nothing appears, the request never reached the SDK: check that protect() runs for that route and that WEBDECOY_API_KEY is set (local-only mode has nowhere to report to).

const webdecoy = new WebDecoy({
apiKey: 'sk_live_...', // optional, omit for local-only mode
apiUrl: 'https://in.webdecoy.com', // default
enableTLSFingerprinting: true, // default
threatScoreThreshold: 70, // block above this score (default: 80)
timeout: 5000, // request timeout (ms)
debug: false
});

Per-request options on protect(metadata, options):

Option Description
threshold Custom threat score threshold for this request
skipLocalAnalysis Only use server-side detection
metadata Extra data to include in the detection

protect() resolves to a decision object. Its main fields are conclusion (ALLOW, DENY, CHALLENGE, or ERROR), allowed, results (each rule’s outcome), detection, error?, ruleResult?, and agent?, plus the helpers isAllowed(), isDenied(), isChallenged(), isErrored() and deniedBy(rule). detection is:

Field Meaning
decision allow, challenge, or block
confidence Confidence score, 0–100
threat_level MINIMAL, LOW, MEDIUM, HIGH, or CRITICAL
bot_detected Whether a bot was detected
bot_type Type of bot, when detected
detection_id ID for cross-referencing the Detections screen
rule_enforced Whether a response rule fired

The SDK fails open: if the detection call errors, the request is allowed and the error is surfaced on error.

Rules evaluate locally, before any network call. A tripwire turns a hidden decoy link into a deterministic block:

import { WebDecoy, tripwire, honeytoken } from '@webdecoy/node';
// Generate a hidden decoy link + its secret path
const trap = honeytoken(); // { path, linkHtml }
const webdecoy = new WebDecoy({
rules: [tripwire({ paths: [trap.path] })]
});
// Inject trap.linkHtml into your pages. It's off-screen,
// aria-hidden, and nofollow. Only scrapers ever request it.

The rules engine runs entirely inside the SDK. Rate limiting, filtering, and tripwires work locally even with no apiKey configured:

import { WebDecoy, rateLimit, filter, tripwire, bots } from '@webdecoy/node';
const wd = new WebDecoy({
rules: [
// Fixed or sliding-window rate limiting, keyed however you like.
// `window` is in seconds.
rateLimit({ max: 60, window: 60, algorithm: 'sliding', action: 'THROTTLE' }),
// Filter expression language over request + enrichment data
filter({ expression: 'ip.abuse_score > 80 or ip.tor', action: 'DENY' }),
// Honeypot paths that only scrapers ever request
tripwire({ includeDefaults: true, action: 'DENY' }),
// Declared AI crawlers, by category
bots({ categories: ['training_crawler'], action: 'DENY' }),
],
});
  • Actions are 'DENY' or 'THROTTLE'.
  • Every rule supports dryRun: true to evaluate and report without blocking.
  • Rule violations are batched to POST /api/v1/sdk/violations/batch (when an API key is set) so they appear in your dashboard.
  • Filter expressions referencing IP intelligence use GET /api/v1/sdk/ip/{address}/enrichment (cached in-SDK for an hour).

bots() matches the agent registry that also powers AI scraper scoring, so the category names here are the same ones you see in your dashboard.

// Keep AI models out of your content, keep your search ranking.
bots({ categories: ['training_crawler'] })
// Everything AI, minus the one you want referral traffic from.
bots({ ai: true, allow: ['perplexitybot'] })
// A specific operator, slowed rather than blocked.
bots({ agents: ['gptbot', 'ClaudeBot'], action: 'THROTTLE' })

ai: true covers training_crawler, ai_search_crawler, ai_agent and ai_assistant. It deliberately does not cover search_crawler, because blocking Googlebot would deindex your site.

Namespace Fields
ip vpn, proxy, tor, relay, hosting, country, country_name, city, timezone, asn, asn_org, abuse_score, total_reports, is_high_risk
req path, method, ip, user_agent, header("name")
bot known, ai, category, name, id, organization, score, respects_robots
edge present, class, clearance, verified, crawler, script, browser
filter({ expression: 'bot.category == "training_crawler"' })
filter({ expression: 'bot.ai and not bot.respects_robots' })
filter({ expression: 'bot.category in ["generic_scraper", "headless_browser"]' })

Comparisons against a missing value are always false, so a rule cannot accidentally fire on data that was never populated.

detectBot() verifies Web Bot Auth signatures locally: no network call on the warm path, and no API key required. Pass a WHATWG Request or a plain { method, url, headers }:

const verdict = await webdecoy.detectBot(request);
// verdict.status: 'verified' | 'impersonation' | 'claimed' | 'none'
Status Meaning
verified Signature checked out against the agent’s published keys
impersonation It claimed to be a signing agent and the proof failed
claimed Says who it is; nothing corroborates it
none No agent claim at all

To act on it, add the rule when you create the client. It denies impersonation by default:

import { WebDecoy, webBotAuth } from '@webdecoy/node';
const webdecoy = new WebDecoy({
apiKey: process.env.WEBDECOY_API_KEY,
rules: [webBotAuth()],
});
const result = await webdecoy.protect(metadata);
// result.agent carries the verdict either way

Two design points worth knowing:

  • Key directories are curated, never fetched from the request. A signature naming an arbitrary Signature-Agent URL will not send the SDK to that host, so a malicious agent cannot use verification as an SSRF primitive.
  • Directories are cached with stale-while-revalidate, so a warm verification never blocks on the network.

Requires @webdecoy/node 0.11.0 or later, and runs in edge runtimes as well as Node.

The SDK ships a proof-of-work captcha you run on your own domain, with no third-party script:

import { Captcha } from '@webdecoy/node';
const captcha = new Captcha({ secret: process.env.WEBDECOY_SECRET });
// Verify a token submitted by the browser widget
app.post('/login', async (req, res) => {
const { valid } = await captcha.verifyToken(req.body.webdecoy_token, req.ip);
if (!valid) {
return res.status(403).json({ error: 'captcha failed' });
}
// proceed
});

createCaptchaEndpoints() generates the challenge/verify HTTP handlers; the framework packages mount them for you, and the Browser Client provides the widget that solves them.

Full type definitions ship with the package: WebDecoyConfig, RequestMetadata, ProtectResult, SDKDetectionResponse, and the rule and captcha types are all exported.