Documentation

BotShield is a proof-of-work captcha. The browser solves a small SHA-256 challenge, your server checks the signed result with one HTTP call, and no cookies are set. Adding it to a page is a script tag and one div.

Quickstart

You need a project API key. Create a project in the dashboard to get one — the free plan does not ask for a card.

HTML example
<!-- 1. Load the SDK, in <head> or before </body> -->
<script src="https://botshield.dev/sdk.min.js?v=0.2.5-1"></script>
HTML example
<!-- 2. Place the widget inside your <form> -->
<form method="POST" action="/contact">
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>

  <div id="botshield" data-key="sc_live_YOUR_API_KEY"></div>

  <button type="submit">Send</button>
</form>

The widget adds three hidden fields to the surrounding form when it solves. Step three is checking them on your server, below — a captcha that is not verified server-side protects nothing, because a bot can simply post your form directly.

Widget attributes

AttributeValuesDefaultNotes
data-keysc_live_… or sc_test_…requiredYour project API key. Can be set on the container or on the script tag; the container wins.
data-modeauto, invisibleautoauto renders a checkbox. invisible solves in the background and shows a badge.
data-themeauto, light, dark, high-contrastautoauto follows the page’s dark mode. high-contrast raises contrast for the widget surface.
data-accentblue, green, pink, amber, cyanblueAccent colour of the widget.
data-targetany CSS selector#botshield, #scaptchaSet on the script tag to mount the widget somewhere other than the default container ids.

What gets submitted

On a successful solve the widget writes three hidden inputs into the closest form:

  • captcha_challenge — The challenge the widget was issued.
  • captcha_nonce — The proof-of-work nonce it found.
  • captcha_signature — The signed payload. Single-use — a replayed signature is rejected.

If you submit the form with JavaScript instead, read the same three values from the botshield:solved event detail.

Verifying on your server

Post the three fields and your key to POST https://botshield.dev/api/v1/verify. A successful response is { "success": true }; anything else means do not process the submission.

Keep the key on the server. It is the credential that spends your monthly allowance.

Node.js

JavaScript example
// Node.js (Express)
app.post("/contact", async (req, res) => {
  const captchaRes = await fetch("https://botshield.dev/api/v1/verify", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      key: process.env.BOTSHIELD_KEY || process.env.SCAPTCHA_KEY || "sc_live_YOUR_API_KEY",
      captcha_challenge: req.body.captcha_challenge ?? "",
      captcha_nonce: req.body.captcha_nonce ?? 0,
      captcha_signature: req.body.captcha_signature ?? "",
    }),
  })

  const { success } = await captchaRes.json()
  if (!success) return res.status(400).send("Captcha failed")

  // Captcha passed - continue request handling
})

Python

Python example
# Python (Flask)
import os
import requests
from flask import request, abort

def verify_captcha():
    response = requests.post(
        "https://botshield.dev/api/v1/verify",
        json={
            "key": os.getenv("BOTSHIELD_KEY", os.getenv("SCAPTCHA_KEY", "sc_live_YOUR_API_KEY")),
            "captcha_challenge": request.form.get("captcha_challenge", ""),
            "captcha_nonce": request.form.get("captcha_nonce", ""),
            "captcha_signature": request.form.get("captcha_signature", ""),
        },
        timeout=5,
    )

    if not response.ok or not response.json().get("success"):
        abort(400, "Captcha failed")

PHP

PHP example
<?php
// PHP (vanilla)
$payload = [
  'key' => getenv('BOTSHIELD_KEY') ?: (getenv('SCAPTCHA_KEY') ?: 'sc_live_YOUR_API_KEY'),
  'captcha_challenge' => $_POST['captcha_challenge'] ?? '',
  'captcha_nonce' => $_POST['captcha_nonce'] ?? '',
  'captcha_signature' => $_POST['captcha_signature'] ?? '',
];

$ch = curl_init('https://botshield.dev/api/v1/verify');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response, true);
if (empty($result['success'])) {
  http_response_code(400);
  exit('Captcha failed');
}
?>

Java

Java example
// Java (Spring Boot)
Map<String, Object> payload = Map.of(
  "key", System.getenv().getOrDefault("BOTSHIELD_KEY", System.getenv().getOrDefault("SCAPTCHA_KEY", "sc_live_YOUR_API_KEY")),
  "captcha_challenge", request.getParameter("captcha_challenge"),
  "captcha_nonce", request.getParameter("captcha_nonce"),
  "captcha_signature", request.getParameter("captcha_signature")
);

HttpRequest verifyRequest = HttpRequest.newBuilder()
  .uri(URI.create("https://botshield.dev/api/v1/verify"))
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofString(new ObjectMapper().writeValueAsString(payload)))
  .build();

HttpResponse<String> verifyResponse = httpClient.send(verifyRequest, HttpResponse.BodyHandlers.ofString());
JsonNode json = new ObjectMapper().readTree(verifyResponse.body());

if (!json.path("success").asBoolean(false)) {
  throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Captcha failed");
}

cURL

Shell example
curl -X POST "https://botshield.dev/api/v1/verify" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "sc_live_YOUR_API_KEY",
    "captcha_challenge": "<captcha_challenge>",
    "captcha_nonce": "<captcha_nonce>",
    "captcha_signature": "<captcha_signature>"
  }'

Browser events

The container element emits these events. Each is also emitted under the legacy scaptcha: prefix.

JavaScript example
const widget = document.getElementById("botshield");

widget.addEventListener("botshield:solved", (event) => {
  // event.detail: { captcha_challenge, captcha_nonce, captcha_signature }
});

widget.addEventListener("botshield:error", (event) => {
  console.error(event.detail.message);
});

widget.addEventListener("botshield:expired", () => {
  // The challenge passed its 5-minute TTL. The widget resets itself and
  // clears the hidden fields, so a stale solution is never submitted.
});

widget.addEventListener("botshield:fallback", () => {
  // Invisible mode gave up and rendered a checkbox instead.
});

widget.addEventListener("botshield:disabled", () => {
  // Protection is switched off for this project in the dashboard.
});

Invisible mode

In invisible mode the challenge is solved in the background and the user sees a small badge rather than a checkbox. If solving takes longer than five seconds the widget renders a checkbox instead and emits botshield:fallback, so a slow device never ends up with a form it cannot submit.

HTML example
<!-- Invisible mode: solves in the background, shows a small badge
     instead of a checkbox, and falls back to a checkbox if solving
     takes longer than 5 seconds. -->
<div id="botshield"
     data-key="sc_live_YOUR_API_KEY"
     data-mode="invisible"></div>

Invisible mode is available on every plan, including free, and is bounded only by that plan’s monthly solve allowance. See pricing.

Testing

Every project has a second key beginning sc_test_. A test key always passes verification without checking the proof of work, so you can exercise your form handler without solving a challenge. Use it in development and in CI, and your live key only in production.

Test-key verifications are still recorded as solves against your monthly allowance, so a long CI run does consume it.

Allowances and errors

  • Over your allowance — verify returns { "success": false, "error": "Monthly solve limit reached" } for the rest of the calendar month.
  • Replayed solution — each signature verifies once. A second attempt returns "Solution already used".
  • Expired challenge — challenges live for five minutes.
  • Rate limited — a burst of requests on one key returns HTTP 429.
  • Unknown key — HTTP 403. A malformed request body returns HTTP 400.

Using WordPress? See the WordPress integration guide.