Weekly report
Traffic, outcomes, attacks, new suspicious networks and solve times, once a week.
OTCaptcha verifies every visitor in the background, raises the cost for suspicious traffic and tightens by itself during an attack.
For sign-up, login, password reset and any other form.



Most visitors see neither. The loader is under 16 KB compressed, and the background check finishes before anyone notices it ran.
Each step looks like part of your page, in your colour and in the visitor's language.
The real box on a phone, over a plain sign-up form. First in motion: a press, the fill, the pass. Then the hold waiting and part way through, the slide puzzle, and the moment the check passes.





Every attempt goes through all three. None of them asks a normal visitor to do anything.
The visitor's device completes a short calculation in the background. Trivial once, costly at scale.
Automation traces, network origin and inconsistent device details are scored together. No single signal decides.
Your backend confirms each pass. A pass works once and expires in minutes.
OTCaptcha learns your usual traffic. When volume or suspicious activity jumps, it steps up protection, then steps back down once the attack ends. Nobody has to be woken up.
| State | Work per attempt | Suspicious visitors |
|---|---|---|
| Normal | 1x | Standard checks |
| Elevated | 2x | Held sooner |
| Severe | 4x | Held at the lowest risk score |


// When the sign-up form is shown. A box opens only if needed.
otcaptcha.init({ captchaId: '[SITE KEY]' }, (widget) => {
form.addEventListener('submit', (event) => {
event.preventDefault();
widget
.onReady(() => {
if (!widget.getValidate()) widget.showBox();
})
.onSuccess(() => sendForm(form, widget.getValidate()));
});
});import { createHmac } from 'node:crypto';
const { captcha_id, ...fields } = pass; // the five strings
const sign_token = createHmac('sha256', SECRET)
.update(fields.lot_number).digest('hex');
const url = 'https://verify.otcaptcha.com/validate'
+ '?captcha_id=' + encodeURIComponent(captcha_id);
const body = new URLSearchParams({ ...fields, sign_token });
const res = await fetch(url, {
method: 'POST', body, signal: AbortSignal.timeout(5000),
});
// Anything but a 200 is no decision, never a pass.
if (res.status !== 200) throw new Error('no decision');
const ok = (await res.json()).result === 'success';$sign = hash_hmac('sha256', $pass['lot_number'], SECRET);
$body = http_build_query([
'lot_number' => $pass['lot_number'],
'captcha_output' => $pass['captcha_output'],
'pass_token' => $pass['pass_token'],
'gen_time' => $pass['gen_time'],
'sign_token' => $sign,
]);
$type = 'Content-Type: application/x-www-form-urlencoded';
$ctx = stream_context_create(['http' => ['method' => 'POST',
'timeout' => 5, 'ignore_errors' => true,
'header' => $type, 'content' => $body]]);
$url = 'https://verify.otcaptcha.com/validate?captcha_id='
. rawurlencode($pass['captcha_id']);
$raw = file_get_contents($url, false, $ctx);
$status = $http_response_header[0] ?? '';
// Anything but a 200 is no decision, never a pass.
if ($raw === false || !str_contains($status, ' 200 ')) {
throw new RuntimeException('no decision');
}
$ok = json_decode($raw, true)['result'] === 'success';mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(p.LotNumber))
form := url.Values{
"lot_number": {p.LotNumber},
"captcha_output": {p.CaptchaOutput},
"pass_token": {p.PassToken},
"gen_time": {p.GenTime},
"sign_token": {hex.EncodeToString(mac.Sum(nil))},
}
endpoint := "https://verify.otcaptcha.com/validate" +
"?captcha_id=" + url.QueryEscape(p.CaptchaID)
client := &http.Client{Timeout: 5 * time.Second}
res, err := client.PostForm(endpoint, form)
// Anything but a 200 is no decision, never a pass.
if err != nil || res.StatusCode != http.StatusOK {
return false, errors.New("no decision")
}
defer res.Body.Close()
var a struct{ Result string }
err = json.NewDecoder(res.Body).Decode(&a)
return err == nil && a.Result == "success", nildigest = hmac.new(SECRET.encode(), p["lot_number"].encode(),
hashlib.sha256)
body = urllib.parse.urlencode({
"lot_number": p["lot_number"],
"captcha_output": p["captcha_output"],
"pass_token": p["pass_token"],
"gen_time": p["gen_time"],
"sign_token": digest.hexdigest(),
}).encode()
url = ("https://verify.otcaptcha.com/validate?captcha_id="
+ urllib.parse.quote(p["captcha_id"]))
req = urllib.request.Request(url, data=body, method="POST")
# A non-200 raises: no decision, never a pass.
with urllib.request.urlopen(req, timeout=5) as res:
ok = json.load(res)["result"] == "success"Shortened. The developer guide has the complete samples, with error handling and the outage policy.
Protection you can read about, not just trust. Every check is recorded without personal data, and the reports answer the questions a team asks after an attack.
Traffic, outcomes, attacks, new suspicious networks and solve times, once a week.
Every automatic escalation is kept as one event: when it started, what raised it, which networks and countries were involved, and when it ended.
How many visitors who saw a hold or a puzzle finished it, gave up, or switched to the other check. Visitors who leave never reach your server, so this is the only place they show up.
A site that shows the check but never confirms passes on its server is unprotected. We name it, so an integration gap is noticed before an attacker does.
We are onboarding a small number of teams first.