Quickstart

You need a sitekey and a secret. Create a site in the dashboard at /en/sites; the secret is shown there and can be re-displayed at any time.

1. Add the widget

<form action="/signup" method="post">
  <input name="email" type="email" required>

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

  <button type="submit">Sign up</button>
</form>

The script scans the page for .privcaptcha elements on load and renders each one. When the visitor solves the puzzle, the token is written into a hidden input named privcaptcha-response, which submits with the form.

2. Verify on the server

Never trust the form field on its own — redeem it.

import os
from privcaptcha import verify

result = verify(
    secret=os.environ["PRIVCAPTCHA_SECRET"],
    response=request.form["privcaptcha-response"],
    remote_ip=request.remote_addr,
)
if not result.success:
    abort(400, result.error_codes)

Or with plain HTTP, from any language:

curl -X POST https://privcaptcha.com/api/v1/siteverify \
  -d "secret=sk_your_secret" \
  -d "response=THE_TOKEN_FROM_THE_FORM"
{
  "success": true,
  "challenge_ts": "2026-08-10T12:34:56+00:00",
  "hostname": "example.com",
  "error-codes": []
}

Ready-made helpers exist for Python, Node, PHP and Go.

3. Lock down the domains

While a site's domain list is empty, any origin may use its sitekey — that is deliberate, so your first integration works before you configure anything. Add your production hostnames in the dashboard as soon as the integration works. See Sites and domains.

Things worth knowing

  • A token is single-use and lives for 120 seconds. Verify it as part of handling the submit, not later in a queue.
  • The browser clears the token after 110 seconds and resets the widget, so a slowly-filled form does not submit a token that is already dead.
  • If verification fails, call privcaptcha.reset() before asking the visitor to try again; a spent token cannot be reused.
  • Fail closed. If you cannot reach /siteverify, treat the request as unverified. Every official SDK already does this and returns verification-unavailable.