Skip to content

Contact form and lead notifications

A contact or lead form on a landing page is often the first live touch with a customer. You want to respond fast: a fresh lead replied to within 5 minutes is a very different conversion than “we’ll call you back tomorrow”. A server-side form handler sends an instant push with a short preview and a link to the admin panel. Plus bot protection so the phone doesn’t ring from spam.

  • Server-side only. The form posts to your backend; call notify() from there — you can’t keep the token on the client.
  • Anti-spam is mandatory. A public form is a magnet for bots. At minimum: a honeypot (hidden field) and a per-IP rate limit.
  • Short preview + link. Put the name, a truncated message, and a direct link to the admin panel in the push; everything else we view on click.

We take notify() from the overview page:

import os, requests
def notify(title, message, priority=5):
requests.post(
f"{os.environ['NOTIFLY_URL']}/message",
params={"token": os.environ["NOTIFLY_TOKEN"]},
json={"title": title, "message": message, "priority": priority},
timeout=5,
)

A honeypot field + a per-IP rate limit, then a push with a preview and a link:

const {notify} = require('./notifly');
const lastByIp = new Map(); // ip -> ts, a simple rate limit
const MIN_INTERVAL = 30_000; // no more than 1 lead per 30s from one IP
app.post('/contact', express.json(), async (req, res) => {
const {name = '', email = '', message = '', website = ''} = req.body;
// 1) honeypot: the website field is hidden via CSS; if filled, it's a bot
if (website) return res.json({ok: true}); // pretend we accepted it
// 2) per-IP rate limit
const now = Date.now();
if (now - (lastByIp.get(req.ip) || 0) < MIN_INTERVAL) {
return res.status(429).json({error: 'too fast'});
}
lastByIp.set(req.ip, now);
const lead = await saveLead({name, email, message});
notify(
`📨 Заявка от ${name || email || 'аноним'}`,
`${message.slice(0, 200)}\n\nОткрыть: https://admin.example.com/leads/${lead.id}`,
6
);
res.json({ok: true});
});

In the form markup, a hidden honeypot that humans don’t see but bots fill in:

<form method="post" action="/contact">
<input name="name" placeholder="Имя">
<input name="email" placeholder="Email">
<textarea name="message"></textarea>
<!-- honeypot: hidden via CSS, a real person won't fill it -->
<input name="website" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px">
<button>Отправить</button>
</form>

The same trick on FastAPI:

import time
from collections import defaultdict
from fastapi import FastAPI, Request, Form
from notifly import notify
app = FastAPI()
last_by_ip: dict[str, float] = defaultdict(float)
MIN_INTERVAL = 30 # no more than 1 lead per 30s from one IP
@app.post("/contact")
async def contact(
request: Request,
name: str = Form(""),
email: str = Form(""),
message: str = Form(""),
website: str = Form(""), # honeypot
):
# 1) honeypot filled — it's a bot
if website:
return {"ok": True}
# 2) per-IP rate limit
ip = request.client.host
now = time.time()
if now - last_by_ip[ip] < MIN_INTERVAL:
return {"ok": True} # silently ignore the flood
last_by_ip[ip] = now
lead = await save_lead(name, email, message)
notify(
f"📨 Заявка от {name or email or 'аноним'}",
f"{message[:200]}\n\nОткрыть: https://admin.example.com/leads/{lead['id']}",
6,
)
return {"ok": True}
  • Leads from hot pages (pricing page, “buy”) — a higher priority: this is closer to a sale than a general contact.
  • A daily digest “N leads per day” — quietly (priority 3), to see the flow and not miss a broken form.
  • The form goes silent. If there’s been no lead for N hours when there usually are, the form may be broken; a heartbeat fits well here.
  • A fast reply = higher conversion. A fresh lead in your pocket seconds after submission.
  • Spam doesn’t get through. The honeypot and rate limit filter out bots before the push.
  • Everything at hand. A preview in the notification, the full lead via the admin link.