Skip to main content
Bkey browser SDK for human and age checks. One API serves all check types. The widget starts at 300 × 65 px, expands to show a QR code, and closes to a checkmark after approval. It supports light, dark and automatic themes, keyboard controls and reduced motion. The built-in connection accepts human, age, and age-of-majority checks returned by the backend. The backend decides which checks it supports. Age approval requires backend and mobile support; SDK support alone does not enable it. The default demo simulates approval. Use /demo/live.html to connect to a real backend.

Render a check

For another environment, call bkey.configure({ apiUrl: 'https://dev-api.bkey.id' }) before rendering. The URL must be an HTTPS origin; HTTP is allowed for localhost. The deployment must enable CAPTCHA requests and approval for the configured check. The sitekey must allow the browser hostname, including localhost explicitly for local development. Never put a server secret in sitekey or browser code. The SDK sends POST /v1/captcha/requests with { sitekey }, uses the returned qrPayload unchanged, and polls GET /v1/captcha/requests/:id/status with X-Bkey-Poll-Token. Each widget keeps its polling credential only in memory and clears it on reset, removal, expiry, failure, or completion. Requests omit cookies and account credentials. Server expiry controls both request and result lifetime. action and cData are currently rejected by the built-in connection because the backend does not accept them. They remain available to custom transports. Your server must POST { sitekey, clientId, clientSecret, response } to /v1/captcha/siteverify, require success: true, and check that hostname matches the expected site. This consumes the result once. Keep clientSecret on your server. A widget success callback alone must not grant access.

Sitekey policy

The app owner configures the required check on the sitekey through authenticated app management. These are stored policy values, not browser render options:
There is no default check. The backend must load the stored policy and copy it into the request. The widget reads that policy from the create response and uses it for labels and status matching. mode, age, and check render options are rejected. They are not sent to the transport. Authenticated portal management and backend request enforcement are not implemented by this UI package. Ages are whole years from 0 to 150. Range endpoints and comparisons are inclusive. Exact ages and ranges can disclose personal information. The phone approval flow must explain the requested claim before the person approves it. ageOfMajority asks for adult status without a site-selected country or age. The planned service chooses the rule from the visitor’s IP location and returns only the result. This package does not locate the visitor or implement those rules. An IP location does not establish residence. A future service connection must use the visitor’s IP, not the website server’s outgoing IP.

API

A widget target is its returned ID, a container selector, or the container element. If omitted, the first active widget is used. Removed or unknown targets throw. Containers must be attached to the document. Policy changes belong in authenticated app management. Reset creates a new request and reads the current service policy; it does not change an existing approval. The internal UI and controller are not public APIs. execution: 'render' prepares the QR request when the widget is rendered. The checkbox opens the QR panel and starts status polling. execution: 'execute' waits for execute() before creating a request. It disables the checkbox until then and shows the neutral label “Verify with bkey” until the service returns the policy. Neither execution mode bypasses approval on the phone. Reset, denial, errors and expiry clear the response field. Native form reset also resets the widget unless the reset event is cancelled. The SDK removes expired responses when a form builds its FormData. Server verification is still required. The SDK does not block form submission or grant access.

Classic script and HTML

Host the built dist/api.js file at a URL you control. There is no assumed CDN URL. The bundle exposes window.bkey.
Define the service connection before the script loads. The onload function runs before automatic rendering of .bkey-captcha containers. To render only through JavaScript, add &render=explicit to the script URL and call bkey.render in the onload function. data-callback values refer to named functions on window. data-mode, data-age, and data-check are rejected. Other supported attributes include data-theme, data-size, data-execution, data-action, data-cdata, data-response-field, data-response-field-name, data-error-callback, data-expired-callback and data-denied-callback.

Service connection contract

The exported Transport type has two async methods. Both receive the public sitekey, optional action and cData, and an AbortSignal. Neither accepts a check, age rule, or location override. action and cData do not select policy. status also receives requestId. Honor the signal to cancel network work. Each call has a 15-second UI timeout, including adapters that ignore cancellation. create(input) returns:
The QR link is an example of the contract, not a claim about a deployed app route. Use the exact link supplied by the service. Only HTTPS and bkey: links are accepted; the connection must also enforce trusted hosts. Poll intervals are bounded to 1–10 seconds. status(input) returns the same request ID and check conditions, plus status: pending, scanned, approved, denied or expired. Approval must include:
Omit age for human and ageOfMajority. Expiry values are ISO 8601 UTC strings. The response is not the request ID. Missing tokens, changed conditions, mismatched request IDs and malformed responses do not produce success. Request and response expiry are separate. Expired or late replies cannot restore a cleared response. These checks protect UI state; they do not verify identity in the browser. The future service must load policy from the sitekey, reject browser overrides, bind the token to the sitekey, request and saved policy, enforce expiry and single use, and provide server-side verification. The website server must verify the expected sitekey and required policy, including after configuration changes. A token from a weaker sitekey must not grant access. Never trust a browser callback or a hidden field alone. The demo uses fake tokens and has no such service.

Turnstile integration model

The main API follows the common Turnstile method names, token callback and hidden field model. It is a single SDK, not a wrapper around a second public element API. To migrate, change the loader/import and namespace to Bkey, use a Bkey site key, connect the Bkey service, and replace server-side verification. If an existing form needs the old field name, set 'response-field-name': 'cf-turnstile-response'. This changes only the form name; Bkey tokens are not Cloudflare tokens. This is a supported subset, not full Cloudflare API or protocol compatibility. Features such as compact/invisible widgets, appearance policies and automatic retry settings are not supported. Unknown render options throw. The bundle does not replace window.turnstile. References: client rendering, widget options, server validation.

Local development and verification

From the typescript workspace:
The demo opens at http://127.0.0.1:4173. E2E tests run the compiled SDK in desktop Chromium and mobile WebKit. They exercise both package entry points, real QR encoding, callbacks, forms, age conditions, cancellation, expiry and failures. Repeatable artifacts are in playwright-report/ and test-results/, including screenshots, JSON results and browser traces. See E2E-PLAN.md for failure cases.