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.
<!-- 1. Load the SDK, in <head> or before </body> -->
<script src="https://botshield.dev/sdk.min.js?v=0.2.5-1"></script><!-- 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
| Attribute | Values | Default | Notes |
|---|---|---|---|
| data-key | sc_live_… or sc_test_… | required | Your project API key. Can be set on the container or on the script tag; the container wins. |
| data-mode | auto, invisible | auto | auto renders a checkbox. invisible solves in the background and shows a badge. |
| data-theme | auto, light, dark, high-contrast | auto | auto follows the page’s dark mode. high-contrast raises contrast for the widget surface. |
| data-accent | blue, green, pink, amber, cyan | blue | Accent colour of the widget. |
| data-target | any CSS selector | #botshield, #scaptcha | Set 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
// 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 (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
// 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 (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
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.
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.
<!-- 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 HTTP400.
Using WordPress? See the WordPress integration guide.