Перейти к содержимому

Уведомления о заявках с формы

Контактная форма или форма заявки на лендинге — часто первый живой контакт с клиентом. Отвечать хочется быстро: свежий лид, ответивший через 5 минут, — это совсем другой конверт, чем «перезвоним завтра». Serverside-обработчик формы шлёт мгновенный пуш с коротким превью и ссылкой в админку. Плюс защита от ботов, чтобы телефон не звенел от спама.

  • Только на сервере. Форма постится на ваш бэкенд, notify() вызываем оттуда — токен на клиенте держать нельзя.
  • Анти-спам обязателен. Публичная форма — магнит для ботов. Минимум: honeypot (скрытое поле) и rate-limit по IP.
  • Короткое превью + ссылка. В пуш — имя, обрезанное сообщение и прямую ссылку в админку; всё остальное смотрим по клику.

Берём notify() из обзорной страницы:

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,
)

Honeypot-поле + rate-limit по IP, затем пуш с превью и ссылкой:

const {notify} = require('./notifly');
const lastByIp = new Map(); // ip -> ts, простой rate-limit
const MIN_INTERVAL = 30_000; // не чаще 1 заявки в 30с с одного IP
app.post('/contact', express.json(), async (req, res) => {
const {name = '', email = '', message = '', website = ''} = req.body;
// 1) honeypot: поле website скрыто в CSS; заполнено — это бот
if (website) return res.json({ok: true}); // делаем вид, что приняли
// 2) rate-limit по IP
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});
});

В разметке формы — скрытый honeypot, который люди не видят, а боты заполняют:

<form method="post" action="/contact">
<input name="name" placeholder="Имя">
<input name="email" placeholder="Email">
<textarea name="message"></textarea>
<!-- honeypot: скрыто через CSS, реальный человек не заполнит -->
<input name="website" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px">
<button>Отправить</button>
</form>

Тот же приём на 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 # не чаще 1 заявки в 30с с одного IP
@app.post("/contact")
async def contact(
request: Request,
name: str = Form(""),
email: str = Form(""),
message: str = Form(""),
website: str = Form(""), # honeypot
):
# 1) honeypot заполнен — это бот
if website:
return {"ok": True}
# 2) rate-limit по IP
ip = request.client.host
now = time.time()
if now - last_by_ip[ip] < MIN_INTERVAL:
return {"ok": True} # тихо игнорируем флуд
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}
  • Заявки с горячих страниц (страница тарифов, «купить») — приоритет повыше: это ближе к продаже, чем общий контакт.
  • Ежедневный дайджест «N заявок за сутки» — тихо (приоритет 3), чтобы видеть поток и не пропустить провал формы.
  • Форма молчит. Если за N часов ни одной заявки, хотя обычно они есть, — возможно, форма сломана; сюда хорошо ложится heartbeat.
  • Быстрый ответ = выше конверсия. Свежий лид в кармане через секунды после отправки.
  • Спам не долетает. Honeypot и rate-limit отсеивают ботов до пуша.
  • Всё под рукой. Превью в уведомлении, полная заявка — по ссылке в админку.