Add CAPTCHA to your website
CDNShark CAPTCHA stops spam and bots on your forms using an invisible proof-of-work challenge — no image puzzles, no "click the traffic lights". A real visitor sees a checkbox that verifies itself in about a second; automated spam bots that don't run JavaScript can't produce a valid token at all.
Included with every plan. Your plan includes a number of free CAPTCHA sites (5 on most plans). Need more? Add extra sites for $1/site per month from Resource Add-ons in your portal.Step 1 — Create a site & get your keys
In the customer portal go to CAPTCHA → Add Site. You'll get two keys:
| Key | Where it goes |
|---|---|
| Site Key (public) | In your page's HTML. Safe to expose. |
| Secret Key (private) | On your server only. Never put it in HTML or JavaScript. |
Step 2 — Add the widget to your form
Paste the stylesheet and script once per page, and drop the widget <div> inside the
<form> you want to protect. Replace the site key with your own.
<link rel="stylesheet" href="https://cdnshark.com/css/sharkcaptcha.css">
<script src="https://cdnshark.com/js/sharkcaptcha.js" defer></script>
<form method="POST" action="/your-handler">
<!-- your fields -->
<div class="sharkcaptcha" data-sitekey="YOUR_SITE_KEY" data-theme="auto"></div>
<button type="submit">Send</button>
</form>
On submit the widget adds a hidden field named sharkcaptcha_token to your form.
Widget options
| Attribute | Values | Description |
|---|---|---|
data-sitekey | your site key | Required. |
data-theme | auto (default), light, dark | Match your site. auto follows the visitor's system setting. |
data-callback | function name | Optional. Called with the token when verification succeeds. |
data-appearance | interaction (default), auto | interaction waits for the visitor to click the checkbox. auto starts solving as soon as the page loads. |
data-branding | on (default), off | off hides the CDNShark logo and the Privacy · Terms links. |
data-field | input name | Name of the hidden field the token is written to. Default sharkcaptcha_token. |
data-endpoint | URL | Where the widget fetches its challenge. Defaults to /captcha/challenge on the origin the script was loaded from — so with the snippet above it is https://cdnshark.com/captcha/challenge. Only set this if you proxy the endpoint through your own domain. |
data-logo | image URL | Brand image in the widget footer. Defaults to the CDNShark logo on the script's origin. |
You do not need to proxy /captcha/* on your own domain. The challenge endpoint is
served cross-origin (Access-Control-Allow-Origin: *) and carries no cookies, so the browser can call
cdnshark.com directly from your page.
Step 3 — Verify the token on your server
When the form is submitted, send the sharkcaptcha_token together with your secret key to the
verify endpoint. Reject the submission unless the response is success: true.
Endpoint: POST https://cdnshark.com/captcha/siteverify
cURL
curl -X POST https://cdnshark.com/captcha/siteverify \
-d secret=YOUR_SECRET_KEY \
-d token=THE_SUBMITTED_TOKEN
PHP
$res = json_decode(file_get_contents(
'https://cdnshark.com/captcha/siteverify?' . http_build_query([
'secret' => 'YOUR_SECRET_KEY',
'token' => $_POST['sharkcaptcha_token'] ?? '',
])
), true);
if (empty($res['success'])) {
http_response_code(422);
exit('Human verification failed. Please try again.');
}
// ...continue processing the form...
Node.js
const params = new URLSearchParams({
secret: 'YOUR_SECRET_KEY',
token: req.body.sharkcaptcha_token || '',
});
const r = await fetch('https://cdnshark.com/captcha/siteverify', { method: 'POST', body: params });
const data = await r.json();
if (!data.success) return res.status(422).send('Human verification failed.');JavaScript
// token = the submitted form field "sharkcaptcha_token"
const params = new URLSearchParams({
secret: "YOUR_SECRET_KEY",
token: req.body.sharkcaptcha_token || "",
});
const resp = await fetch("https://cdnshark.com/captcha/siteverify", { method: "POST", body: params });
const data = await resp.json();
if (!data.success) {
return res.status(422).send("Human verification failed.");
}
// verified — process the form
A successful response
{ "success": true, "challenge_ts": "2026-07-22T10:15:00+00:00", "hostname": "cdnshark.com" }
challenge_ts is when the challenge was issued. hostname is the host that verified the
token — it is not the site the visitor was on, so do not use it to check where the solve came from. Restrict
that with the Allowed domains list on the site instead.
A failed response
Failures return HTTP 200 with success: false and an error-codes array:
{ "success": false, "error-codes": ["invalid-secret"] }
| Code | Meaning |
|---|---|
invalid-secret | The secret key is wrong, or the site is inactive or deleted. |
missing-token | No token was posted. Usually the form was submitted before the widget finished, or the field name was changed with data-field. |
malformed-token | The token isn't in the expected format — normally truncation or double-encoding in transit. |
bad-signature | The token was altered after it was issued. |
bad-difficulty | The token claims a difficulty outside the allowed range. Forged token. |
bad-proof | The proof-of-work doesn't satisfy the stated difficulty. Forged token. |
expired | More than 2 minutes passed between solve and verify. Verify on submit, not later. |
replayed | This token was already verified once. Each one is single-use. |
sitekey-mismatch | The token belongs to a different site than the secret key you sent. |
site-suspended | CDNShark support has suspended this site. It stops issuing challenges too. Open a support ticket — changing your own settings will not clear it. |
Treat every one of these the same way in production — reject the submission. The codes are for your logs.
Good to know
- Single use & short-lived. Each token works once and expires after 2 minutes. Verify it right away.
- Allowed domains. If you set a domain list on the site, the widget only runs on those domains.
- Keep the secret secret. Only verify from your backend — never expose the secret key in the browser.
- Difficulty. Raise it in site settings only if you still see spam; the default is a good balance for phones and desktops.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| Widget shows "Verification failed — retry" and never succeeds | The challenge request failed. Open the browser console — the widget logs the exact URL and status it tried.
A 404 against your own domain means the script is being loaded from a copy on your server,
or data-endpoint is set to a path that doesn't exist there; point it at
https://cdnshark.com/captcha/challenge. |
403 domain-not-allowed on the challenge request |
The site's Allowed domains list doesn't include the domain the form is on. A listed domain also covers its subdomains. Add the domain, or clear the list to allow any. |
404 unknown-sitekey |
The site key is mistyped, or the site was deleted or deactivated in your portal. |
429 rate-limited |
More than 120 challenges from one IP in a minute. Normal for load tests, not for real traffic. |
Verify always returns expired |
Your server clock is off, or the token is being verified long after submit. Verify in the request that receives the form. |
Your exact site key, secret key and a ready-to-copy snippet are always on the site's page under CAPTCHA → Manage in your portal.