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.