Skip to content
WebShield Docs

Bots sign up and spam through forms

How to ask WebShield for a score of every sign-up or lead from your backend and filter out fake accounts, spam and SMS pumping.

Through sign-up and lead forms bots create fake accounts, send junk and trigger paid SMS to random numbers. The form fields alone cannot tell a bot from a person, and a captcha checks every visitor alike.

The fix is form protection: a tag on your pages observes the visit, and at submission your backend asks us for a score and gets a verdict. What to do with the verdict is up to your code.

What you get: every sign-up or lead gets an allow, challenge or deny verdict with reasons; bots are filtered out, and doubtful visitors get a check — only them, not everyone. You need: access to the site’s code — both the page template and the form handler on the server. You don’t need to move the domain to us. Time: an hour or two including the rollout.

Services → Form protection → Create project. In Allowed site addresses enter the scheme and domain without a path — https://example.com and https://www.example.com. You get a site key and ready-to-paste tag code.

The backend needs an API token with the Form protection: write scope. Keep it with your application secrets, next to the database password.

In the <head> of every page, not only the one with the form:

<script src="https://af.webshield.pro/t/v1.js?k=YOUR_KEY"
data-forms="#signup" async></script>

data-forms lists the CSS selectors of your forms, comma-separated. The tag adds a hidden ws_sid field — the visitor’s session ID — to each of them. The tag goes on every page because the visit before the form is part of the score too.

Check: open the page with the form and find <input type="hidden" name="ws_sid"> in the developer tools — within a second or two it gets a value.

In the form handler, before creating the user and sending an SMS, send ws_sid to us.

PHP:

function ws_decision(string $sid, string $event): string
{
$ch = curl_init('https://webshield.pro/api/v1/antifraud/score');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WS_TOKEN'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'site_key' => getenv('WS_SITE_KEY'),
'session_id' => $sid,
'event' => $event,
]),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
// Our failure must not break your sign-up.
if ($body === false || $code !== 200) {
return 'allow';
}
return json_decode($body, true)['decision'] ?? 'allow';
}
$decision = ws_decision($_POST['ws_sid'] ?? '', 'signup');

Node.js:

async function wsDecision(sid, event) {
try {
const resp = await fetch("https://webshield.pro/api/v1/antifraud/score", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ site_key: process.env.WS_SITE_KEY, session_id: sid, event }),
signal: AbortSignal.timeout(3000),
});
if (!resp.ok) return "allow";
return (await resp.json()).decision ?? "allow";
} catch {
// Our failure must not break your sign-up.
return "allow";
}
}
const decision = await wsDecision(req.body.ws_sid ?? "", "signup");

A Python example is in the reference.

event names the action, and you choose it: signup, login, contact, sms. Give different forms different names: rates are counted for each separately.

Don’t turn on refusals on day one. For a week just record the verdict next to each sign-up and see whom it would cut off. The report in the project card shows the same from our side: the share of automation, the reasons that fire, and recent sessions.

Once the verdicts earn your trust, act on them:

if ($decision === 'deny') {
http_response_code(403);
exit('Sign-up rejected.');
}
if ($decision === 'challenge') {
// No outright refusal: email confirmation, manual moderation,
// no SMS until confirmed — whatever is cheaper for you and doesn't bother a person.
}
// allow — sign up as usual.

The most valuable rule against SMS pumping: send a paid message on nothing but allow. For challenge an email or a visitor check will do.

5. Turn on the check for doubtful visitors

Section titled “5. Turn on the check for doubtful visitors”

The project card has a single switch — whether to show the visitor a check. When it is on, on a doubtful verdict the tag itself shows a short picture task over the page and submits the form once it is solved. Your code changes nothing: the next score request already accounts for the solved task.

While the check is off, the API verdict is still real — the switch only affects what the visitor sees.

  • Sign up yourself from a regular browser: the verdict is allow.
  • Submit the form from curl without ws_sid: the verdict is challenge with the no_session reason — a request that bypasses the browser does not go unnoticed.
  • Sessions and events show up in the project report.
Symptom Cause and fix
Everyone gets challenge with no_session ws_sid does not reach the backend. The selector in data-forms does not match the form, or your JavaScript submits the form without the hidden field — see manual form binding.
403 from the API The token lacks the Form protection: write scope or has expired.
The report is empty, ws_sid stays blank The page address is not in Allowed site addresses — check the scheme and www.
A second form on the page isn’t protected The tag is installed twice. Keep one and list both forms in data-forms, comma-separated.
A mobile app gets challenge The app has no tag and so no session. Give it a separate handler with its own logic.