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 TLS fingerprinting. 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
});
if (!allowed) {
return res.status(403).json({ error: 'Bot detected' });
}
// Process legitimate request
});

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://ingest.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 { allowed, detection, error?, ruleResult? }, where 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. It denies impersonation by default:

import { webBotAuth } from '@webdecoy/node';
const result = await webdecoy.protect(request, {
rules: [webBotAuth()],
});
// 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 adds well under 5ms and never blocks on the network.

Requires @webdecoy/node 0.5.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', (req, res) => {
const { valid } = 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.