Что получится: скрипт на Python, который рассылает напоминания о записи. Канал он выбирает по номеру клиента: WhatsApp, если номер там есть, иначе MAX. Пишет только в рабочие часы, держит паузы и соблюдает правила WhatsApp для первых сообщений. Если что-то отложено, скрипт сам повторит отправку позже. Повторный запуск не отправит второе напоминание.
список записей ──► канал по номеру ──► рабочие часы? ──► sendMessage
(checkAccount, нет → позже │
кэш у себя) ├─ 200 → отправлено
├─ 429 → пауза или на завтра
└─ 400 → в отчёт, без повтора
Почему это важно
Мессенджер оценивает номер по поведению. Залпы одинаковых сообщений людям, которые давно не писали, похожи на спам — номер ограничат или заблокируют. Gratio держит базовые правила сам и отвечает 429, когда их нарушают, — приложению остаётся не спорить с ними:
| Правило WhatsApp | code при нарушении |
|---|---|
| Первое сообщение (новый чат или молчание 30 дней) — не чаще раза в минуту, не больше 200 в сутки; новому номеру — 20 в сутки 10 дней | first_message_gap, first_message_limit |
| Одинаковый текст первого сообщения — не больше 10 в час | same_text_limit |
| Без ответа собеседника — до 3 сообщений за 2 минуты и одно напоминание через сутки | no_reply_limit, follow_up_too_early |
| WhatsApp ограничил номеру новые чаты | restricted |
Подробно — WhatsApp и Темп и лимиты.
Что понадобится
- два канала с ключами: WhatsApp и MAX (можно один — тогда без выбора);
- Python 3.10+,
pip install requests; - файл
bookings.csvс записями:
id,phone,name,course,time
1532,79991234567,Мария,Робототехника,2026-10-18 11:00
1533,79997654321,Олег,Python,2026-10-18 12:00
Шаг 1. Каналы и вызов API
Создайте файл remind.py. Ключи каналов — в переменной GRATIO_CHANNELS, по порядку предпочтения:
import csv
import json
import os
import random
import time
from datetime import datetime, timedelta
import requests
API = os.environ.get("GRATIO_URL", "https://api.gratio.net")
# по порядку предпочтения: {"whatsapp": ["7100000001", "токен"], "max": ["7100000002", "токен"]}
CHANNELS: dict[str, list[str]] = json.loads(os.environ["GRATIO_CHANNELS"])
WORK_HOURS = range(10, 20) # пишем с 10:00 до 19:59 по времени клиента
def call(channel: str, method: str, payload: dict) -> requests.Response:
instance, token = CHANNELS[channel]
return requests.post(f"{API}/waInstance{instance}/{method}/{token}", json=payload, timeout=90)
Шаг 2. Канал по номеру
checkAccount ищет аккаунт по номеру. Ответ сохраняем в файл: мессенджеры строго ограничивают поиск по номерам, и проверять одну базу снова и снова нельзя. Допишите в remind.py:
CACHE_FILE = "accounts.json"
cache: dict = json.load(open(CACHE_FILE)) if os.path.exists(CACHE_FILE) else {}
def pick_channel(phone: str) -> tuple[str, str] | None:
for channel in CHANNELS:
key = f"{channel}:{phone}"
if key not in cache:
r = call(channel, "checkAccount", {"phoneNumber": int(phone)})
if r.status_code == 469: # предел поиска — следующий канал
continue
r.raise_for_status()
cache[key] = r.json()["chatId"] if r.json()["exist"] else ""
json.dump(cache, open(CACHE_FILE, "w"))
if cache[key]:
return channel, cache[key]
return None
Шаг 3. Отправка с разбором отказов
clientMessageId — из id записи: повторный запуск скрипта не отправит второе напоминание. Текст личный — с именем и временем, поэтому правило одинаковых текстов не срабатывает. Допишите:
LATER = {"first_message_limit", "restricted", "follow_up_too_early", "no_reply_limit"}
def send_reminder(row: dict) -> str:
target = pick_channel(row["phone"])
if target is None:
return "нет мессенджера"
channel, chat_id = target
text = (f"{row['name']}, напоминаем: завтра в {row['time'][11:]} занятие «{row['course']}». "
"Если планы изменились, ответьте на это сообщение — перенесём.")
for _ in range(3):
r = call(channel, "sendMessage", {"chatId": chat_id, "message": text,
"clientMessageId": f"reminder-{row['id']}"})
if r.status_code == 200:
return f"отправлено в {channel}: {r.json()['idMessage']}"
body = r.json()
if r.status_code == 429 and body.get("code") not in LATER:
time.sleep(int(r.headers.get("Retry-After", 60))) # пауза темпа — ждём и повторяем
continue
if r.status_code == 429:
return f"позже: {body['code']}" # дневной предел — завтра
return f"ошибка {r.status_code} {body.get('code')}: {body.get('message')}"
return "позже: темп"
Шаг 4. Цикл по записям
Допишите — файл готов:
def main() -> None:
if datetime.now().hour not in WORK_HOURS:
print("Вне рабочих часов — запустите позже")
return
tomorrow = (datetime.now() + timedelta(days=1)).date().isoformat()
with open("bookings.csv", encoding="utf-8") as f:
rows = [r for r in csv.DictReader(f) if r["time"].startswith(tomorrow)]
for row in rows:
print(row["id"], row["name"], "→", send_reminder(row))
time.sleep(random.uniform(3, 6)) # свой темп между разными людьми
if __name__ == "__main__":
main()
Шаг 5. Запустите и проверьте
export GRATIO_CHANNELS='{"whatsapp": ["7100000001", "токен"], "max": ["7100000002", "токен"]}'
python remind.py
Вы увидите:
1532 Мария → отправлено в whatsapp: 3EB0C767D26A1D5A6F0B
1533 Олег → отправлено в max: 117428956522361912
Запустите ещё раз — Gratio вернёт те же idMessage, второго сообщения не будет.
Запускайте скрипт по расписанию, например каждый час с 10 до 19: строки «позже» отправятся при следующем запуске.
Как снизить риск блокировки
- Пишите тем, кто ждёт сообщения: клиентам с записью, а не по купленной базе.
- Давайте отказаться: «Ответьте СТОП, если не хотите напоминаний» — и храните отказы у себя.
- Отвечающие клиенты — лучший сигнал для мессенджера. Ответы приходят уведомлениями — см. Текст.
- Новый номер разгоняйте постепенно: первые 10 дней — не больше 20 первых сообщений в сутки (отметьте номер новым в кабинете).
Что дальше
- Узнать, прочитано ли напоминание, — outgoingMessageStatus.
- Ответы клиентов — в своей системе: Переписка в своей CRM.
