JavaScript API

/1/api.js exposes a single global, window.privcaptcha. Every method takes an optional widget id; omit it and the first widget on the page is used.

Methods

privcaptcha.render(container, params)

Renders a widget and returns its id, or null if no sitekey was resolved.

var id = privcaptcha.render('captcha-slot', { sitekey: 'pk_...' });
Param Type Meaning
sitekey string your public sitekey; falls back to the container's data-sitekey
callback function called with the token on success; falls back to the global named by data-callback
expired-callback function called when the local token lifetime lapses; falls back to the global named by data-expired-callback
error-callback function called with the error when a challenge or solve request fails; falls back to the global named by data-error-callback
responseField string name of the hidden input holding the token (default privcaptcha-response)
required boolean make the widget a required form field; falls back to the container's data-required
requiredMessage string message for the browser's validation bubble; falls back to data-required-message
theme string light (default) or dark, the same values as hCaptcha and reCAPTCHA; falls back to data-theme

container may be an element or an element id.

privcaptcha.getResponse(id)

Returns the current token, or '' if the widget is unsolved.

fetch('/signup', {
  method: 'POST',
  body: new URLSearchParams({
    email: email.value,
    'privcaptcha-response': privcaptcha.getResponse(),
  }),
});

privcaptcha.reset(id)

Clears the token and the hidden input, closes the puzzle if it is open, and returns the badge to its unchecked state. Call it whenever your server rejects a submission — tokens are single-use, so the old one cannot be sent again.

privcaptcha.remove(id)

Removes the widget and empties its container, so the container can be rendered into again. The theme is fixed at render time, as with hCaptcha and reCAPTCHA, so to switch it on a page whose own theme changed, remove the widget and render it again with the new theme.

privcaptcha.remove(id);
id = privcaptcha.render('captcha-slot', { sitekey: 'pk_...', theme: 'dark' });

privcaptcha.execute(id)

Opens the puzzle and requests a challenge programmatically, without the visitor ticking the checkbox. Use it for invisible-style flows where the captcha runs on submit.

form.addEventListener('submit', function (e) {
  if (!privcaptcha.getResponse()) {
    e.preventDefault();
    privcaptcha.execute();
  }
});

Callbacks

privcaptcha.render('slot', {
  sitekey: 'pk_...',
  callback: function (token) { /* solved */ },
  'expired-callback': function () { /* token cleared locally */ },
  'error-callback': function (err) { /* network or API failure */ },
});

error-callback fires for a genuine failure. It does not fire when the account is out of quota and credits: that case is not an error — the badge shows a quiet unavailable state and the host page is left untouched. If you need to distinguish "over limit" from "widget broke", that is the difference.

What runs where

The parent page — not the iframe — makes every API call. That is deliberate: a request from your page is cross-origin to us, so the browser attaches the Origin header itself, and Origin is a header page JavaScript cannot set. It is the one trustworthy statement about which site is embedding the widget, and it is what the domain allowlist checks. A call from inside our own iframe would be same-origin and carry no Origin at all.

The iframes are purely presentational: they draw the puzzle and report the visitor's input by postMessage, origin-checked in both directions. The answer never leaves the server, so it does not matter that the host page can see the challenge payload.