CDNShark Documentation

CAPTCHA

← Back to Documentation

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:

KeyWhere 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

AttributeValuesDescription
data-sitekeyyour site keyRequired.
data-themeauto (default), light, darkMatch your site. auto follows the visitor's system setting.
data-callbackfunction nameOptional. Called with the token when verification succeeds.
data-appearanceinteraction (default), autointeraction waits for the visitor to click the checkbox. auto starts solving as soon as the page loads.
data-brandingon (default), offoff hides the CDNShark logo and the Privacy · Terms links.
data-fieldinput nameName of the hidden field the token is written to. Default sharkcaptcha_token.
data-endpointURLWhere 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-logoimage URLBrand 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"] }
CodeMeaning
invalid-secretThe secret key is wrong, or the site is inactive or deleted.
missing-tokenNo token was posted. Usually the form was submitted before the widget finished, or the field name was changed with data-field.
malformed-tokenThe token isn't in the expected format — normally truncation or double-encoding in transit.
bad-signatureThe token was altered after it was issued.
bad-difficultyThe token claims a difficulty outside the allowed range. Forged token.
bad-proofThe proof-of-work doesn't satisfy the stated difficulty. Forged token.
expiredMore than 2 minutes passed between solve and verify. Verify on submit, not later.
replayedThis token was already verified once. Each one is single-use.
sitekey-mismatchThe token belongs to a different site than the secret key you sent.
site-suspendedCDNShark 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

SymptomCause & 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.