Documentation

Everything you need to put WAU Captcha in front of a form or an API.

Examples use this installation's address, https://api.waucaptcha.com. Replace it with yours if you run your own.

How it works

A captcha check takes three calls. Your page fetches a challenge with the public site key, the person answers it and gets a one-time token, and your server redeems the token with the secret key before accepting the request.

Your keys

  • Site key (pk_…) — public. Used in the browser to fetch challenges.
  • Secret key (sk_…) — private. Used by your server to redeem tokens. Never ship it to a browser.
  • Create a key pair on the API keys page of the dashboard.

Quick start

  1. Create an API key in the dashboard and copy its site key and secret key.
  2. Add the widget, or <WauCaptcha /> in React, to your form. Once solved, the form sends a hidden field named wau-captcha-token.
  3. On your server, redeem that token with the secret key and reject the request when it fails.

Browser widget

The widget shows the challenge, a text box and two buttons, and loads a new challenge after a wrong answer. With the script build, any element with data-wau-captcha renders by itself.

html
<script src="https://cdn.jsdelivr.net/npm/@wau-captcha/client@1/dist/wau-captcha.global.js"></script>

<form action="/signup" method="post">
  <input name="email" type="email" required />
  <div data-wau-captcha data-site-key="pk_…" data-base-url="https://api.waucaptcha.com"></div>
  <button type="submit">Sign up</button>
</form>

Attributes: data-site-key and data-base-url (required), data-challenge-type (image_distort or motion_noise) and data-input-name (default wau-captcha-token).

With a bundler, render it yourself to get callbacks and your own labels:

bash
npm install @wau-captcha/client
javascript
import { renderWidget } from "@wau-captcha/client";

const widget = renderWidget(document.querySelector("#captcha"), {
  siteKey: "pk_…",
  baseUrl: "https://api.waucaptcha.com",
  onSuccess(token) {
    // Also in the hidden input "wau-captcha-token" of the surrounding form.
  },
  onError(error) {
    console.error(error.code, error.message);
  },
});

React component

@wau-captcha/react gives you one component, <WauCaptcha />, for React 18 or later. Put it inside a form: once solved, the form sends the token in the hidden field wau-captcha-token.

bash
npm install @wau-captcha/react

Next.js App Router

It works straight from a Server Component page; the server action of the form redeems the token with @wau-captcha/server.

app/signup/page.tsx
import { WauCaptcha } from "@wau-captcha/react";
import { signUp } from "./actions";

export default function SignUpPage() {
  return (
    <form action={signUp}>
      <input name="email" type="email" required />
      <WauCaptcha siteKey="pk_…" baseUrl="https://api.waucaptcha.com" />
      <button type="submit">Sign up</button>
    </form>
  );
}
app/signup/actions.ts
"use server";

import { WauCaptchaServer } from "@wau-captcha/server";

export async function signUp(formData: FormData) {
  const captcha = new WauCaptchaServer({
    secretKey: process.env.WAU_SECRET_KEY!,
    baseUrl: "https://api.waucaptcha.com",
  });
  const result = await captcha.verifyToken(String(formData.get("wau-captcha-token") ?? ""));
  if (!result.success) throw new Error("Please solve the captcha.");
  // … create the account
}

Modal mode

WauCaptchaMode.MODAL shows a button that opens the challenge in a dialog, which closes by itself once the answer is right. The challenge only loads when the dialog opens. Callbacks such as onSuccess need a Client Component.

app/signup/signup-captcha.tsx
"use client";

import { ChallengeType, WauCaptcha, WauCaptchaMode } from "@wau-captcha/react";

export function SignUpCaptcha() {
  return (
    <WauCaptcha
      mode={WauCaptchaMode.MODAL}
      challengeType={ChallengeType.MOTION_NOISE}
      siteKey="pk_…"
      baseUrl="https://api.waucaptcha.com"
      onSuccess={(token) => console.log("solved", token)}
      onError={(error) => console.error(error.code)}
    />
  );
}

Props

PropDescription
siteKey, baseUrlRequired: the site key of your API key and the address of the WAU Captcha API.
modeWauCaptchaMode.INLINE (default) shows the challenge in place; WauCaptchaMode.MODAL opens it from a button.
challengeTypeChallengeType.IMAGE_DISTORT (default) or ChallengeType.MOTION_NOISE.
themeWauCaptchaTheme.AUTO (default) follows the color-scheme of the page; LIGHT and DARK force one.
inputNameName of the hidden field that carries the token. Default wau-captcha-token.
labelsTexts to replace in your language, e.g. { open: "Verify you are human", verify: "Verify" }.
onSuccess, onExpire, onErrorCalled with the token of a right answer, when the token expires unused, and with a WauCaptchaError.

To start over, e.g. after the backend has redeemed the token, change the component's key. To match your brand, set --wau-captcha-primary, --wau-captcha-background or --wau-captcha-border on it or on an ancestor.

Custom UI in the browser

Use WauCaptchaClient to build your own interface. A challenge can be answered once: after a wrong answer, fetch a new one.

javascript
import { ChallengeType, WauCaptchaClient } from "@wau-captcha/client";

const captcha = new WauCaptchaClient({ siteKey: "pk_…", baseUrl: "https://api.waucaptcha.com" });

const challenge = await captcha.newChallenge(); // or { challengeType: ChallengeType.MOTION_NOISE }
image.src = challenge.imageUrl;
image.width = challenge.width;
image.height = challenge.height;

const result = await captcha.verify(challenge.id, input.value);
if (result.success) {
  // Send result.token to your server with the form.
} else {
  // Wrong answer: the challenge is used up, fetch a new one.
}

Verify on a Node.js server

Redeem the token with @wau-captcha/server (Node.js 18 or later). A thrown WauCaptchaError means the token could not be checked; treat it as a failure.

bash
npm install @wau-captcha/server
javascript
import { ErrorCode, WauCaptchaServer } from "@wau-captcha/server";

const wau = new WauCaptchaServer({
  secretKey: process.env.WAU_SECRET_KEY,
  baseUrl: "https://api.waucaptcha.com",
});

app.post("/signup", async (req, res) => {
  try {
    const result = await wau.verifyToken(req.body["wau-captcha-token"]);
    if (!result.success) return res.status(400).send("Please solve the captcha.");
  } catch (error) {
    if (error.code === ErrorCode.SECRET_KEY_INVALID) console.error("Check WAU_SECRET_KEY");
    return res.status(503).send("Captcha check unavailable."); // fail closed
  }
  // … create the account
});

Verify on a Java server

Redeem the token with com.waucaptcha:wau-captcha-sdk (Java 17 or later, no dependencies). The client is immutable and safe to share between threads.

xml
<dependency>
    <groupId>com.waucaptcha</groupId>
    <artifactId>wau-captcha-sdk</artifactId>
    <version>1.0.0</version>
</dependency>
java
WauCaptchaClient wau = WauCaptchaClient.builder()
        .baseUrl("https://api.waucaptcha.com")
        .secretKey(System.getenv("WAU_SECRET_KEY"))
        .build();

try {
    if (!wau.verifyToken(request.getParameter("wau-captcha-token")).success()) {
        // reject the request
    }
} catch (WauCaptchaException e) {
    // e.getCode(): SECRET_KEY_INVALID, NETWORK_ERROR, … — fail closed
}

REST API

The SDKs call these endpoints; any HTTP client can call them too. Errors come back as JSON {code, reason, message, metadata}.

GET /challenge/new

Issues a challenge. Query: site_key (required), challenge_type (image_distort by default). image is base64; display it at width × height CSS pixels.

bash
curl "https://api.waucaptcha.com/challenge/new?site_key=pk_…&challenge_type=image_distort"

{
  "challenge_id": "0f9c…",
  "challenge_type": "image_distort",
  "image": "iVBORw0KGgo…",
  "mime_type": "image/png",
  "width": 200,
  "height": 70,
  "expires_in": 120
}

POST /challenge/verify

Checks an answer. success is false after a wrong answer; the challenge is used up either way.

bash
curl -X POST "https://api.waucaptcha.com/challenge/verify" \
  -H "Content-Type: application/json" \
  -d '{"challenge_id": "0f9c…", "answer": "a8Kq2"}'

{ "success": true, "token": "…", "expires_in": 120 }

POST /siteverify

Redeems a token, once. Send the secret key as a bearer token. success is false when the token is unknown, expired, already used or issued for another key.

bash
curl -X POST "https://api.waucaptcha.com/siteverify?token=$TOKEN" \
  -H "Authorization: Bearer $WAU_SECRET_KEY"

{ "success": true, "challenge_type": "image_distort", "challenge_ts": "2026-09-23T08:15:02Z" }

Challenge types

  • image_distort — five characters (letters and digits) distorted in a PNG. The default.
  • motion_noise — four characters (capital letters and digits, without O, 0, Q, I and 1) hidden in a looping animated GIF of moving dots.

Answers are not case-sensitive. The plan of your account decides which types a key may request. In the JavaScript SDKs, name them with ChallengeType.IMAGE_DISTORT and ChallengeType.MOTION_NOISE.

Limits and security

  • Challenges and tokens expire after two minutes, and each works once.
  • Allowed IP addresses: on a key's page, list the addresses or networks (such as 203.0.113.0/24) that may request challenges. With none listed, every address may; 0.0.0.0 also allows everyone.
  • Resetting a secret key keeps the previous one valid for 24 hours, or until you revoke it, so servers can switch without downtime.
  • Each account has a daily quota of successful verifications (1,000 on the free plan). Once it is used up, new challenges are refused until midnight (Asia/Ho_Chi_Minh).
  • A key that fetches at least 200 challenges in a day but sends fewer than 20% of them to /challenge/verify ((/challenge/verify) ÷ (/challenge/new) under 20%) is taken for spam and suspended automatically.

Errors

In the JavaScript SDKs, compare error.code with the ErrorCode constants, e.g. ErrorCode.SITE_KEY_INVALID.

CodeMeaning
SITE_KEY_INVALIDUnknown, paused or deleted site key.
SECRET_KEY_INVALIDUnknown, paused or deleted key, or a wrong secret.
ORIGIN_NOT_ALLOWEDThe caller's IP address is not among the key's allowed addresses.
CHALLENGE_TYPE_NOT_ALLOWEDThe account's plan does not include this challenge type.
CHALLENGE_TYPE_UNSUPPORTEDchallenge_type is not a known type.
CHALLENGE_NOT_FOUNDThe challenge expired or was already answered. The widget and <WauCaptcha /> load a new one by themselves.
API_KEY_SUSPENDEDSuspended by an administrator or by the scraping check.
ACCOUNT_BANNEDThe owner of the key was banned.
DAILY_QUOTA_EXCEEDEDThe account used up today's successful verifications.
RATE_LIMITEDToo many requests; try again shortly.
ROUTE_NOT_FOUNDNo service handles the path; check the base URL.
UPSTREAM_UNAVAILABLEThe service is temporarily unavailable; try again later.
NETWORK_ERROR, TIMEOUT, INVALID_RESPONSEReported by the SDKs when no usable reply arrived.