DocsOverview and quickstart

Add OTCaptcha to a form

One script on your page, one call when the form opens, one check on your server. The whole integration on one page, with links to the detail.

The three parts

Each site has a site key, 32 lowercase hex characters, which is public and goes in the page, and a secret, which stays on your server. The secret signs your server's calls to us and our passes to you. Never put it in the browser.

  1. Your pageLoad the scriptOne file from us or from your own origin. It defines window.otcaptcha.
  2. Your pageAsk for a passotcaptcha.init returns a widget. Most visitors pass with nothing to click; some see a short hold or a slide puzzle.
  3. Your serverConfirm the passPost the five strings of the pass, signed with your secret, to the verify endpoint. Only a success lets the form through.

1. Load the script

Add the loader before your own code. With defer on both tags the browser runs them in order, so otcaptcha exists when your script starts. The file is about 12 KB gzipped, carries no cookies, and starts its background workers from its own URL.

HTML
<script src="https://api.otcaptcha.com/captcha/v1/otcaptcha.js" defer></script>
<script src="/js/signup.js" defer></script>

The script does three things the moment it runs: it defines window.otcaptcha with init and version, it adds a preconnect hint for our API host so the first call saves a connection setup, and it starts counting pointer activity on the page, which the risk score uses later. It makes no network request until you call init.

You can also serve the file from your own origin. Updates then arrive when you copy a new file. See Self-hosting.

2. Ask for a pass

Call init with your site key and the page language. The callback receives the widget at once, and the widget asks us for a challenge and starts its background workers straight away. Nothing is shown yet. When the visitor submits, call showBox() from onReady: it finishes the check, and a box appears only if the attempt looks risky. onSuccess fires when a pass exists.

JavaScript
// signup.js: runs after the loader, because both scripts are deferred and in order.
const form = document.querySelector('#signup');
let widget;

// When the form is shown: the challenge is fetched and solved in the background.
function arm() {
  otcaptcha.init({ captchaId: '[SITE KEY]', language: document.documentElement.lang }, (w) => { widget = w; });
}
arm();

form.addEventListener('submit', (event) => {
  event.preventDefault();
  widget
    .onReady(() => {
      if (!widget.getValidate()) widget.showBox();   // finishes the check; a box appears only if needed
    })
    .onSuccess(() => {
      const pass = widget.getValidate();   // five strings: see the Browser API page
      widget.destroy();
      sendForm(form, pass);                // your code: post the form fields and the pass together
    })
    .onClose(() => { widget.destroy(); arm(); })   // the visitor closed the box: ready for another try
    .onError((e) => {
      widget.destroy(); arm();
      showMessage('Verification did not complete (' + e.code + '). Please try again.');
    });
});

One widget is one attempt. After destroy(), call init again for the next one. Every on… method returns the widget, so they chain, and a handler attached after its event has already fired is still called, so attaching them on submit is safe. The full reference, including what fires in which order and the exact payloads, is on the Browser API page.

Calling init when the form is shown, as above, gives the risk score a few seconds of page behaviour to look at, and the background work is usually finished before the visitor presses the button. A challenge is valid for ten minutes; a form left open longer than that gets an error on submit, which the sample answers by arming again. If you would rather keep it simple, call init inside the submit handler instead: the check then takes a moment longer.

3. Confirm the pass on your server

The pass is five strings: captcha_id, lot_number, captcha_output, gen_time and pass_token. Send them with the other form fields, and before you act on the form, post them to the verify endpoint as a form body with sign_token, the hex HMAC-SHA256 of lot_number under your secret. A pass works once and expires five minutes after it was issued.

Node.js
// Node.js, on your server. SECRET never leaves it.
import { createHmac } from 'node:crypto';

export async function confirmPass(pass) {
  const sign_token = createHmac('sha256', SECRET).update(pass.lot_number).digest('hex');
  const body = new URLSearchParams({
    lot_number: pass.lot_number,
    captcha_output: pass.captcha_output,
    pass_token: pass.pass_token,
    gen_time: pass.gen_time,
    sign_token,
  });
  const url = 'https://verify.otcaptcha.com/validate?captcha_id=' + encodeURIComponent(pass.captcha_id);
  const res = await fetch(url, { method: 'POST', body, signal: AbortSignal.timeout(5000) });
  if (res.status !== 200) throw new Error('no decision: HTTP ' + res.status);  // see Outage policy
  const answer = await res.json();
  return answer.result === 'success';   // otherwise answer.code says why: see Server check
}

Every decision is an HTTP 200 with result set to success or fail, and a fail carries a code for your logs. Anything else, a timeout or a 5xx, is not a decision, and your outage policy says what happens then. The Server check page has every field, code and limit, with samples in Python and Go too.

Treat a missing pass the same as a failed one. A page that shows the check but never confirms passes on the server is unprotected, and our reports name such sites so the gap is noticed before an attacker does.

Where to go next

Recommended flowHow we suggest a site builds the whole thing, end to end: the sequence, a diagram, the decisions you make and the things not to do.Browser APIThe otcaptcha.init call, its configuration, the widget it returns, every event and payload, and what the loader does on the network.Server checkConfirm a pass from your server: the request, how to sign it, every answer and code, retries and timeouts.How a check worksWhat happens between the call and the pass: the challenge, the background work, the risk score, the hold, the puzzle and attack mode.Outage policyWhat the browser does when it cannot reach us, what your server should do when it cannot, and why blocking our domain is not a bypass.Content security policyHosts, directives and request shapes, so the loader works on a page with a strict policy and behind a firewall.Brand lookAccent colour, corner radius, light or dark and the trust line, set per site, and how the box behaves on phones and with a keyboard.Self-hostingServe the loader from your own origin, or run the whole service yourself: the binary, the store, the ports and the settings.Reports and dataWhat a record holds and never holds, how long it is kept, the challenge funnel, attack timelines and the metrics a self-hosted service exposes.TestingWhat to check before going live, how to see every outcome on a staging page, and how to test the server check with curl.TroubleshootingSymptoms in the browser and on your server, what each one means and what to change.