Привет Хабр! Сегодня мы покажем вам, как автоматизировать напоминания клиентам вашей компании, которые перестали пользоваться ее услугами. Пример: случай сети институтов красоты.

Ежедневно во всех филиалах сети регистрируется около ста человек. Некоторых потерянных клиентов можно реактивировать с помощью СМС-уведомлений о персональной скидке. Но делать это вручную неудобно и долго. Поэтому мы создали автоматический скрипт: он раз в день проверяет базу YClients, находит неактивных клиентов, выбирает лучшее время для отправки сообщения через Exolve Smart МТС Проверка номера и отправляет им СМС с предложением возврата.

Общая схема работы

Решение построено просто: скрипт Python, планировщик и два API. Скрипт работает по расписанию, анализирует клиентскую базу и отправляет СМС-сообщения в то время, когда получатель с наибольшей вероятностью их прочитает.

Как выглядит сценарий?

  1. Планировщик запускает скрипт один раз в день и получает список клиентов через API YClients.

  2. Затем скрипт проверяет даты посещения и выделяет тех, кто не пошел в салон красоты после указанной даты.

  3. Для каждого номера телефона сделайте запрос на проверку смарт-номера через API поиска номеров и получает диапазон наилучшего времени, когда человек с наибольшей вероятностью прочтет сообщение

  4. В середине этого диапазона сообщение планируется отправить через СМС API

  5. После каждой отправки система проверяет, было ли посещение, и если да, то помечает клиента как неактивного, чтобы не обрабатывать его повторно.

Начало работы

Скрипт запускается планировщиком один раз в день. Функция 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, т.е. клиент считается неактивным и ставится в очередь на повторную активацию.

ЧИТАТЬ  Мы связываем себя с калифорнийскими общественными колледжами, чтобы вооружить миллионы студентов и учителей Google AI.

Такой подход упрощает фильтрацию: функция возвращает только 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 и последующую проверку.

ЧИТАТЬ  Microsoft Research: маркетинг и продажи будут работать с ИИ, а не конкурировать с ним | Маршировать

Алгоритм работы:

  1. Получите номер телефона клиента

  2. Благодаря смарт-проверке номера определите, когда абонент активен

  3. Выберите середину интервала как оптимальное время

  4. Запланировать отправку СМС

  5. Запланируйте проверку, чтобы узнать, вернулся ли клиент после отправки

Задачи распределяются равномерно между текущим анализом и следующим. Перед каждой отправкой осуществляется проверка: если клиент уже пришел, сообщение не отправляется.

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 данные могут оказаться частично полезными, поэтому система все равно обрабатывает такой отчет.

ЧИТАТЬ  Рост подлинности: почему подлинные связи будут стимулировать социальные сети в 2025 году

Отчет сохраняется локально в файле 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.

Помните, что все персональные данные клиентов должны быть получены с их согласия.

Source