Quick Answer
Before saving a contact form message, POST it to https://rsflowhub.com/api/v1/ai/spam-detection with requests. If is_spam is true and spam_score is above about 0.8, drop it; between 0.5 and 0.8, keep it in a review folder. Combine this with a honeypot field and per-IP rate limiting.
Contact forms are a magnet for SEO spam, fake offers and phishing links. CAPTCHAs annoy real visitors and are increasingly solved by bots. In this tutorial you will add AI spam detection to a Python contact form in Flask or Django, so spam is filtered by what the message says rather than by how the visitor clicks.
What contact form spam actually looks like
Before choosing a defence it helps to know what you are filtering. Most spam that reaches a public contact form falls into a few groups, and each one slips past a different simple defence:
- Automated link spam: a bot posts a block of text with links to a pill shop, casino or counterfeit store. A honeypot field usually catches these because bots fill every input they find.
- "Services" outreach: a human or a script offers SEO, web design or lead generation. The message is polite, grammatically fine and has no obvious bad words, so keyword blocklists miss it.
- Phishing and scams: fake invoices, "your account is suspended" notices and crypto offers that try to get one of your team to click a link.
- Hand-typed spam: low-paid workers paste a template into thousands of forms. These pass any CAPTCHA because a real person solves it.
The last two groups are why content-based detection matters. A model that reads the message can notice that a "partnership proposal" is really a link-building pitch, no matter who or what typed it.
A CAPTCHA proves a visitor is probably human. It says nothing about what that human wrote. Content analysis answers the question you actually care about: is this message worth my team's time? The two approaches work well together, but a honeypot plus AI detection already removes most spam without adding friction for real visitors.
1. Write a spam check helper
import os
import requests
RSFLOWHUB_URL = "https://rsflowhub.com/api/v1/ai/spam-detection"
def check_spam(text: str) -> dict | None:
"""Returns {is_spam, spam_score, spam_category, reason} or None if unavailable."""
try:
response = requests.post(
RSFLOWHUB_URL,
headers={"X-API-Key": os.environ["RSFLOWHUB_KEY"]},
json={"text": text[:5000]},
timeout=30,
)
response.raise_for_status()
return response.json()["data"]["result"]
except requests.RequestException:
return None
The result includes is_spam, a spam_score between 0 and 1, a spam_category such as promotional or phishing, and a short reason you can show to whoever reviews borderline messages.
Two details in this helper are deliberate. The text is cut to 5,000 characters because that is the maximum the endpoint accepts, and the function returns None on any network problem instead of raising. That lets the calling code choose what to do when the check is unavailable, which is the most important design decision in the whole integration (more on that in the thresholds section).
2. Use it in a Flask contact form
from flask import Flask, request, jsonify
from spam_check import check_spam
app = Flask(__name__)
@app.post("/contact")
def contact():
form = request.get_json() or request.form
message = (form.get("message") or "").strip()
# Honeypot: real users never fill this hidden field.
if form.get("website"):
return jsonify(ok=True)
verdict = check_spam(message)
status = "inbox"
if verdict and verdict["is_spam"]:
status = "spam" if verdict["spam_score"] >= 0.8 else "review"
if status != "spam":
save_message(form.get("email"), message, status=status)
return jsonify(ok=True)
The endpoint always answers ok so bots learn nothing from the response, while real messages land in your inbox or a review folder.
3. Use it in a Django form
from django import forms
from .spam_check import check_spam
class ContactForm(forms.Form):
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea, max_length=5000)
website = forms.CharField(required=False) # honeypot, hide with CSS
def clean(self):
data = super().clean()
if data.get("website"):
raise forms.ValidationError("Something went wrong. Please try again.")
verdict = check_spam(data.get("message", ""))
data["spam_status"] = "inbox"
if verdict and verdict["is_spam"]:
data["spam_status"] = "spam" if verdict["spam_score"] >= 0.8 else "review"
return data
In your view, save messages with spam_status and only notify your team for the inbox ones. For high-traffic sites, run check_spam in a Celery task so the form responds instantly.
4. Tune the request for contact forms
The basic call only sends text, but the endpoint accepts a few optional fields that make the verdict more useful for a contact form:
| Field | What it does | Suggested value |
|---|---|---|
content_type | Tells the model where the text came from, so it knows what a normal message looks like | "contact_form" |
strictness | low, medium or high; how aggressively borderline text is flagged | Start at medium, raise it if too much gets through |
detect_toxicity | Adds is_toxic and toxicity_score for abusive messages | true if staff read the inbox |
return_sanitized | Returns sanitized_text with links, profanity and personal data replaced by [REMOVED] | true for the review folder |
payload = {
"text": text[:5000],
"content_type": "contact_form",
"strictness": "medium",
"detect_toxicity": True,
"return_sanitized": True,
}
With return_sanitized on, you can show reviewers the cleaned version of a borderline message, so nobody has to click a suspicious link just to decide whether it is spam.
5. Run the check in a background task
An AI check usually takes one to two seconds. That is fine for a form that shows a spinner, but on busy sites you may prefer to accept the message immediately and screen it afterwards. The pattern is: save the message as pending, queue a task, and update the status when the verdict arrives.
from celery import shared_task
from .models import Message
from .spam_check import check_spam
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def screen_message(self, message_id: int):
message = Message.objects.get(pk=message_id)
verdict = check_spam(message.body)
if verdict is None:
# API unavailable: retry later, and fall back to the inbox after the last try.
if self.request.retries >= self.max_retries:
message.status = "inbox"
message.save(update_fields=["status"])
return
raise self.retry()
if verdict["is_spam"] and verdict["spam_score"] >= 0.8:
message.status = "spam"
elif verdict["is_spam"]:
message.status = "review"
else:
message.status = "inbox"
message.spam_score = verdict["spam_score"]
message.spam_reason = verdict.get("reason", "")
message.save()
Storing spam_score and reason alongside each message pays off later. When someone asks why a genuine enquiry was filtered, you can answer in seconds, and you can look at the score distribution to adjust your thresholds with real data.
6. Thresholds and layered protection
- Score of 0.8 and above: discard silently.
- 0.5 to 0.8: keep in a review folder and check it weekly.
- If the API is unavailable: accept the message; never lose a real lead because of a timeout.
- Add cheap layers first: a honeypot field and per-IP rate limiting stop most bots before they cost an API call.
Rate limiting is the easiest layer to add. In Flask, the Flask-Limiter extension limits how often one address can post:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(get_remote_address, app=app)
@app.post("/contact")
@limiter.limit("5 per hour")
def contact():
...
Five messages per hour from one address is generous for a real visitor and useless for a bot trying to submit thousands. Together, the honeypot, the rate limit and the content check form three independent layers: a bot must beat all of them, and each one costs less than the layer behind it.
For comment sections it is safer to hold content when moderation is unavailable. For a contact form the opposite is true: a lost enquiry can be a lost customer. If the check fails, accept the message and flag it for review.
7. Test the filter before you trust it
Before connecting the filter to your live form, run a small set of messages through the helper and look at the scores. A short test file keeps this repeatable:
from spam_check import check_spam
SAMPLES = {
"genuine": "Hi, we are looking for a quote for 40 licences and would like a call next week.",
"seo_pitch": "We can put your website on page 1 of Google in 30 days. Reply for our packages.",
"phishing": "Your invoice #88231 is overdue. Open http://secure-pay-check.example to avoid suspension.",
"casino": "WIN BIG!!! best online casino bonus 500% click here now",
}
for label, text in SAMPLES.items():
verdict = check_spam(text)
print(label, "->", verdict and (verdict["is_spam"], verdict["spam_score"], verdict["spam_category"]))
Scores vary from run to run, so treat the output as guidance rather than a fixed expectation. What you are looking for is that the genuine enquiry is clearly not spam and that the other three score well above your discard threshold. If a real message from your own customers scores above 0.5, add it to your test file and raise the threshold or lower strictness.
8. Common mistakes to avoid
- Telling bots what happened. If you return an error for spam and a success for real messages, bots learn to adapt. Return the same response in both cases.
- Calling the API from the browser. Your API key would be visible to every visitor. Always call it from your server.
- Deleting instead of storing. For the first few weeks keep everything, including discarded spam, with its score. You cannot tune thresholds without examples.
- Checking empty or tiny messages. Skip the call for messages shorter than a few characters; they are not worth an API request.
- Forgetting the character limit. The endpoint accepts up to 5,000 characters, so cut longer text before sending it, as the helper does.
- Relying on a single layer. The honeypot, rate limit and AI check each fail alone. The combination is what makes the form hard to abuse.
Filter Spam by What Messages Say
Spam detection is a utility endpoint covered by the free monthly quota on every RSFlowHub plan.
View Spam Detection API →Key Takeaways
- Check message content with AI instead of relying only on CAPTCHAs.
- Use spam_score thresholds: discard high, review medium.
- Return the same response for spam and real messages so bots learn nothing.
- Never drop messages when the API is unavailable.
- Combine with a honeypot and rate limiting for cheap first-line defence.
- Store the score and reason with every message so you can tune thresholds with real data.
- Use content_type and strictness to adapt the check to a contact form.
No comments yet
Be the first to share your thoughts on this article!