# Bot and Spam Protection

https://dev.wisecp.com/es/bot-and-spam-protection

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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/security/bot-shield' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/bot-shield', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
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}}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/security/captcha' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/captcha', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
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"]}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/security/captcha/modules' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/captcha/modules', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/security/captcha/ReCaptcha/fields' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/security/spam' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/spam', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
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}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/security/spam-records' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/security/spam-records', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/settings/security/spam-records' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// 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.

## Related Articles

- [Security Settings](https://dev.wisecp.com/en/security-settings)
- [Authentication Settings](https://dev.wisecp.com/en/authentication-settings)
- [Client Registration](https://dev.wisecp.com/en/client-registration)
