Привет, Хабр! Сегодня покажем, как автоматизировать напоминания клиентам компании, переставшим пользоваться ее услугами. Пример: кейс сети салонов красоты.
Во все филиалы сети ежедневно записываются около ста человек. Реактивировать часть потерянных клиентов можно с помощью SMS-информирования о персональной скидке. Но делать это вручную неудобно и долго. Поэтому мы сделали автоматический сценарий: он раз в сутки проверяет базу YClients, находит неактивных клиентов, выбирает лучшее время для отправки сообщения через Умную проверку номера МТС Exolve и отправляет им SMS с предложением вернуться.
Общая схема работы
Решение построено просто: Python‑скрипт, планировщик и два API. Скрипт запускается по расписанию, анализирует базу клиентов и отправляет SMS в часы, когда получатель с наибольшей вероятностью его прочтёт.
Как выглядит сценарий
Планировщик запускает скрипт раз в сутки и получает список клиентов через YClients API
Затем скрипт проверяет даты визитов и выделяет тех, кто не был в салоне красоты после заданной даты
Для каждого номера телефона делает запрос Умной проверки номера через Number Lookup API и получает диапазон лучшего времени, в течение которого человек с наибольшей вероятностью прочтёт сообщение
В середине этого диапазона планируется отправка сообщения через SMS API
После каждой рассылки система сверяет, был ли визит, и при необходимости помечает клиента как неактивного, чтобы не обрабатывать повторно
Старт работы
Скрипт запускается раз в сутки планировщиком. Функция update_filter() пересчитывает критическую дату — порог, после которого клиента считают неактивным. Затем retrieve_by_visits() обращается к API YClients, получает список клиентов и отбирает тех, у которых нет записи о посещении салона красоты после полученной даты. Для каждого из них add_tasks() создаёт задачи на реактивацию: определить удобное время через умную проверку номера, отправить SMS с предложением и позже проверить, вернулся ли клиент.
def main_task():
filt.update_filter()
bad_clients = retrieve_by_visits()
for bc in bad_clients:
add_tasks(bc)
Работа с YClients
Первым делом нужно получить ключ авторизации. В YClients он называется User Token. Для этого откройте личный кабинет системы и создайте приложение разработчика в разделе интеграций в подразделе «Аккаунт разработчика». После регистрации выберите тип «Непубличное», заполните данные и откройте раздел «Доступ к API» — там в поле User Token будет показан ключ для интеграции. Об этом есть описание в официальной документации, но более подробную инструкцию мы нашли в неофициальном источнике.
Получение списка клиентов
Функция query_clients() отправляет запрос к API YClients и получает список клиентов постранично. Константа PER_PAGE определяет размер одной выборки — в коде она равна 200, это максимум. Такой подход снижает нагрузку на сервер и позволяет стабильно обрабатывать большие клиентские базы. Каждый ответ содержит часть данных и метаданные с общим числом клиентов.
PER_PAGE = 200
DAYS_NOT_VISITED = 100
filt = Filter()
def query_clients(pages_counter: int):
url = "https://api.yclients.com/api/v1/company/{}/clients/search".format(CID)
querystring = {"page": pages_counter, "page_size": PER_PAGE}
response = requests.post(url, headers=h, json=querystring)
assert response.status_code == 200
response = response.json()
total_clients = response['meta']['total_count']
clients = response['data']
return clients, total_clients
Проверка визитов клиентов
Функция not_visited() проверяет историю визитов клиента за период от 2000-01-01 до «критической даты». Эта дата вычисляется фильтром и служит порогом давности. Из ответа берётся массив data.records; если он пуст, функция возвращает True, то есть клиент считается неактивным и попадает в очередь на реактивацию.
Такой подход делает фильтрацию проще: функция возвращает только True или False. Благодаря ограниченному диапазону дат запрос выполняется быстро даже при большой базе клиентов.
def not_visited(clid: str):
url = "https://api.yclients.com/api/v1/company/{}/clients/visits/search".format(CID)
querystring = {
"client_id": clid,
"client_phone": None,
"from": "2000-01-01",
"to": filt.critical_date,
"payment_statuses": None,
"attendance": None
}
response = requests.post(url, headers=h, json=querystring)
ans = response.json()
data = ans['data']['records']
return len(data) == 0
Получение карточки клиента
Функция retrieve_client() запрашивает из YClients информацию о клиенте: контактные данные, история визитов и другие поля, которые нужны для отправки сообщений и анализа результата.
Из карточки используются только основные поля:
идентификатор клиента
номер телефона для отправки SMS
имя для персонализации сообщений
дата последнего визита
def retrieve_client(client_id):
url = f"https://api.yclients.com/api/v1/client/{CID}/{client_id}"
response = requests.get(url, headers=h)
assert response.status_code == 200
return response.json()['data']
Фильтрация неактивных клиентов
Функция filter_clients() перебирает список полученных клиентов из YClients и для каждого проверяет, посещал ли он салон красоты после критической даты. Если клиент не приходил, вызывается retrieve_client() для получения полной карточки, и данные добавляются в итоговый список filtered_clients.
def filter_clients(clients: list[dict]):
filtered_clients = []
for c in clients:
idx = c['id']
if not_visited(idx):
full_client_info = retrieve_client(idx)
filtered_clients.append(full_client_info)
return filtered_clients
Постраничная обработка клиентов
В этой функции объединяются все предыдущие шаги и выполняется полная выборка клиентов из YClients. Она проходит по страницам базы, вызывая query_clients() для получения данных и filter_clients() для отбора неактивных пользователей.
Результаты каждой итерации добавляются в общий список bad_clients, который возвращается в конце. В нём остаются только клиенты, не посетившие салон красоты после критической даты.
def retrieve_by_visits():
total_clients = 1
pages_counter = 0
bad_clients = []
while pages_counter*PER_PAGE < total_clients:
pages_counter += 1
clients, total_clients = query_clients(pages_counter)
clients = filter_clients(clients)
bad_clients.extend(clients)
return bad_clients
Фильтрация и работа с критической датой
Система получает список клиентов в формате JSON и проверяет, когда каждый из них перестал посещать салон. Функция not_visited() определяет неактивных, а retrieve_client() добавляет их полные данные в итоговый список.
Класс Filter хранит и пересчитывает критическую дату на основе параметра DAYS_NOT_VISITED, который задаёт, через сколько дней без визитов клиент считается неактивным.
class Filter:
critical_date = "2025-01-01"
def update_filter(self):
crit_date = datetime.today() - timedelta(days=DAYS_NOT_VISITED)
self.critical_date = datetime.date(crit_date).isoformat()
Проверка, вернулся ли клиент
Функция got_visit() запрашивает визиты клиента за период от критической даты до текущего дня. Она обращается к методу clients/visits/search и возвращает True, если в этом диапазоне найдены записи о визите.
def got_visit(clid: str):
url = "https://api.yclients.com/api/v1/company/{}/clients/visits/search".format(CID)
querystring = {
"client_id": clid,
"client_phone": None,
"from": filt.critical_date,
"to": datetime.today().date().isoformat(),
"payment_statuses": None,
"attendance": None
}
response = requests.post(url, headers=h, json=querystring)
ans = response.json()
data = ans['data']['records']
return len(data) > 0
Добавление клиента в планировщик
Функция add_tasks() добавляет клиента в планировщик реактивации. Она получает данные клиента, определяет время, когда ему лучше всего отправить сообщение, и создаёт задачи на отправку SMS и последующую проверку.
Алгоритм работы:
Получить номер телефона клиента
Через Умную проверку номера определить, когда абонент активен
Выбрать середину интервала как оптимальное время
Запланировать отправку SMS
Запланировать проверку, не вернулся ли клиент после рассылки
Задачи распределяются равномерно между текущей и следующей проверкой. Перед каждой отправкой выполняется проверка — если клиент уже пришёл, сообщение не отправляется.
schedule = Scheduler()
def add_tasks(client: dict, df: pd.DataFrame | None = None):
cl_id, number = client['id'], client['phone']
number = number.strip('+')
if df is None:
time_range = get_time_range(number)
sending_time = (time_range['till'] + time_range['since'])/2
else:
sending_time = float(df.loc[number].values)
def send_sms_():
if got_visit(cl_id): return
send_SMS(number)
for ai in range(SMS_NUMBER):
schedule.once(timedelta(days=ai, hours=sending_time), send_sms_)
def mark_client_(): mark_client(cl_id)
schedule.once(timedelta(minutes=SMS_NUMBER+1, seconds=sending_time), mark_client_)
Определение времени отправки SMS
Мы отправляем сообщения в то время, когда клиент с наибольшей вероятностью прочтёт сообщение. Для этого используем Умную проверку номера. Через API можно запросить интервал лучшего времени для одного номера методом GetBestSmsTime или сразу для списка номеров методом GenerateActivityScoreReport.
def get_time(t_str: str):
return datetime.strptime(t_str, '%H:%M:%S').time().hour
def get_time_range(recepient: str):
payload = {'number': recepient}
r = requests.post(r'https://api.exolve.ru/hlr/v1/GetBestSmsTime
', headers={'Authorization': 'Bearer '+exolve_api_key}, data=json.dumps(payload))
print(r.text)
if r.status_code == 200:
ans = json.loads(r.text)
text = ans['result']
elems = text.split(',')
since, till = [get_time(t_str) for t_str in elems]
else:
since, till = 12, 12
ans = {}
ans.update({'since': since, 'till': till})
return ans
В качестве времени отправки сообщения выбираем середину полученного интервала — лучшую же минуту можно найти только экспериментальным путём и на больших данных. Если интервала нет, то используем значение по умолчанию — середина рабочего дня.
При работе с несколькими номерами строку со списком телефонов кодируем в формате base64, разделяя их символом переноса строки. После запроса к методу GenerateActivityScoreReport получаем номер отчёта. Этот идентификатор используется для вызова метода GetHLRReport, который отдаёт результаты проверки — готовый отчёт с интервалами максимальной вероятности прочтения SMS клиентами. В ответ приходит JSON-объект с полем base64, внутри которого хранится CSV-файл.
Мы декодируем файл, читаем его с помощью библиотеки pandas и, если в данных есть столбец с ошибками, удаляем его. Затем заполняем пропуски во втором столбце значениями по умолчанию. Такие пропуски означают, что по этому номеру нет данных о лучшем интервале для отправки сообщения.
def get_multiple_hlr(clients: list[str]):
data = '\n'.join(clients)
st = str(base64.b64encode(data.encode())).replace("b'", '').replace("'", '')
payload = {'numbers': st}
r = requests.post(r'https://api.exolve.ru/hlr/v1/GenerateActivityScoreReport
', headers={'Authorization': 'Bearer '+exolve_api_key}, data=json.dumps(payload))
print(r.text)
def get_times(t_str: str):
elems = t_str.split(',')
since, till = [get_time(e) for e in elems]
return (since + till)/2
assert r.status_code == 200
while True:
r = requests.post(r'https://api.exolve.ru/hlr/v1/GetHLRReport',
headers={'Authorization': 'Bearer ' + exolve_api_key}, data=r.text)
assert r.status_code == 200
ans = json.loads(r.text)
status = int(ans['status'])
assert status < 5
if status == 3 or status == 4:
data = ans['base64']
with open('phones_utf8.txt', 'wt') as f:
f.write(base64.b64decode(data).decode("utf-8"))
my_data = pd.read_csv('phones_utf8.txt').drop('Error', axis=1).set_index('Number').fillna(
value='12:00:00,12:00:00')
return my_data.map(get_times)
time.sleep(10)
Файл с отчётом формируется не сразу, поэтому перед обработкой нужно дождаться его готовности. Состояние отражает поле status в ответе API: значение 3 означает, что отчёт успешно подготовлен, а 4 — что при формировании возникли ошибки. Даже при статусе 4 данные могут быть частично полезными, поэтому система всё равно обрабатывает такой отчёт.
Отчёт сохраняется локально в файл phones_utf8.txt в директории, откуда запускается скрипт. Его можно открыть в любом текстовом или табличном редакторе.
Чтобы использовать этот сценарий, немного изменяем основную функцию — добавляем этап получения интервалов доступности номеров перед созданием задач:
def main_task():
filt.update_filter()
bad_clients = retrieve_by_visits()
df = get_multiple_hlr(bad_clients)
for bc in bad_clients:
add_tasks(bc, df)
Отправка SMS
Функция send_SMS() отправляет SMS клиентам, отобранным для реактивации, через SMS API. Она формирует JSON-запрос с номером отправителя, номером получателя и текстом сообщения, после чего отправляет его методом POST на эндпоинт SendSMS.
send_str = 'Приходите в ближайшие 7 дней и получите скидку 10% на любую услугу.'
def send_SMS(recepient: str):
payload = {'number': exolve_phone, 'destination': recepient, 'text': send_str}
r = requests.post(r'https://api.exolve.ru/messaging/v1/SendSMS', headers={'Authorization': 'Bearer '+sms_api_key}, data=json.dumps(payload))
print(r.text)
return r.text, r.status_code
В примере используем универсальный текст приглашения с общей скидкой.

Проверка реактивации клиента
Функция mark_client() завершает цикл реактивации. Она проверяет, вернулся ли клиент после рассылки, вызывая got_visit(). Если визитов нет, система считает клиента неактивным и добавляет в его карточку через API YClients комментарий «Потерян».
Таким образом, в базе остаётся отметка о том, что клиент не вернулся в течение заданного периода. Это помогает избежать повторной рассылки тем же пользователям и при необходимости работать с ними отдельно — например, вручную или через другой канал.
def mark_client(cl_id):
if got_visit(cl_id): return
url=f'https://api.yclients.com/api/v1/company/{CID}/clients/{cl_id}/comments'
querystring = {'text': 'Потерян'}
response = requests.post(url, headers=h, params=querystring)
Результаты
Вместо ручных разовых рассылок для работы с неактивными клиентами можно построить простой сценарий автоматических сообщений. Скрипт сам проверяет базу, находит нужных клиентов и отправляет им сообщения в подходящее время.
Это позволяет возвращать часть клиентов минимальными усилиями, без участия администратора и высвобождает время для работы с активными клиентами, улучшениями сервиса или анализа продаж.
Что дальше
Базовая версия шлёт только один текст SMS, но сценарий можно развивать:
Добавить индивидуальные скидки в зависимости от количества визитов, типа услуг и суммы трат
Отправлять уведомления в Telegram через бота или в другие каналы
Включить в сообщение ссылку на онлайн-запись, чтобы клиент мог сразу выбрать время
Сделать серию сообщений с напоминанием или альтернативными предложениями
Если тема вам интересна — пишите в комментариях, расскажем подробнее и доработаем решение.
Исходный код на GitHub
Напомним, что все персональные данные клиентов должны быть получены с их согласия.
Evgenym
Если человек больше не приходит в какое-то заведение, значит, у него есть какие-то причины для этого. Может, ушел человек, к которому он ходил, может, нашел лучше/ближе/удобнее. Или вы представляете, что человек забыл, куда ходил, не может вспомнить, а тут приходит волшебная смска и человек вспоминает адрес?
Как по мне, какой-то очередной бесполезный спам-механизм, стоящий в одном ряду со спам-обзвонами.
Katner Автор
Евгений, рассылка уведомлений - традиционный маркетинговый ход. Мы рассказываем, как можно его автоматизировать
Evgenym
А есть статистика, сколько в среднем человек возвращаются/пользуются услугой после получения СМС/звонка?
Katner Автор
По имеющейся у нас информации, услуга привлекает от 2% до 8% клиентов сети салонов красоты.
А вот данные из различных аналитических и отраслевых отчетов (подготовил ИИ):
Общая эффективность SMS-рассылок:
По данным MessageBird, средний показатель конверсии для SMS-маркетинга составляет около 4.5%. Это означает, что из 100 получателей сообщения примерно 4-5 человек совершают целевое действие.
Отчет SimpleTexting за 2024 год показывает, что 35% опрошенных бизнесов отмечают показатель конверсии от 2% до 5% для своих SMS-кампаний. При этом 15% компаний сообщают о конверсии выше 20%, что часто относится к сегментированным рассылкам для лояльных клиентов, куда как раз попадают кампании по возврату "уснувших" клиентов.
Ссылка на данные: https://simpletexting.com/sms-marketing-statistics/
Эффективность именно "win-back" (возвращающих) кампаний:
Согласно исследованию Omnisend, кампании, направленные на повторную активацию клиентов, показывают одну из самых высоких конверсий среди всех типов маркетинговых коммуникаций. Их данные указывают на среднюю конверсию в покупку на уровне 5.8% для автоматизированных цепочек сообщений, нацеленных на неактивных клиентов.
Ссылка на данные: https://www.omnisend.com/automation-statistics/