Skip to content

Browser Client

@webdecoy/client is the client-side half of WebDecoy’s SDK story. It collects behavioral, environmental, and temporal signals in the browser, solves the proof-of-work challenge, and submits to your server: the self-hosted captcha endpoints mounted by the Express, Fastify, or Next.js packages. No third-party captcha script ever watches your visitors.

Prefer zero code? The Detection Script is a one-tag install with the full detection engine. The browser client is for teams who want the widget and signals under their own build.

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

Terminal window
npm install @webdecoy/client
import { WebDecoyCaptcha } from '@webdecoy/client';
WebDecoyCaptcha.configure({
serverUrl: 'https://your-server.com'
});
// Checkbox widget
WebDecoyCaptcha.render('captcha-box', {
siteKey: 'YOUR_SITE_KEY', // any label; your own captcha endpoints receive it
callback: (token) => submit(token)
});
// Or run a zero-click session in the background
WebDecoyCaptcha.invisible({ siteKey: 'YOUR_SITE_KEY' });
// Or score on demand, when you choose
const result = await WebDecoyCaptcha.execute('YOUR_SITE_KEY', { action: 'login' });

Three modes:

Mode Behavior
Checkbox Renders the familiar widget in a container element
Invisible Collects signals and solves in the background, with no click
On-demand WebDecoyCaptcha.execute() runs the flow programmatically when you choose

The token the widget produces goes to your backend, which verifies it with await captcha.verifyToken(token, ip) from the Node SDK.

The widget gathers the signals the detection engine scores: behavioral (mouse and interaction patterns), environmental (browser and device characteristics), and temporal (timing consistency). It binds them into the proof-of-work, so a bot can’t skip the collection and still solve the challenge.

The browser check that session clearance builds on is this same prove-you’re-a-real-browser flow: passing it is how a session earns clearance on enforced routes.

For most sites you don’t need the captcha at all: silent minting earns the token during normal browsing. Either drop in the standalone script with your site key (for clearance, the site key is your publishable organization ID from the Enforcement page):

<script async src="https://cdn.jsdelivr.net/npm/@webdecoy/client/dist/webdecoy.global.js"
data-site-key="YOUR_SITE_KEY"></script>

or call it programmatically:

import { startClearance } from '@webdecoy/client';
startClearance({ siteKey: 'YOUR_SITE_KEY' });

Minting posts the site key plus a device fingerprint to POST /api/v1/clearance and sets a Secure, SameSite Lax, host-only wd_clearance cookie. Initial work runs at browser idle. A fresh cookie is reused and renewal continues before expiry, including when the page starts with an existing cookie. Use @webdecoy/client 0.18.5 or later for this renewal fix.

Repeated startClearance() calls share one lifecycle on the page. The client reuses its collected fingerprint and checks the cookie periodically, so renewal by another tab can postpone its own request. Returning to a visible page checks clearance again. A mint request times out after 10 seconds; transient failures retry with backoff from one minute up to five minutes. An explicit server refusal stops automatic attempts on that page. Token expiry read by the client is only a scheduling hint; the server and edge validator still decide whether clearance is valid.

Minting does not solve proof-of-work or block page rendering. After the visitor interacts, the client can upgrade the token once with an aggregate interaction summary (pass behavior: false to turn that off). Accepted aggregates remain on later renewal requests, and the server evaluates each request again.

The client does not intercept or retry your application’s fetch/XHR requests. An API request challenged by the edge returns a JSON 403; the explicit helper below can recover safe reads through your application’s clearance flow. Clearance remains first-party: this cookie does not travel to shared-domain decoys or a different customer subdomain.

With @webdecoy/client 0.19.0 or later and an updated edge validator, opt in for the API reads your application wants to recover:

import { createClearanceFetch } from '@webdecoy/client';
import { earnClearance } from './clearance-flow'; // your application's flow
const fetchProtected = createClearanceFetch({
origin: location.origin,
timeoutMs: 10_000,
recover: ({ origin, challenge, signal }) =>
earnClearance({ origin, reason: challenge.reason, signal }),
onEvent: event => console.debug(event.type, event.status),
});
const controller = new AbortController();
const response = await fetchProtected('/api/catalog', {
credentials: 'same-origin',
signal: controller.signal, // your application's AbortController
});
// Handle response.ok/status/body just as with native fetch.

earnClearance is application code, not an SDK export. It must earn and install the host’s wd_clearance cookie through your configured issuance/verification flow and return true only when ready to retry. startClearance() returns void; awaiting it does not wait for a token. A self-hosted captcha widget token is not a clearance cookie. A stronger-grade response needs a flow that earns the route’s required grade; simply replacing a token cannot grant that grade.

The helper retries GET/HEAD once after a recognized recoverable challenge. Concurrent calls through one adapter share one recovery callback for its configured origin; that callback receives the first challenge’s reason. Use one adapter for each origin/recovery flow. The edge evaluates every retry again. POST/PATCH/PUT/DELETE and other methods pass through once with their original body. There is no mutation replay opt-in and no XHR interception. The standalone script exposes the same helpers at window.WebDecoyClearance.

Recovery and the retry share a deadline after the first challenge: 10 seconds by default, configurable from 1 ms to 60 seconds. Refusal, resolver rejection, offline state, or deadline expiry returns the original readable challenge response. Caller cancellation and fetch network errors reject as native fetch does. One cancelled caller does not cancel other callers waiting for the shared flow; when none remain, the recovery signal aborts. Your flow must stop network work and close its UI on that signal. If it ignores cancellation, later calls remain attached to that stopped attempt until it settles, rather than opening overlapping recovery dialogs. Browsers without AbortSignal.any() return the challenge without attempting recovery.

Only version-1 responses with the exact WebDecoy challenge headers qualify. Ordinary application 403s, unsupported/malformed contracts, revoked sessions, crawler-policy refusals and agent impersonation do not trigger recovery. Redirected responses, different origins and credentials: 'omit' also do not. The helper preserves request options and leaves response bodies/streams unread. It adds no request headers or CORS permission. Cross-origin calls need credentials: 'include', the protected host’s cookie, and an application-approved CORS policy that exposes the four challenge headers; otherwise the browser cannot identify the response. A cookie set on the page’s host does not clear a different API host.

onEvent reports challenge-served, recovery-attempted, recovery-succeeded/recovery-failed, and the retried enforcement-outcome (status). Attempts are per waiting request; count callback invocations separately for shared flows. Success means the retry returned an OK response, not a new trust grade. These local observations send no telemetry automatically and must remain distinct from the validator’s verdict counts. Observation failures cannot change the request. Interactive application flows must provide keyboard access, focus restoration, an announced status and cancellation; the helper renders no UI.