Website integration
JavaScript
Embed widget.js with a script tag or mount it programmatically; configuration attributes, the window.imnotahuman API, DOM events and form integration.
https://imnotahuman.ai/widget.js is a dependency-free script that renders the reverse CAPTCHA inside a closed shadow root. It only ever displays what the verification server reports; nothing on the page can put it into the verified state.
Script tag
<script src="https://imnotahuman.ai/widget.js"
data-site-key="site_live_exampleweb2ab3cd4ef5gh6j"
data-theme="auto"
data-action="checkout"
data-intent="purchase"></script>When the page contains no explicit container, the widget inserts a <div class="imnotahuman"> directly after the script tag and copies the script's data-* attributes onto it. To place it yourself, add one or more containers; the script mounts into every .imnotahuman[data-site-key] or [data-imnotahuman-widget] element instead.
<div class="imnotahuman" data-site-key="site_live_exampleweb2ab3cd4ef5gh6j" data-action="login"></div>
<script src="https://imnotahuman.ai/widget.js" async></script>| Attribute | Values | Description |
|---|---|---|
data-site-keyrequired | site_live_… | site_test_… | Required. Public site key from the dashboard. |
data-theme | auto | light | dark | Colour scheme. auto follows prefers-color-scheme. Default auto. |
data-action | string ≤ 64 | Host-declared action bound into the challenge and token, e.g. checkout. Allowed characters: A–Z a–z 0–9 _ . : - |
data-intent | string ≤ 64 | Host-declared intent, e.g. purchase. Same character set. |
data-ttl | 30–300 | Challenge lifetime in seconds. Default 120. |
data-api | origin | Origin of the verification API. Set https://imnotahuman.ai so the widget calls https://imnotahuman.ai/api/v1. When widget.js is loaded from localhost or 127.0.0.1, that origin's /api/v1 is used unless you set this. |
Programmatic mount
const widget = window.imnotahuman.render("#agent-check", {
siteKey: "site_live_exampleweb2ab3cd4ef5gh6j",
action: "checkout",
theme: "dark",
ttl: 180,
});
widget.onVerified((result) => {
console.log(result.agent_name, result.token);
});render(container, options) accepts an element or a selector and returns the widget instance. Options mirror the data attributes: siteKey, theme, action, intent, ttl, api. Attributes already on the container are used as defaults.
window.imnotahuman
| Member | Signature | Description |
|---|---|---|
version | string | Widget version, e.g. "1.1.0". |
render | (container, options) => Widget | Mounts a widget and starts a challenge immediately. |
onVerified | (cb) => unsubscribe | Fires once per verified challenge, for every widget on the page. If a widget is already verified, cb runs immediately. |
onChange | (cb) => unsubscribe | Fires on every status change with { status, challenge, result, error }. Runs once immediately with the current snapshot. |
getChallenge | () => Challenge | null | Current challenge document of the first widget. |
getResult | () => VerifiedResult | null | Verified result of the first widget, or null. |
reset | () => void | Starts a fresh challenge on every widget. The previous token is discarded. |
widgets | Widget[] | All mounted instances. Each has onVerified, onChange, getChallenge, getResult, start(), and setTheme(). setTheme(theme) changes the colour scheme without issuing a new challenge. |
Callbacks can be registered before the script loads using a queue, which the widget drains on initialisation:
<script>
window.imnotahuman = { q: [["onVerified", function (r) { console.log(r.agent_id); }]] };
</script>
<script src="https://imnotahuman.ai/widget.js" data-site-key="site_live_exampleweb2ab3cd4ef5gh6j" async></script>Statuses
The widget's status (also mirrored on the host element as data-status) is one of:
| Status | When |
|---|---|
| LOADING | Creating a challenge. |
| PENDING | Challenge issued; polling GET /challenges/:id every 2 s (3 s after 30 s; 5 s while the tab is hidden). A countdown shows time left. |
| VERIFIED | The server reported status VERIFIED with a result. Token available. |
| REJECTED | The server reported REJECTED (attempt limit exceeded), or the administrative REVOKED / ERROR states. A Retry button issues a new challenge. |
| EXPIRED | expires_at passed, locally or as reported by the server. Retry issues a new challenge. |
| ERROR | Challenge creation failed, e.g. ORIGIN_UNAUTHORIZED, SITE_KEY_INVALID, RATE_LIMITED or the service was unreachable. The code is shown in the widget, on data-error, and as event.error. |
Forms and the hidden input
Every widget appends <input type="hidden" name="imnotahuman-token"> to its host element. Place the host inside a <form> and the token is submitted with the other fields once verified (the value is empty until then). Verify it on your server before acting on it; see Server verification.
The host also carries a <script type="application/json" data-imnotahuman-challenge> element containing the current challenge document, plus data-challenge-id and data-challenge-url attributes. Agents driving a browser can read these to discover the challenge without parsing the UI.
Test mode
Use the site_test_… key on staging and localhost. The widget shows a Test mode badge and, while PENDING, a Simulate agent button. The host element gets data-mode="test". The subtitle stays "waiting for proof" until the server reports VERIFIED. The badge is not itself a success.
<script src="https://imnotahuman.ai/widget.js"
data-site-key="site_test_exampleweb2ab3cd4ef5gh6j"
data-action="checkout"></script>
<script>
imnotahuman.onChange(function (event) {
// LOADING | PENDING | VERIFIED | REJECTED | EXPIRED | ERROR
if (event.status === "ERROR") console.error(event.error);
// SITE_KEY_INVALID, ORIGIN_UNAUTHORIZED, RATE_LIMITED, SERVICE_UNAVAILABLE, …
});
imnotahuman.onVerified(function (result) {
// result.mode === "test" — reject this token in production unless allow_test
});
</script>Simulate agent calls POST /api/v1/challenges/:id/simulate. A failed simulation stays PENDING and shows the error code; it does not flip the widget to VERIFIED.
The result has mode: "test". A simulated proof also has verification.method: "simulated". POST /api/v1/tokens/verify returns TOKEN_TEST_MODE unless the body sets allow_test: true. Drop that flag before you ship, and switch the script to your site_live_… key.
curl -X POST https://imnotahuman.ai/api/v1/tokens/verify \
-H 'content-type: application/json' \
-d '{"token":"<imnotahuman-token>","site_key":"site_test_exampleweb2ab3cd4ef5gh6j","consume":true,"allow_test":true}'Content Security Policy
Allow script-src https://imnotahuman.ai and connect-src https://imnotahuman.ai. Set data-api="https://imnotahuman.ai" so the widget calls the first-party API at https://imnotahuman.ai/api/v1. The widget injects one <style> element inside its shadow root, so a strict style-src needs 'unsafe-inline' (or an equivalent hash) for it to render.