Installation

Load the embed script and mark a container. Everything else is automatic.

<script src="https://privcaptcha.com/1/api.js" async defer></script>
<div class="privcaptcha" data-sitekey="pk_your_sitekey"></div>

The script is cached for 10 minutes, so a widget fix reaches your visitors the same day without you redeploying anything.

Container attributes

Attribute Required Meaning
class="privcaptcha" yes, for automatic rendering marks the container the loader scans for
data-sitekey yes your public sitekey
data-callback no name of a global function called with the token on success
data-expired-callback no name of a global function called when the token lapses
data-error-callback no name of a global function called when a challenge or solve request fails
data-theme no light (default) or dark, the same values as hCaptcha and reCAPTCHA
data-required no make the widget a required form field - the browser blocks submit until it is solved
data-required-message no the message shown in that browser bubble (default: "Please confirm that you are human.")
<div class="privcaptcha"
     data-sitekey="pk_your_sitekey"
     data-callback="onCaptchaSolved"></div>

<script>
  function onCaptchaSolved(token) {
    document.querySelector('#submit').disabled = false;
  }
</script>

The response field

On success the token is written into a hidden input named privcaptcha-response inside the container. A normal form submit carries it; you do not need any JavaScript.

Read it server-side as you would any form field, then verify it. If you submit with fetch() instead, read the token with privcaptcha.getResponse().

Making it required

Add data-required and the widget behaves like any other required field: the browser refuses to submit the form and shows its native validation bubble at the widget until the user has solved it.

<div class="privcaptcha"
     data-sitekey="pk_your_sitekey"
     data-required
     data-required-message="Please confirm that you are human."></div>

The token field then renders as a visually hidden text input instead of a type="hidden" one, because hidden inputs are excluded from HTML5 constraint validation. Nothing else changes: the field keeps the same name and the same value.

This is a client-side convenience only - the browser can be bypassed. You must still verify the token server-side on every submit.

It only applies to forms the browser itself validates. A form with novalidate, or one submitted from JavaScript without calling form.reportValidity() first, is not affected.

Explicit rendering

Skip the privcaptcha class and render when you are ready — useful for dynamically inserted forms, modals or single-page apps.

<div id="captcha-slot"></div>
<script>
  var id = privcaptcha.render('captcha-slot', {
    sitekey: 'pk_your_sitekey',
    callback: function (token) { console.log(token); },
    'expired-callback': function () { console.log('token expired'); },
    'error-callback': function (err) { console.error(err); },
  });
</script>

render() accepts an element or an element id and returns a widget id you can pass to the other JavaScript API methods. It returns null if no sitekey was given.

Layout

The badge is a 302×76 iframe. The puzzle opens in a centred overlay above the page (z-index: 2147483647), dismissible with Escape or a click outside. Nothing is injected into your page's styles, and the iframes carry a frame-ancestors policy scoped to your site's own domains, so no one else can frame your widget.

The iframe endpoints

The loader builds these itself; you never link to them directly, but they show up in a CSP report or a network tab, so they are listed here.

Path What it is
/widget/badge the 302×76 checkbox iframe
/widget/challenge the puzzle iframe inside the overlay

Both take the sitekey and the widget theme as query parameters and answer with a Content-Security-Policy: frame-ancestors header scoped to that site's domains, so only your own pages can frame them. A site with an empty domain list gets *, so a first integration works before the list is configured; an unknown sitekey gets 'none'. Both are noindex.

Lifetime

A token is valid for 120 seconds server-side. The widget clears it after 110 seconds, resets itself and fires expired-callback, so a slowly-filled form never submits a dead token.