Bot and Spam Protection

7 views Markdown

The ten endpoints that stop unwanted traffic with a bot shield, a captcha and spam checks.

Overview

These ten endpoints stop unwanted traffic in three separate layers. The bot shield counts repeated attempts and cuts them off. The captcha puts a check in front of forms. The spam protection looks at what was submitted and at the visitor's reputation.

The three are independent and each has its own switch. Configuring one layer leaves the others alone, so protecting a form means switching on the right layer.

Reference

Reading the Bot Shield

get/api/v1/admin/settings/security/bot-shield
Settings/GetBotShield admin

Returns when repeated failed attempts get stopped.

Response fields data — 3
statusintWhether the shield is on.
within_timeobjectThe window the attempts are counted in. A map from period name to minutes, and only one period is kept.
attemptsobjectHow many attempts each protected operation allows.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/settings/security/bot-shield' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/bot-shield', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/bot-shield');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The attempt count is kept PER OPERATION: lowering it for one leaves the others alone.
$shield = Api::Settings()->GetBotShield()['data'];
$signIn = $shield['attempts']['sign-in'] ?? null;

Writing the Bot Shield

put/api/v1/admin/settings/security/bot-shield
Settings/UpdateBotShield admin

Switches the shield on and writes the counting window and the attempt limits.

Body 3
statusintSwitches the shield on or off.
within_timeobjectThe counting window. One period is accepted, and what you send replaces the old one.
attemptsobjectThe attempt limit per operation.
Response fields data — 3
statusintWhether the shield is on.
within_timeobjectThe window the attempts are counted in. A map from period name to minutes, and only one period is kept.
attemptsobjectHow many attempts each protected operation allows.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/settings/security/bot-shield' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":1,"within_time":{"hour":60},"attempts":{"sign-in":5}}'
const res = await fetch('https://panel.example.com/api/v1/admin/settings/security/bot-shield', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    status: 1,
    within_time: { hour: 60 },
    attempts: { 'sign-in': 5 },
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/bot-shield');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'status'      => 1,
        'within_time' => ['hour' => 60],
        'attempts'    => ['sign-in' => 5],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Set the limit too low and REAL clients behind a shared connection get blocked as well.
Api::Settings()->UpdateBotShield([
    'status'      => 1,
    'within_time' => ['hour' => 60],
    'attempts'    => ['sign-in' => 5],
]);

Reading the Captcha Settings

get/api/v1/admin/settings/security/captcha
Settings/GetCaptcha admin

Returns which provider is used and which forms are protected.

Response fields data — 3
statusintWhether the captcha is on.
typestringThe provider in use.
protected_areasobjectThe protected forms. A map from area name to whether it is on.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/settings/security/captcha' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/captcha', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/captcha');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// With the main switch off the protected list still reads FULL while none of it is enforced.
$cap  = Api::Settings()->GetCaptcha()['data'];
$live = $cap['status'] === 1;

Writing the Captcha Settings

put/api/v1/admin/settings/security/captcha
Settings/UpdateCaptcha admin

Writes the provider, the protected forms and the provider's own settings.

Body 4
statusintSwitches the captcha on or off.
typestringThe provider to use. The key comes from the provider list.
protected_areasarray | objectThe forms to protect: contact-form, sign-up, sign-in, sign-forget, customer-feedback, newsletter, domain-check, software-license. Either a list or a map.
configobjectThe provider's own settings. Written only when you send it.
Response fields data — 3
statusintWhether the captcha is on.
typestringThe provider in use.
protected_areasobjectThe protected forms. A map from area name to whether it is on.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/settings/security/captcha' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":1,"type":"DefaultCaptcha","protected_areas":["sign-in","sign-up"]}'
const res = await fetch('https://panel.example.com/api/v1/admin/settings/security/captcha', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    status: 1,
    type: 'DefaultCaptcha',
    protected_areas: ['sign-in', 'sign-up'],
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/captcha');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'status'          => 1,
        'type'            => 'DefaultCaptcha',
        'protected_areas' => ['sign-in', 'sign-up'],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Changing provider REQUIRES its own settings: a provider with no keys breaks the form.
$fields = Api::Settings()->GetCaptchaFields(['module' => 'ReCaptcha'])['data'];

Api::Settings()->UpdateCaptcha([
    'status' => 1,
    'type'   => 'ReCaptcha',
    'config' => ['site_key' => $siteKey, 'secret_key' => $secret],
]);

Listing the Providers

get/api/v1/admin/settings/security/captcha/modules
Settings/GetCaptchaModules admin

Returns the captcha providers installed.

Response fields data[] — 4
keystringThe provider key. This is what the write endpoint takes.
namestringThe provider name.
descriptionstringWhat it does.
activeboolWhether it is the one in use.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/settings/security/captcha/modules' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/captcha/modules', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/captcha/modules');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The built-in provider comes FIRST in the list and needs no settings; the others need keys.
$modules = Api::Settings()->GetCaptchaModules()['data'];

Reading a Provider's Fields

get/api/v1/admin/settings/security/captcha/{module}/fields
Settings/GetCaptchaFields admin

Returns which settings a provider wants and what is stored for them.

Response fields data — 3
modulestringThe provider key.
fieldsarrayThe raw definition of the settings wanted. They differ between providers.
configobjectThe stored setting values.
Errors 3
module_required422No provider name was given.
not_found404No such provider.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/settings/security/captcha/ReCaptcha/fields' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/captcha/ReCaptcha/fields', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/captcha/ReCaptcha/fields');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Learn what a provider wants from here BEFORE switching to it.
$needs = Api::Settings()->GetCaptchaFields(['module' => 'ReCaptcha'])['data']['fields'];

Reading the Spam Protection

get/api/v1/admin/settings/security/spam
Settings/GetSpamProtection admin

Returns the word filter, the outside reputation service and the proxy check.

Response fields data — 6
word_liststringThe words that get blocked.
api_statusintWhether the outside reputation service is on.
api_keystringThe service key.
api_risk_scoreintThe risk score above which a visitor is blocked.
block_temporaryintWhether the temporary block is on.
contact_check_proxyintWhether visitors arriving through a proxy are checked.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/settings/security/spam' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/spam', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/spam');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The outside service does not run WITHOUT A KEY: it can read as on while no lookup happens.
$spam = Api::Settings()->GetSpamProtection()['data'];
$live = $spam['api_status'] === 1 && $spam['api_key'] !== '';

Writing the Spam Protection

put/api/v1/admin/settings/security/spam
Settings/UpdateSpamProtection admin

Writes the word filter and the outside reputation check.

Body 6
word_liststringThe words to block.
api_statusintTurns the outside reputation service on.
api_keystringThe service key. The service does not run without it.
api_risk_scoreintThe blocking threshold. A lower value blocks more visitors.
block_temporaryintTurns the temporary block on.
contact_check_proxyintChecks visitors arriving through a proxy.
Response fields data — 6
word_liststringThe words that get blocked.
api_statusintWhether the outside reputation service is on.
api_keystringThe service key.
api_risk_scoreintThe risk score above which a visitor is blocked.
block_temporaryintWhether the temporary block is on.
contact_check_proxyintWhether visitors arriving through a proxy are checked.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/settings/security/spam' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"word_list":"spam,scam","block_temporary":1}'
const res = await fetch('https://panel.example.com/api/v1/admin/settings/security/spam', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    word_list: 'spam,scam',
    block_temporary: 1,
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/spam');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'word_list'       => 'spam,scam',
        'block_temporary' => 1,
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Turning the service on needs a KEY; send both or the check quietly does nothing.
Api::Settings()->UpdateSpamProtection([
    'api_status'     => 1,
    'api_key'        => $key,
    'api_risk_score' => 25,
]);

Listing What Was Blocked

get/api/v1/admin/settings/security/spam-records
Settings/GetSpamRecords admin

Returns the requests blocked recently and the total number blocked.

Response fields data[] + meta
data[]objectThe records blocked recently. Only a recent slice is kept, not the whole history.
total_blockedintThe total number blocked. It comes back under meta and can exceed how many rows the list holds.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/settings/security/spam-records' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/spam-records', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/spam-records');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The counter and the list are NOT the same thing: the total can far exceed what the list shows.
$rec   = Api::Settings()->GetSpamRecords();
$shown = count($rec['data']);
$total = $rec['meta']['total_blocked'];

Clearing What Was Blocked

delete/api/v1/admin/settings/security/spam-records
Settings/ClearSpamRecords admin the counter resets too

Clears the blocked records and resets the total counter.

Response fields data — 1
clearedboolWhether the clear ran.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/settings/security/spam-records' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch('https://panel.example.com/api/v1/admin/settings/security/spam-records', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/security/spam-records');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The clear resets the COUNTER as well: how many were blocked over the installation's life is lost.
// Read it and keep it on your side first.
$total = Api::Settings()->GetSpamRecords()['meta']['total_blocked'];
Api::Settings()->ClearSpamRecords();

Pitfalls

With the main switch off the protected areas do nothing

While the captcha's main switch is off, the protected form list still reads full and none of it is enforced. Adding a form to that list does not protect it; the main switch has to be on as well. The same holds for the bot shield.

Changing provider requires its settings

The built-in captcha runs with no settings; the others need keys. Switching provider without supplying them breaks the forms: the check never loads and clients cannot submit. Read what the provider wants from the fields endpoint before switching.

The reputation service quietly does nothing without a key

Switching the outside reputation service on is not enough on its own: with an empty key no lookup happens and no visitor is checked. The setting reads as on and no error is raised. Send both together, then watch whether the blocked records start growing.

A tight limit cuts off real clients too

The bot shield counts attempts by address. Clients behind a shared connection, in an office or a school, appear as one address; with a low limit they spend each other's attempts and none of them gets in. When lowering the limit, weigh the counting window with it.

The clear resets the counter as well

Clearing the blocked records does not merely empty the list: the total counter of everything blocked over the installation's life is reset with it. That number never comes back, so read it and keep it on your side first. The list itself only holds a recent slice anyway, and the total can be far larger.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.