Привет Хабр! Сегодня мы покажем вам, как автоматизировать напоминания клиентам вашей компании, которые перестали пользоваться ее услугами. Пример: случай сети институтов красоты.
Ежедневно во всех филиалах сети регистрируется около ста человек. Некоторых потерянных клиентов можно реактивировать с помощью СМС-уведомлений о персональной скидке. Но делать это вручную неудобно и долго. Поэтому мы создали автоматический скрипт: он раз в день проверяет базу YClients, находит неактивных клиентов, выбирает лучшее время для отправки сообщения через Exolve Smart МТС Проверка номера и отправляет им СМС с предложением возврата.
Содержание
Общая схема работы
Решение построено просто: скрипт Python, планировщик и два API. Скрипт работает по расписанию, анализирует клиентскую базу и отправляет СМС-сообщения в то время, когда получатель с наибольшей вероятностью их прочитает.
Как выглядит сценарий?
-
Планировщик запускает скрипт один раз в день и получает список клиентов через API YClients.
-
Затем скрипт проверяет даты посещения и выделяет тех, кто не пошел в салон красоты после указанной даты.
-
Для каждого номера телефона сделайте запрос на проверку смарт-номера через API поиска номеров и получает диапазон наилучшего времени, когда человек с наибольшей вероятностью прочтет сообщение
-
В середине этого диапазона сообщение планируется отправить через СМС API
-
После каждой отправки система проверяет, было ли посещение, и если да, то помечает клиента как неактивного, чтобы не обрабатывать его повторно.
Начало работы
Скрипт запускается планировщиком один раз в день. Функция update_filter() пересчитывает критическую дату — порог, после которого клиент считается неактивным. Затем get_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» — там ключ интеграции отобразится в поле «Токен пользователя». Описание этого есть в официальная документацияно более подробную инструкцию мы нашли в неофициальный источник.
Получить список клиентов
Функция query_clients() отправляет запрос в API YClients и получает постраничный список клиентов. Константа PER_PAGE определяет размер выборки — в коде он равен 200, это максимум. Такой подход снижает нагрузку на сервер и позволяет последовательно обрабатывать большие клиентские базы данных. Каждый ответ содержит данные и метаданные с общим количеством клиентов.
PER_PAGE = 200
DAYS_NOT_VISITED = 100
filt = Filter()
def query_clients(pages_counter: int):
url = "
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() проверяет историю посещений клиента за период с 01.01.2000 до «критической даты». Эта дата рассчитывается фильтром и служит порогом давности. Таблица data.records берется из ответа; если пусто, функция возвращает True, т.е. клиент считается неактивным и ставится в очередь на повторную активацию.
Такой подход упрощает фильтрацию: функция возвращает только True или False. Благодаря ограниченному диапазону дат запрос обрабатывается быстро даже при большой клиентской базе.
def not_visited(clid: str):
url = "
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 Получение карты клиента
Функция return_client() запрашивает у YClients информацию о клиенте: контактную информацию, историю посещений и другие поля, необходимые для отправки сообщений и анализа результата.
Используются только основные поля карточки:
-
идентификатор клиента
-
номер телефона для отправки СМС
-
имя для персонализации сообщений
-
дата последнего визита
def retrieve_client(client_id):
url = f"
response = requests.get(url, headers=h)
assert response.status_code == 200
return response.json()['data'] Фильтрация неактивных клиентов
Функция filter_clients() просматривает список клиентов, полученный из YClients, и для каждого проверяет, посетили ли они салон красоты после критической даты. Если клиент не приходит, вызывается метод return_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() идентифицирует неактивных клиентов, а return_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() запрашивает посещения клиентов за период от критической даты до текущего дня. Он вызывает метод «клиенты/посещения/поиск» и возвращает значение «Истина», если в этом диапазоне обнаружены какие-либо записи о посещениях.
def got_visit(clid: str):
url = "
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 и последующую проверку.
Алгоритм работы:
-
Получите номер телефона клиента
-
Благодаря смарт-проверке номера определите, когда абонент активен
-
Выберите середину интервала как оптимальное время
-
Запланировать отправку СМС
-
Запланируйте проверку, чтобы узнать, вернулся ли клиент после отправки
Задачи распределяются равномерно между текущим анализом и следующим. Перед каждой отправкой осуществляется проверка: если клиент уже пришел, сообщение не отправляется.
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_) Определить, когда будет отправлено текстовое сообщение
Мы отправляем сообщения тогда, когда клиент, скорее всего, прочитает сообщение. Для этого мы используем проверку смарт-номера. Через API вы можете запросить лучший временной интервал для числа, используя метод GetBestSmsTime или напрямую для списка чисел с помощью метода Создать отчетActivityScoreReport.
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'
', 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, разделяя их символом новой строки. После вызова метода Создать отчетActivityScoreReport получаем номер отчета. Этот идентификатор позволяет вызвать метод ПолучитьHLRReportкоторый предоставляет результаты проверки – готовый отчет с интервалами максимальной вероятности чтения СМС клиентами. Ответ представляет собой объект 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'
', 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'
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)
Файл отчета не создается сразу, поэтому перед его обработкой необходимо дождаться его готовности. Статус отражается в поле статуса в ответе API: значение 3 означает, что отчет был подготовлен успешно, а 4 означает, что при формировании возникли ошибки. Даже при статусе 4 данные могут оказаться частично полезными, поэтому система все равно обрабатывает такой отчет.
Отчет сохраняется локально в файле Telephone_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) Отправка СМС
Функция send_SMS() отправляет SMS-сообщения клиентам, выбранным для повторной активации, через СМС API. Он генерирует запрос JSON с номером отправителя, номером получателя и текстом сообщения, а затем отправляет его с помощью метода POST в конечную точку. Отправить СМС.
send_str="Приходите в ближайшие 7 дней и получите скидку 10% на любую услугу."
def send_SMS(recepient: str):
payload = {'number': exolve_phone, 'destination': recepient, 'text': send_str}
r = requests.post(r' headers={'Authorization': 'Bearer '+sms_api_key}, data=json.dumps(payload))
print(r.text)
return r.text, r.status_code В примере мы используем универсальный текст приглашения с общей скидкой.
Проверка реактивации клиента
Функция mark_client() завершает цикл реактивации. Он проверяет, вернулся ли клиент после трансляции, вызывая got_visit(). Если посещений нет, система считает клиента неактивным и через YClients API добавляет к его файлу комментарий «Потерян».
Таким образом, в базе данных остается отметка о том, что клиент не вернулся в отведенное время. Это позволяет избежать повторной отправки сообщений одним и тем же пользователям и при необходимости работать с ними отдельно — например вручную или через другой канал.
def mark_client(cl_id):
if got_visit(cl_id): return
url=f'
querystring = {'text': 'Потерян'}
response = requests.post(url, headers=h, params=querystring) Результаты
Вместо разовых ручных рассылок для работы с неактивными клиентами можно создать простой скрипт для автоматических сообщений. Скрипт сам проверяет базу данных, находит нужных клиентов и отправляет им сообщения в подходящее время.
Это позволяет привлекать определенных клиентов с минимальными усилиями, без участия администратора, а также освобождает время для работы с активными клиентами, улучшения обслуживания или анализа продаж.
Что дальше
Базовая версия отправляет только одно SMS, но скрипт можно расширить:
-
Добавляйте индивидуальные скидки в зависимости от количества посещений, типа услуг и суммы трат.
-
Отправляйте уведомления в Telegram через бота или другие каналы.
-
Включите в сообщение ссылку на онлайн-бронирование, чтобы клиент мог сразу выбрать время
-
Сделайте серию сообщений с напоминаниями или альтернативными предложениями.
Если вам интересна тема, пишите в комментариях, мы расскажем более подробно и доработаем решение.
Исходный код для GitHub.
Помните, что все персональные данные клиентов должны быть получены с их согласия.

