FORMS · Published 2026-10-06 · Updated 2026-10-06 · SecureSMTP
Spam protection
Captcha providers, rate limits, blocked IPs and the spam check on contact-form email.
SecureSMTP checks form submissions and outgoing email in several ways. This page lists each check, what it looks at and what happens when it fires.
| Check | Applies to | When it fires |
|---|---|---|
| Captcha | Form submissions | Submission dropped: blocked: true. |
| IP limit and IP blocks | Form submissions | Submission dropped: blocked: true. |
| Spam score | Form submissions | Stored as spam, not emailed. |
| Spam score | Outgoing email | Refused: 400 content_flagged. |
| Contact-form spam hold | Contact-form notifications sent through the API or SMTP | Not sent: held: true. |
| AI check | Outgoing email (sampled) | Refused: 400 ai_flagged. |
Captcha
Each site has one captcha setting:
| provider | What it is |
|---|---|
shared | Cloudflare Turnstile run by SecureSMTP. The default for new sites. No keys needed. |
turnstile | Cloudflare Turnstile with your own keys. |
hcaptcha | hCaptcha with your own keys. |
recaptcha | Google reCAPTCHA v2 (the “I’m not a robot” checkbox) with your own keys. |
recaptcha_v3 | Google reCAPTCHA v3 with your own keys. Invisible. A score below 0.5 is rejected. |
none | No captcha. The other checks still run. |
Change it in the dashboard (Sites → Spam protection), on the WordPress plugin’s Spam Protection page, or with POST /sites/captcha.
Read the setting
curl https://securessmtp.com/api/v1/sites/captcha \
-H "x-securessmtp-api-key: $SECURESSMTP_API_KEY"{
"ok": true,
"provider": "turnstile",
"site_key": "0x4AAAAAAAxxxxxxxxxxxxxx",
"widget_js": "https://challenges.cloudflare.com/turnstile/v0/api.js",
"widget_class": "cf-turnstile",
"response_field": "cf-turnstile-response"
}With none, every field except provider is null. The secret key is never returned.
Show the widget
| provider | widget_js | widget_class | response_field |
|---|---|---|---|
shared, turnstile | https://challenges.cloudflare.com/turnstile/v0/api.js | cf-turnstile | cf-turnstile-response |
hcaptcha | https://js.hcaptcha.com/1/api.js | h-captcha | h-captcha-response |
recaptcha | https://www.google.com/recaptcha/api.js | g-recaptcha | g-recaptcha-response |
recaptcha_v3 | https://www.google.com/recaptcha/api.js | g-recaptcha-v3 | g-recaptcha-response |
For Turnstile, hCaptcha and reCAPTCHA v2, load widget_js and put a div with widget_class and data-sitekey inside the form. When the visitor passes, the widget adds a hidden input named response_field to the form.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form method="post" action="/contact">
<input name="name" required>
<input name="email" type="email" required>
<textarea name="message" required></textarea>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Send</button>
</form>reCAPTCHA v3 has no visible widget and adds no field. Load the script with your site key, ask for a token when the form is submitted, and put it in a hidden field yourself:
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<form id="contact" method="post" action="/contact">
<input name="name" required>
<input name="email" type="email" required>
<textarea name="message" required></textarea>
<input type="hidden" name="g-recaptcha-response">
<button type="submit">Send</button>
</form>
<script>
document.getElementById('contact').addEventListener('submit', function (e) {
e.preventDefault();
var form = this;
grecaptcha.ready(function () {
grecaptcha.execute('YOUR_SITE_KEY', { action: 'submit' }).then(function (token) {
form.elements['g-recaptcha-response'].value = token;
form.submit();
});
});
});
</script>Pass the token
On your server, read the field named response_field from the posted form and send it as captcha_token in POST /forms/submit. SecureSMTP checks it with the provider using the secret key saved for the site.
- Rejected token:
{ "ok": true, "blocked": true }. Nothing is stored. - No token: the captcha check is skipped. Always send the token.
- Provider down or slow: the submission goes through and the other checks still run.
Use your own keys
curl -X POST https://securessmtp.com/api/v1/sites/captcha \
-H "x-securessmtp-api-key: $SECURESSMTP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "turnstile",
"site_key": "0x4AAAAAAAxxxxxxxxxxxxxx",
"secret_key": "0x4AAAAAAAyyyyyyyyyyyyyyyyyyyyyyyyy"
}'site_keyis required forturnstile,hcaptcha,recaptchaandrecaptcha_v3.secret_keyis required the first time. Later you can leave it out to keep the saved one. It is stored encrypted (AES-256-GCM) and never returned.- Switching to
sharedornonedeletes any saved keys. - The response has the same shape as the GET response, with the new setting.
- Errors:
site_key_required,secret_key_required_on_first_save(400),encryption_failed(500),invalid_payload(400).
IP limit and IP blocks
These checks use visitor.ip from POST /forms/submit. If you do not send it, they do not run.
- When an IP already has more than 3 submissions in the last 60 seconds, the next one is dropped and the IP is blocked for 1 hour with the reason
rate_limit. - While an IP is blocked, its submissions are dropped and the answer is
{ "ok": true, "blocked": true }. - You can lift a
rate_limitblock yourself. Blocks with other reasons are set by SecureSMTP and need support. See Blocks.
Spam score on form submissions
Each submission gets a score from 0 to 100. Points are added for three or more links (more for six or more), for common spam words, for some domain endings often used by spammers, and for long text written in capitals.
| Score | Class | What happens |
|---|---|---|
| 0–29 | legitimate | Stored and emailed. |
| 30–59 | suspicious | Stored and emailed. An AI check also reads it in the background and can change the class to spam in the Submissions list. |
| 60–100 | spam | Stored, not emailed. The answer is {"ok":true,"id":"…"} with no email_sent. |
If at least 20 submissions reach a site in 24 hours and half or more of them are spam, the site is disabled automatically. You can turn it back on yourself — see Blocks.
Spam score on outgoing email
Email sent with POST /mail/send or SMTP gets the same score, worked out from the subject and the body.
- 60 or more: refused with
400 content_flaggedand logged asflagged. - 30–59: sent. The email log shows
warn:content_spam_score:and the score next to it.
Contact-form spam hold
Contact-form notifications relayed through POST /mail/send or SMTP are checked by the same spam classifier that checks hosted-form submissions. An email counts as a contact-form notification when:
source_pluginis a known form plugin, such ascontact-form-7,wpforms,elementor-pro,gravityforms,fluentform,ninja-forms,forminatororjetpack; or- the subject reads like one, for example “New message”, “Contact form”, “New enquiry” or “Message from …”.
Subjects that look like an auto-reply to the visitor (“Thank you…”, “We received…”, “Your enquiry…”) are not checked.
If the classifier is at least 90% sure the message is spam, the email is held: it is not sent, it is logged as flagged with the reason, and the API answers:
{
"ok": true,
"sent": true,
"held": true,
"reason": "held_as_spam"
}The answer says sent: true on purpose, so the WordPress plugin does not send the spam another way. Over SMTP a held message gets 250. If the classifier fails, the email is sent.
AI check on outgoing email
- It runs on every send while the site has fewer than 100 entries in its email log. After that it runs on about 1 email in 20, chosen from the subject, so the same subject always gets the same decision.
- It does not run on contact-form notifications; those get the spam hold above.
- If it is at least 85% sure the email is spam, the send is refused with
400 ai_flaggedand logged asflagged. - If the check fails or times out, the email is sent.
See Deliverability for what else is checked before an email leaves.