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

Auto-mount
<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.

Explicit container
<div class="imnotahuman" data-site-key="site_live_exampleweb2ab3cd4ef5gh6j" data-action="login"></div>
<script src="https://imnotahuman.ai/widget.js" async></script>
AttributeValuesDescription
data-site-keyrequiredsite_live_… | site_test_…Required. Public site key from the dashboard.
data-themeauto | light | darkColour scheme. auto follows prefers-color-scheme. Default auto.
data-actionstring ≤ 64Host-declared action bound into the challenge and token, e.g. checkout. Allowed characters: A–Z a–z 0–9 _ . : -
data-intentstring ≤ 64Host-declared intent, e.g. purchase. Same character set.
data-ttl30–300Challenge lifetime in seconds. Default 120.
data-apioriginOrigin 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

render()
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

MemberSignatureDescription
versionstringWidget version, e.g. "1.1.0".
render(container, options) => WidgetMounts a widget and starts a challenge immediately.
onVerified(cb) => unsubscribeFires once per verified challenge, for every widget on the page. If a widget is already verified, cb runs immediately.
onChange(cb) => unsubscribeFires on every status change with { status, challenge, result, error }. Runs once immediately with the current snapshot.
getChallenge() => Challenge | nullCurrent challenge document of the first widget.
getResult() => VerifiedResult | nullVerified result of the first widget, or null.
reset() => voidStarts a fresh challenge on every widget. The previous token is discarded.
widgetsWidget[]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:

Async snippet
<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:

StatusWhen
LOADINGCreating a challenge.
PENDINGChallenge 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.
VERIFIEDThe server reported status VERIFIED with a result. Token available.
REJECTEDThe server reported REJECTED (attempt limit exceeded), or the administrative REVOKED / ERROR states. A Retry button issues a new challenge.
EXPIREDexpires_at passed, locally or as reported by the server. Retry issues a new challenge.
ERRORChallenge 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.

Test site key
<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.

Test tokens are not production proof

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.

Verify a test token
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.