так, смотри, есть у меня бот в телеграм, когда я захожу в "Получить sms", то мне выводится инлайн клавиатура с сервисами, но выводятся абослютно все сервисы, которые есть у меня в bot/constants/services.py. Я хочу сделать так, чтобы выводилась точно такая же клавиатура, но чтобы были реально доступные сервисы исходя из настроек страны указанной у пользовталея в настройках. логику ты можешь взять по сути из следующщего этапа, когда пользовтаель уже нажимает на кокнертный сервис, то происходит запрос кол-ва номеров на сервис и выводится весь список доступных сервисов и номеров для них, я высылаю тебе все необходимое, внеси нужные измнения в необходимые файлы и пришли мне готовый полный исправленный вариант текстом. я тебе высылаю сам хендлер, апи и документацию к апи. сервисы должны отображаться как и сейчас, т.е. такая же клавиатура из инлайн кнопок и так же чтобы учитывалась их популярность.

import asyncio
import logging
import time
import json
from pathlib import Path
from typing import Optional, List, Tuple
from aiogram import Dispatcher, types, F
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
Импорт дополнительных модулей
from ..data import get_user_api_key, get_user_operator, get_user_country
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message
from .start import send_main_menu
from .activation import user_activations, poll_for_sms
from ..constants.operators import OPERATOR_DISPLAY_NAMES
from ..constants.countries import COUNTRIES
from ..constants.services import SERVICE_DISPLAY_NAMES

logger = logging.getLogger(name)

class SmsOrderState(StatesGroup):
waiting_for_service = State()
waiting_for_max_price = State()

SERVICES: List[Tuple[str, str]] = list(SERVICE_DISPLAY_NAMES.items())
POPULARITY_FILE = Path("service_popularity.json")

def load_popularity_data():
if POPULARITY_FILE.exists():
with open(POPULARITY_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {}

def save_popularity_data(popularity_data):
with open(POPULARITY_FILE, 'w', encoding='utf-8') as f:
json.dump(popularity_data, f, ensure_ascii=False, indent=4)

def update_service_popularity(service_code: str):
popularity_data = load_popularity_data()
if service_code in popularity_data:
popularity_data[service_code] += 1
else:
popularity_data[service_code] = 1
save_popularity_data(popularity_data)

def get_sorted_services():
popularity_data = load_popularity_data()
sorted_services = sorted(SERVICES, key=lambda service: popularity_data.get(service[0], 0), reverse=True)
return sorted_services

def get_service_pages(services: List[Tuple[str, str]], page_size: int = 18) -> List[List[Tuple[str, str]]]:
pages = []
for i in range(0, len(services), page_size):
pages.append(services[i:i + page_size])
return pages

def build_service_keyboard(page_number: int = 0) -> InlineKeyboardBuilder:
sorted_services = get_sorted_services()
services_pages = get_service_pages(sorted_services)
keyboard = InlineKeyboardBuilder()


if page_number < len(services_pages):
    current_page = services_pages[page_number]

    # Добавляет кнопки сервисов в сетке 3x6
    for code, name in current_page:
        keyboard.button(text=name, callback_data=f"service_select:{code}")
    
    # Настраивает кнопки навигации
    if page_number == 0:
        # Первая страница - только кнопка "Вперед"
        keyboard.adjust(3)  # Устраиваем сервисные кнопки в 3 столбца
        keyboard.row(types.InlineKeyboardButton(text="➡️ Вперед", callback_data=f"service_page:{page_number + 1}"))
    elif page_number == len(services_pages) - 1:
        # Последняя страница - только кнопка "Назад"
        keyboard.adjust(3)
        keyboard.row(types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"service_page:{page_number - 1}"))
    else:
        # Промежуточные страницы - кнопки "Назад" и "Вперед"
        keyboard.adjust(3)
        keyboard.row(
            types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"service_page:{page_number - 1}"),
            types.InlineKeyboardButton(text="➡️ Вперед", callback_data=f"service_page:{page_number + 1}")
        )

# Кнопка "Назад в меню" на всех страницах
keyboard.row(types.InlineKeyboardButton(text="🔙 Назад в меню", callback_data="back_to_main_menu"))

return keyboard
async def get_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")


user_api_key = get_user_api_key(callback_query.from_user.id)
if not user_api_key:
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="⚙️ Настройки", callback_data="settings")
    keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
    keyboard.adjust(1)
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    return

logger.info(f"Пользователь {callback_query.from_user.id} начал процесс получения номера.")
keyboard = build_service_keyboard()

await state.update_data(cancel_request=False, current_task=None)

await safe_edit_or_send_message(
    message=callback_query.message,
    new_text=(
        "Выберите сервис, нажав на кнопку ниже.\n"
        "Если вы хотите, вы также можете ввести полное название сервиса или хотя бы часть его названия, чтобы получить результаты поиска."
    ),
    state=state,
    reply_markup=keyboard.as_markup()
)
await state.set_state(SmsOrderState.waiting_for_service)
async def go_back_to_service_selection(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
logger.info(f"Пользователь {callback_query.from_user.id} отменил операцию.")


await state.update_data(cancel_request=True)

data = await state.get_data()
current_task = data.get('current_task')
if current_task:
    current_task.cancel()

await safe_edit_or_send_message(
    message=callback_query.message,
    new_text=(
        "Выберите сервис, нажав на кнопку ниже.\n"
        "Если вы хотите, вы также можете ввести полное название сервиса или хотя бы часть его названия, чтобы получить результаты поиска."
    ),
    state=state,
    reply_markup=build_service_keyboard().as_markup()
)

await state.set_state(SmsOrderState.waiting_for_service)
async def back_to_main_menu(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")
logger.info(f"Пользователь {callback_query.from_user.id} вернулся в главное меню.")


await state.set_state(None)
await send_main_menu(callback_query.message, state)
async def service_page_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")


page_number = int(callback_query.data.split(":")[1])
keyboard = build_service_keyboard(page_number)

await safe_edit_or_send_message(
    message=callback_query.message,
    new_text=(
        "Выберите сервис, нажав на кнопку ниже.\n"
        "Если вы хотите, вы также можете ввести полное название сервиса или хотя бы часть его названия, чтобы получить результаты поиска."
    ),
    state=state,
    reply_markup=keyboard.as_markup()
)
async def service_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")


service_code = callback_query.data.split(":")[1]
update_service_popularity(service_code)

await state.update_data(selected_service=service_code, cancel_request=False)
task = asyncio.create_task(service_handler_logic(
    callback_query.message, state, user_id=callback_query.from_user.id, service_code=service_code, is_service_selection=True))
await state.update_data(current_task=task)
async def service_handler(message: types.Message, state: FSMContext, service_code: Optional[str] = None, user_id: Optional[int] = None):
try:
await message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение пользователя: {e}")


user_input = message.text.strip().lower()

if user_id is None:
    user_id = message.from_user.id

logger.info(f"Пользователь {user_id} ввел сервис: {user_input}")

service_code = next((code for code, name in SERVICE_DISPLAY_NAMES.items() if name.lower() == user_input or code.lower() == user_input), None)

if service_code is None:
    matched_services = [(code, name) for code, name in SERVICE_DISPLAY_NAMES.items() if user_input in name.lower()]

    if matched_services:
        keyboard = InlineKeyboardBuilder()
        for code, name in matched_services:
            keyboard.button(text=name, callback_data=f"service_select:{code}")

        keyboard.adjust(3)
        keyboard.button(text="🔙 Назад", callback_data="service_page:0")
        await safe_edit_or_send_message(
            message=message,
            new_text="Пожалуйста, выберите один из представленных сервисов:",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return
    else:
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="🔙 Назад к сервисам", callback_data="service_page:0")
        await safe_edit_or_send_message(
            message=message,
            new_text="Сервис не найден. Попробуйте ввести код или полное название сервиса.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return
else:
    await state.update_data(selected_service=service_code, cancel_request=False)
    task = asyncio.create_task(service_handler_logic(message, state, user_id=user_id, service_code=service_code))
    await state.update_data(current_task=task)
async def service_handler_logic(message: types.Message, state: FSMContext, user_id: int, service_code: str, is_service_selection: bool = False):
logger.info(f"Пользователь {user_id} выбрал сервис: {service_code}")


user_api_key = get_user_api_key(user_id)
user_operator = get_user_operator(user_id) or "any"
user_country_code = get_user_country(user_id) or "0"
country_name = COUNTRIES.get(user_country_code, user_country_code)

if not user_api_key:
    await safe_edit_or_send_message(
        message=message,
        new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
        state=state
    )
    await state.set_state(None)
    await send_main_menu(message, state)
    return

smshub_api = SmsHubAPI(user_api_key)
usd_to_rub_rate = await get_usd_to_rub_rate()

max_attempts = 3
success = False
last_exception = None

try:
    for attempt in range(1, max_attempts + 1):
        data = await state.get_data()
        if data.get("cancel_request"):
            logger.info(f"Запрос для пользователя {user_id} был отменен.")
            return

        try:
            progress_text = f"Идет запрос {attempt}/{max_attempts}..."
            keyboard = InlineKeyboardBuilder()
            keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
            keyboard.adjust(1)

            await safe_edit_or_send_message(
                message=message,
                new_text=progress_text,
                state=state,
                reply_markup=keyboard.as_markup()
            )

            price_info = await smshub_api.get_price_for_service(service_code, country=user_country_code)
            numbers_status = await smshub_api.get_numbers_status(country=user_country_code, operator=user_operator)

            if data.get("cancel_request"):
                logger.info(f"Запрос для пользователя {user_id} был отменен после получения данных.")
                return

            if price_info and numbers_status:
                service_key = f"{service_code}_0"
                available_numbers = int(numbers_status.get(service_key, 0) or 0)
                
                if available_numbers > 0:
                    prices = [float(price_str) for price_str in price_info.keys() if price_str is not None]
                    if prices:
                        min_price_usd = min(prices)
                        max_price_usd = max(prices)
                        min_price_rub = min_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0
                        max_price_rub = max_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0

                        operator_display = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)
                        service_display = SERVICE_DISPLAY_NAMES.get(service_code, service_code)

                        keyboard = InlineKeyboardBuilder()
                        keyboard.button(text=f"⬇️ Мин. цена ({min_price_usd:.4f}$)", callback_data=f"select_price:{min_price_usd}")
                        keyboard.button(text=f"⬆️ Макс. цена ({max_price_usd:.4f}$)", callback_data=f"select_price:{max_price_usd}")
                        keyboard.adjust(2)
                        keyboard.row(types.InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_operation"))

                        message_text = (
                            f"*Страна:* `{country_name}`\n"
                            f"*Доступно номеров:* `{available_numbers}`\n"
                            f"*Оператор:* `{operator_display}`\n"
                            f"*Сервис:* `{service_display}`\n"
                            f"*Цены:* от `{min_price_usd:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_usd:.4f}` $ (₽{max_price_rub:.2f}).\n\n"
                            f"_Выберите максимальную допустимую стоимость покупки с помощью кнопок ниже или введите сумму вручную (например: 0.5). Все суммы указаны в долларах ($):_"
                        )

                        await safe_edit_or_send_message(
                            message=message,
                            new_text=message_text,
                            state=state,
                            reply_markup=keyboard.as_markup(),
                            parse_mode="Markdown"
                        )

                        await state.update_data(
                            service=service_code,
                            service_display=service_display,
                            country_name=country_name,
                            available_numbers=available_numbers,
                            operator_display=operator_display,
                            min_price=min_price_usd,
                            max_price_usd=max_price_usd,
                            min_price_rub=min_price_rub,
                            max_price_rub=max_price_rub,
                            usd_to_rub_rate=usd_to_rub_rate,
                            user_id=user_id
                        )

                        await state.set_state(SmsOrderState.waiting_for_max_price)
                        success = True
                        break
                    else:
                        logger.info(f"Попытка {attempt}: Цены не найдены.")
                        last_exception = "Цены не найдены"
                else:
                    logger.info(f"Попытка {attempt}: Нет доступных номеров.")
                    last_exception = "Нет доступных номеров"

        except Exception as e:
            logger.error(f"Ошибка при получении данных для сервиса '{service_code}', попытка {attempt}: {e}")
            last_exception = e

        await asyncio.sleep(5)

except asyncio.CancelledError:
    logger.info(f"Задача для пользователя {user_id} была отменена.")
    return

if not success:
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="Назад", callback_data="service_page:0")
    await safe_edit_or_send_message(
        message=message,
        new_text=f"Произошла ошибка при обработке запроса: {last_exception}. Попробуйте снова.",
        state=state,
        reply_markup=keyboard.as_markup()
    )
async def price_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.answer()
except Exception as e:
logger.warning(f"Не удалось ответить на callback_query: {e}")


price = float(callback_query.data.split(":")[1])
await state.update_data(max_price=price)

await process_number_purchase(callback_query.message, state)
async def max_price_handler(message: types.Message, state: FSMContext):
max_price_input = message.text.strip()
data = await state.get_data()
user_id = data.get('user_id', message.from_user.id)


try:
    await message.delete()
except Exception as e:
    logger.error(f"Не удалось удалить сообщение пользователя: {e}")

logger.info(f"Пользователь {user_id} указал максимальную стоимость: {max_price_input}")

try:
    max_price = float(max_price_input)
except ValueError:
    user_data = await state.get_data()
    min_price = user_data.get('min_price')
    max_price_usd = user_data.get('max_price_usd')
    min_price_rub = user_data.get('min_price_rub')
    max_price_rub = user_data.get('max_price_rub')

    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
    keyboard.adjust(1)
    await safe_edit_or_send_message(
        message=message,
        new_text=(
            f"Пожалуйста, введите корректное число для максимальной стоимости. "
            f"От `{min_price:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_usd:.4f}` $ (₽{max_price_rub:.2f})."
        ),
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )
    return

if max_price < data.get('min_price'):
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
    keyboard.adjust(1)
    await safe_edit_or_send_message(
        message=message,
        new_text=f"Максимальная стоимость должна быть не менее `{data.get('min_price'):.4f}` $.",
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )
    return

await state.update_data(max_price=max_price)

await process_number_purchase(message, state)
async def process_number_purchase(message: types.Message, state: FSMContext):
user_data = await state.get_data()
user_id = user_data.get('user_id', message.from_user.id)
service = user_data['service']
service_display = user_data['service_display']
country_name = user_data['country_name']
operator_display = user_data['operator_display']
max_price = user_data['max_price']
user_api_key = get_user_api_key(user_id)
smshub_api = SmsHubAPI(user_api_key)
user_country_code = get_user_country(user_id) or "0"


max_attempts = 3
last_exception = None

try:
    for attempt in range(1, max_attempts + 1):
        data = await state.get_data()

        if data.get("cancel_request"):
            logger.info(f"Запрос на получение номера для пользователя {user_id} был отменен.")
            return
        
        progress_text = f"Идет запрос {attempt}/{max_attempts} на получение номера..."
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
        keyboard.adjust(1)

        await safe_edit_or_send_message(
            message=message,
            new_text=progress_text,
            state=state,
            reply_markup=keyboard.as_markup()
        )

        user_operator = get_user_operator(user_id) or "any"

        if data.get("cancel_request"):
            logger.info(f"Запрос на получение номера для пользователя {user_id} был отменен перед выполнением.")
            return

        result = await smshub_api.get_number(service, country=user_country_code, max_price=max_price, operator=user_operator)

        if data.get("cancel_request"):
            logger.info(f"Запрос на получение номера для пользователя {user_id} был отменен.")
            return

        if result and result.startswith("ACCESS_NUMBER"):
            _, activation_id, number = result.split(":")
            logger.info(f"Пользователь {user_id} получил номер: {number}, активация ID: {activation_id}")

            if user_id not in user_activations:
                user_activations[user_id] = {}
            user_activations[user_id][activation_id] = {
                'number': number,
                'status': 'Ожидание SMS',
                'service': service,
                'service_display': service_display,
                'country_name': country_name,
                'last_update': time.time(),
                'task': None
            }

            keyboard = InlineKeyboardBuilder()
            keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
            keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
            keyboard.button(text="🔙 Главное меню", callback_data="back_to_main_menu")
            keyboard.adjust(1)

            message_text = (
                f"*Получен номер:* `{number}`\n"
                f"*Страна:* `{country_name}`\n"
                f"*Оператор:* `{operator_display}`\n"
                f"*Сервис:* `{service_display}`\n"
                f"*Максимальная стоимость:* `{max_price:.2f}` $ (₽{max_price * user_data['usd_to_rub_rate']:.2f})\n"
                f"*Статус:* *Ожидание SMS*\n"
                f"Вы можете управлять активацией или вернуться в главное меню."
            )

            sent_message = await safe_edit_or_send_message(
                message=message,
                new_text=message_text,
                state=state,
                reply_markup=keyboard.as_markup(),
                parse_mode="Markdown"
            )
            user_activations[user_id][activation_id]['message_id'] = sent_message.message_id

            await state.set_state(None)

            task = asyncio.create_task(poll_for_sms(
                bot=message.bot,
                user_id=user_id,
                chat_id=message.chat.id,
                activation_id=activation_id,
                smshub_api=smshub_api
            ))
            user_activations[user_id][activation_id]['task'] = task
            return

        elif result == "NO_NUMBERS":
            logger.info(f"Попытка {attempt} из {max_attempts}: нет доступных номеров.")
            last_exception = result
            if attempt == max_attempts:
                suggestion = (
                    "Попробуйте выбрать 'Любой оператор' в настройках или увеличить максимальную цену."
                    if user_operator != "any" else
                    "Попробуйте увеличить максимальную цену или повторите попытку позже."
                )
                keyboard = InlineKeyboardBuilder()
                keyboard.button(text="Назад", callback_data="service_page:0")
                await safe_edit_or_send_message(
                    message=message,
                    new_text=f"Нет доступных номеров для оператора {operator_display} по данной цене.\n{suggestion}",
                    state=state,
                    reply_markup=keyboard.as_markup()
                )
                return
        else:
            raise Exception("Неожиданный ответ API")

        await asyncio.sleep(5)

except asyncio.CancelledError:
    logger.info(f"Задача для пользователя {user_id} была отменена.")
    return

if last_exception and last_exception != "NO_NUMBERS":
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="Назад", callback_data="service_page:0")
    await safe_edit_or_send_message(
        message=message,
        new_text=f"Произошла ошибка при обработке запроса: {last_exception}. Попробуйте снова.",
        state=state,
        reply_markup=keyboard.as_markup()
    )
def register_sms_handlers(dp: Dispatcher):
dp.callback_query.register(get_sms_handler, F.data == "get_sms")
dp.callback_query.register(go_back_to_service_selection, F.data == "cancel_operation")
dp.callback_query.register(back_to_main_menu, F.data == "back_to_main_menu")
dp.callback_query.register(service_page_handler, F.data.startswith("service_page:"))
dp.callback_query.register(service_selected_handler, F.data.startswith("service_select:"))
dp.callback_query.register(price_selected_handler, F.data.startswith("select_price:"))
dp.message.register(service_handler, SmsOrderState.waiting_for_service)
dp.message.register(max_price_handler, SmsOrderState.waiting_for_max_price)

/bot/api.py
import aiohttp
import logging
import json
from typing import Optional, Dict

logger = logging.getLogger(name)

BASE_URL = "https://smshub.org/stubs/handler_api.php"

class SmsHubAPI:
def init(self, api_key: str):
self.api_key = api_key


async def get_balance(self, currency: str = "643") -> Optional[str]:
    """Запрос баланса с заданной валютой (по умолчанию: RUB)."""
    logger.info(f"Запрос баланса с валютой: {currency}")
    params = {
        "api_key": self.api_key,
        "action": "getBalance",
        "currency": currency
    }
    async with aiohttp.ClientSession() as session:
        async with session.get(BASE_URL, params=params) as response:
            if response.status == 200:
                text = await response.text()
                logger.info(f"Ответ баланса: {text}")
                return text
            else:
                logger.error(f"Ошибка при запросе баланса: {response.status} - {text}")
                return None

async def get_number(self, service: str, country: str, operator: Optional[str] = None, max_price: Optional[float] = None, currency: str = "643") -> Optional[str]:
    """Получение номера для указанного сервиса и страны."""
    logger.info(f"Запрос номера для сервиса: {service}, страна: {country}, оператор: {operator}, макс. цена: {max_price}, валюта: {currency}")
    params = {
        "api_key": self.api_key,
        "action": "getNumber",
        "service": service,
        "country": country,
        "currency": currency
    }
    if operator and operator != "any":
        params["operator"] = operator
    if max_price:
        params["maxPrice"] = max_price

    async with aiohttp.ClientSession() as session:
        async with session.get(BASE_URL, params=params) as response:
            text = await response.text()
            if response.status == 200:
                logger.info(f"Ответ номера: {text}")
                return text
            logger.error(f"Ошибка при запросе номера: {response.status} - {text}")
            return None

async def get_price_for_service(self, service: str, country: str, currency: str = "643") -> Optional[Dict]:
    """Получение цен для указанного сервиса."""
    logger.info(f"Запрос цен для сервиса: {service}, страна: {country}, валюта: {currency}")
    params = {
        "api_key": self.api_key,
        "action": "getPrices",
        "service": service,
        "country": country,
        "currency": currency
    }

    async with aiohttp.ClientSession() as session:
        async with session.get(BASE_URL, params=params) as response:
            response_text = await response.text()
            if response.status == 200:
                try:
                    prices_data = json.loads(response_text)
                    logger.info(f"Цены для сервиса {service}: {prices_data}")
                    country_data = prices_data.get(country)
                    if not country_data:
                        logger.error(f"Страна '{country}' отсутствует в данных: {prices_data}")
                        return None
                    service_data = country_data.get(service)
                    if not service_data:
                        logger.error(f"Сервис '{service}' отсутствует среди доступных для страны '{country}'. Данные: {country_data}")
                        return None
                    return service_data  # Возвращает словарь { "Цена": Количество }
                except json.JSONDecodeError:
                    logger.error(f"Ошибка парсинга JSON: {response_text}")
                    return None
            else:
                logger.error(f"Ошибка при запросе цен: {response.status} - {response_text}")
                return None

async def get_numbers_status(self, country: str, operator: Optional[str] = None) -> Optional[Dict]:
    """Получение статуса номеров по стране и оператору."""
    logger.info(f"Запрос статуса номеров для страны: {country}, оператор: {operator}")
    params = {
        "api_key": self.api_key,
        "action": "getNumbersStatus",
        "country": country
    }
    if operator and operator != "any":
        params["operator"] = operator
    async with aiohttp.ClientSession() as session:
        async with session.get(BASE_URL, params=params) as response:
            response_text = await response.text()
            if response.status == 200:
                try:
                    numbers_status = json.loads(response_text)
                    logger.info(f"Статус номеров: {numbers_status}")
                    return numbers_status  # Возвращает словарь { "SERVICE_CODE": Количество }
                except json.JSONDecodeError:
                    logger.error(f"Ошибка парсинга JSON: {response_text}")
                    return None
            else:
                logger.error(f"Ошибка при запросе статуса номеров: {response.status} - {response_text}")
                return None

async def get_status(self, activation_id: str) -> Optional[str]:
    """Получение статуса активации по ID."""
    logger.info(f"Запрос статуса для активации: {activation_id}")
    params = {
        "api_key": self.api_key,
        "action": "getStatus",
        "id": activation_id
    }
    async with aiohttp.ClientSession() as session:
        async with session.get(BASE_URL, params=params) as response:
            text = await response.text()
            if response.status == 200:
                logger.info(f"Ответ статуса: {text}")
                return text
            logger.error(f"Ошибка при запросе статуса: {response.status} - {text}")
            return None

async def set_status(self, activation_id: str, status: str) -> Optional[str]:
    """Установка статуса для активации."""
    logger.info(f"Изменение статуса активации: {activation_id} на статус {status}")
    params = {
        "api_key": self.api_key,
        "action": "setStatus",
        "id": activation_id,
        "status": status
    }
    async with aiohttp.ClientSession() as session:
        async with session.get(BASE_URL, params=params) as response:
            text = await response.text()
            if response.status == 200:
                logger.info(f"Ответ изменения статуса: {text}")
                return text
            logger.error(f"Ошибка при изменении статуса: {response.status} - {text}")
            return None

async def cancel_activation(self, activation_id: str) -> Optional[str]:
    """Отмена активации."""
    return await self.set_status(activation_id, status="8")
Внимание! Добавлена поддержка валюты.
Методы getBalance, getNumber, getPrices теперь принимают параметр currency

Параметр currency принимает два значения 643, 840 (цифровой код валюты ₽ и $ по стандарту ISO)

Также добавлен новый метод updateApiCurrency, который задает значение по умолчанию, если currency не передано

Если в метод не передан параметр currency и не было задано значение с помощью updateApiCurrency, мы используем значение по умолчанию заданное в нашей системе

API - это протокол взаимодействия между вашим ПО и нашим сервером активаций.

API нужен для того, чтобы автоматизировать процесс получение смс сообщений на вашей стороне

Все запросы (поддерживаются POST и GET) должны идти на https://smshub.org/stubs/handler_api.php

Во всех запросах должен присутствовать API ключ в виде параметра "api_key"

Запрос количества доступных номеров:
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getNumbersStatus&country=COUNTRY&operator=OPERATOR
Входные параметры
Параметр Принимаемые значения Обязательность Описание
APIKEY APIKEY Да Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
COUNTRY См. список стран в левом меню Нет Страна номера, если не задан, то будет выбран номер той страны, которая последний раз была выбрана в левом меню
OPERATOR Список доступных операторов можно найти в левом меню соответсвующей страны Нет Оператор номера, если не задан, то будет выбран тот оператор, который последний раз была выбран в левом меню
Ответ сервиса будет в json формате, пример:

{"apd_0":200,"amy_0":249,"aly_0":246,"tu_0":214,"aho_0":115,"ky_0":86,"apn_0":214,"ajs_0":66,"yd_0":205,"ayc_0":27}

Где до черточки - название сервиса, после обозначение нужно ли включать переадресацию. 0 - не включать. 1 - включать. Номеров с переадресацией мы к сожалению пока не предоставляем

Запрос баланса
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getBalance&currency=CURRENCY
Входные параметры
Параметр Принимаемые значения Обязательность Описание
APIKEY APIKEY Да Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
CURRENCY 643, 840 Нет Если передано - возвращает значение соответствующего баланса.
Выходные данные
Ответ сервера Пример Описание
ACCESS_BALANCE:BALANCE ACCESS_BALANCE:540 BALANCE - Баланс вашего API-ключа
Возможные ошибки
Ответ сервера Описание
BAD_KEY Неверный API-ключ
ERROR_SQL ошибка SQL-сервера
BAD_ACTION Общее неправильное формирование запроса
BAD_CURRENCY Неверное значение currency
Заказ номера
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getNumber&service=SERVICE&operator=OPERATOR&country=COUNTRY&maxPrice=MAXPRICE&currency=CURRENCY
Входные параметры
Параметр Принимаемые значения Обязательность Описание
APIKEY APIKEY Да Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
SERVICE См. список сервисов в левом меню Да Сервис, номер для которого необходимо получить
OPERATOR Принимаемые параметры зависят от конкретной страны, для уточнения обратите внимание на левое меню Нет Сотовый оператор номер которого необходимо получить
COUNTRY См. список стран в левом меню Нет Страна номер которой необходимо получить
MAXPRICE Числовое значение Нет Максимальная цена, за которую вы готовы купить номер по Free Price
CURRENCY 643, 840 Нет С какого баланса будет списана стоимость номера.
Ответы сервиса
Ответ сервера Пример Описание
NO_NUMBERS NO_NUMBERS Нет номеров с заданными параметрами, попробуйте позже, или поменяйте оператора, страну.
NO_BALANCE NO_BALANCE Закончились деньги на API-ключе
API_KEY_NOT_VALID API_KEY_NOT_VALID Невалидный статус API-ключа
WRONG_SERVICE WRONG_SERVICE Не верный идентификатор сервиса
ACCESS_NUMBER:ID:NUMBER ACCESS_NUMBER:234242:79123456789 Получили номер, ID активации - ID, сам номер с кодом страны - NUMBER
Возможные ошибки
Ответ сервера Описание
BAD_ACTION Общее неправильное формирование запросаО
BAD_SERVICE некорректное наименование сервиса
BAD_KEY Неверный API-ключ
ERROR_SQL Ошибка базы SQL-сервера
BAD_CURRENCY Неверное значение currency
Изменить статус
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=setStatus&status=STATUS&id=ID
Входные параметры
Параметр Принимаемые значения Обязательность Описание
APIKEY APIKEY Да Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
ID Числовое значение Да ID активации, полученное при запросе номера
STATUS 1 - смс на номер отправлено, 3 - необходимо повторно смс, 6 - активация успешно завершена, 8 - отменить активацию Да Статус который необходимо передать активации
Какой статус когда отправлять?

Простая логика взаимодействия с API

Сразу после получения номера доступны следующие действия:
8 - Отменить активацию
1 - Сообщить, что SMS отправлена (необязательно)
Для активации со статусом 1:
8 - Отменить активацию
Сразу после получения кода:
3 - Запросить еще одну смс
6 - Подтвердить SMS-код и завершить активацию
Для активации со статусом 3:
6 - Подтвердить SMS-код и завершить активацию
Выходные данные
Ответ сервера Описание
ACCESS_READY Готовность ожидания смс
ACCESS_RETRY_GET Ожидаем новое смс
ACCESS_ACTIVATION Активация успешно завершена
ACCESS_CANCEL Активация отменена
Возможные ошибки
Ответ сервера Описание
BAD_ACTION Общее неправильное формирование запроса
BAD_SERVICE некорректное наименование сервиса
BAD_KEY Неверный API-ключ
NO_ACTIVATION id активации не существует
ERROR_SQL Ошибка базы SQL-сервера, обратитесь к администратору
Получить статус
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getStatus&id=ID
Входные параметры
Параметр Принимаемые значения Обязательность Описание
APIKEY APIKEY Да Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
ID Числовое значение Да ID активации, полученное при запросе номера
Выходные данные
Ответ сервера Описание
STATUS_WAIT_CODE Ожидаем прихода смс
STATUS_WAIT_RETRY:LASTCODE Ожидаем еще одно смс, LASTCODE - последнее полученное смс
STATUS_CANCEL активация отменена
STATUS_OK:CODE Код получен (где CODE - код активации)
Возможные ошибки
Ответ сервера Описание
BAD_ACTION Общее неправильное формирование запроса
BAD_KEY Неверный API-ключ
NO_ACTIVATION id активации не существует
ERROR_SQL Ошибка базы SQL-сервера, обратитесь к администратору
Запросить все цены
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=getPrices&service=SERVICE&country=COUNTRY&currency=CURRENCY
Входные параметры
Параметр Принимаемые значения Обязательность Описание
APIKEY APIKEY Да Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
SERVICE См. список сервисов в левом меню Нет Сервис, номер для которого необходимо получить
COUNTRY См. список стран в левом меню Нет Страна номера
CURRENCY 643, 840 Нет Если передано - метод вернет стоимость в соответствующей валюте.
Выходные данные
Ответ сервера Описание
JSON ответ Ответ в формате { "Страна": { "Сервис": { "Цена": Количество}}}
Возможные ошибки
Ответ сервера Описание
BAD_CURRENCY Неверное значение currency
Изменить валюту по умолчанию
https://smshub.org/stubs/handler_api.php?api_key=APIKEY&action=updateApiCurrency&&currency=CURRENCY
Входные параметры
Параметр Принимаемые значения Обязательность Описание
APIKEY APIKEY Да Ваш API-ключ, обеспечивает доступ к оплаченным услугам. API-ключ следует хранить в тайне
CURRENCY 643, 840 Да Значение валюты которое будет установлено по умолчанию для запросов по API.
Выходные данные
Ответ сервера Описание
JSON ответ Ответ в формате {"status":"success"}
Возможные ошибки
Ответ сервера Описание
BAD_CURRENCY Неверное значение currency
CURRENCY_CHANGE_UNAVAILABLE Изменение валюты недоступно