Схемы Что нового

API

СмИТ Биллинг предоставляет несколько типов API для интеграции с внешними системами: платёжными шлюзами, личными кабинетами, 1С, системами мониторинга и другими сервисами.

API — интеграция без границ
Содержание раздела
Базовый URL: https://your-billing-server.example.com
Авторизация: Cookie-сессия (Django) или SOAP-хэш
Формат данных: JSON (REST), XML (SOAP)

Обзор API

ТипПротоколURLАутентификация
REST API v2JSON over HTTP/rest_api/v2/<model>/Cookie (session)
REST API v1JSON/XML/rest_api/Cookie (session)
SOAP APIXML (Spyne)/api/, /api/fiscal/SOAP-хэш
Mobile APIJSON over HTTP/mobile-api/v1/*JWT (Bearer token)
System APIJSON/system_api/IP whitelist + пароль
Web Admin APIHTML/JSON/admin/*Cookie + CSRF

Все интерфейсы — точки входа в единое ядро (Django 4.2 / PostgreSQL 17). Схема связей:

flowchart TB
    subgraph clients["Клиенты"]
        ADMIN["Админ-панель
AdminLTE"] LKC["Личный кабинет"] MOB["Мобильное приложение
Flutter"] EXT["Платёжные системы /
NAS / СОРМ"] end subgraph api["Точки входа API"] REST["REST API v2
/rest_api/v2/"] SOAP["SOAP API
/api/"] MAPI["Mobile API (JWT)
/mobile-api/v1/"] WADM["Web Admin
/admin/"] end subgraph core["Ядро биллинга (Django 4.2)"] VIEWS["Views
ChangeView · AdminView · AuthView"] SVC["Сервисы
payment · ip_allocation ·
support · radius_user_cache"] TASKS["Celery
billing_worker · nas_sync ·
push · sorm_export"] end DB[("PostgreSQL 17
~135 моделей")] REDIS[("Redis
кэш · очереди")] FR["FreeRADIUS
rlm_python3"] ADMIN --> WADM LKC --> REST MOB --> MAPI EXT --> SOAP EXT --> REST WADM --> VIEWS REST --> VIEWS MAPI --> VIEWS SOAP --> SVC VIEWS --> SVC SVC --> DB SVC --> REDIS TASKS --> DB TASKS --> REDIS FR --> REDIS FR --> DB style core fill:#0f172a,stroke:#43b77a,color:#fff style DB fill:#134e4a,stroke:#43b77a,color:#fff style REDIS fill:#7f1d1d,stroke:#dc2626,color:#fff style FR fill:#1e3a5f,stroke:#5BA89D,color:#fff

Как начать интеграцию

Пять шагов от задачи до первого рабочего запроса.

  1. Выберите интерфейс под задачу.
    ЗадачаИнтерфейс
    Приём платежей от агента или терминалаSOAP API или платёжные API
    Свой сайт, CRM или учётная система читает и меняет данныеREST API v2
    Мобильное приложение для клиентовMobile API
    Блокировать и разблокировать клиентов из внешней системыWeb API
    Проверка готовности данных для СОРМСОРМ API
  2. Заведите для интеграции отдельного сотрудника — не используйте учётную запись администратора. Так в журнале действий видно, что сделала внешняя система, а пароль можно сменить, не трогая людей. Персонал и права.
    Важно. REST API v2 пока не ограничивает данные правами и организациями сотрудника: через него видны все записи доступных моделей. Выдавайте такую учётную запись только доверенной системе.
  3. Получите сессию и передавайте её cookie в каждом запросе; запросы — только по HTTPS на адрес вашей установки. Аутентификация.
  4. Обрабатывайте ошибки и лимиты. Коды ответов — в разделе «Коды ошибок»; Mobile API пропускает 30 запросов в минуту. Если в биллинге включён режим «Только чтение», запись вернёт 403.
  5. Проверьте на тестовой копии, а не на рабочей базе: запросы на запись меняют деньги и доступ клиентов сразу.

Аутентификация

Содержание раздела

Логин через Web (получение сессии)

Двухшаговый процесс: получение CSRF-токена, затем POST с логином и паролем.

# Шаг 1: получить CSRF-токен
curl -s -c cookies.txt "https://billing.example.com/admin/" > /dev/null

# Шаг 2: логин
CSRF=$(grep csrftoken cookies.txt | awk '{print $NF}')
curl -s -c cookies.txt -b cookies.txt \
  -X POST "https://billing.example.com/admin/" \
  -d "username=admin&password=YOUR_PASSWORD&csrfmiddlewaretoken=$CSRF" \
  -H "Referer: https://billing.example.com/admin/" \
  -o /dev/null -w "%{http_code}"
# 302 = успешный вход

API-логин (JSON)

Альтернативный способ — получение сессионного хэша через GET-параметры:

curl -s "https://billing.example.com/admin/?api=1&username=admin&password=YOUR_PASSWORD&format=json"
# Ответ: {"hash": "session_hash_hex"}
# 401 = неверные учётные данные

Логаут

# Web-логаут
curl -s -b cookies.txt "https://billing.example.com/admin/logout"

# API-логаут
curl -s -b cookies.txt -X POST "https://billing.example.com/rest_api/logout"
# Ответ: {"status": "ok"}

REST API v2

Содержание раздела

Универсальный CRUD-интерфейс для работы с любой моделью биллинга. Поддерживает фильтрацию, сортировку, пагинацию и выбор полей.

Список записей

# Получить список клиентов (первые 100)
curl -s -b cookies.txt \
  "https://billing.example.com/rest_api/v2/Abonents/?limit=100&offset=0"

Ответ:

{
  "count": 5554,
  "next": "/rest_api/v2/Abonents/?limit=100&offset=100",
  "previous": null,
  "results": [
    {
      "id": 1,
      "name": "Иванов Иван Иванович",
      "contract_number": "12345",
      "email": "ivanov@example.com",
      "enabled": true,
      ...
    },
    ...
  ]
}

Фильтрация

# Клиенты с тарифом ID=131, только активные
curl -s -b cookies.txt \
  "https://billing.example.com/rest_api/v2/Abonents/?tarif=131&enabled=true"

# Поиск по имени (частичное совпадение)
curl -s -b cookies.txt \
  "https://billing.example.com/rest_api/v2/Abonents/?name__icontains=иванов"

Сортировка

# Сортировка по имени (А→Я)
curl -s -b cookies.txt \
  "https://billing.example.com/rest_api/v2/Abonents/?ordering=name"

# Обратная сортировка по балансу (от большего к меньшему)
curl -s -b cookies.txt \
  "https://billing.example.com/rest_api/v2/Abonents/?ordering=-account__ostatok"

Выбор полей

# Вернуть только id, name и contract_number
curl -s -b cookies.txt \
  "https://billing.example.com/rest_api/v2/Abonents/?fields=id,name,contract_number"

Получение одной записи

curl -s -b cookies.txt \
  "https://billing.example.com/rest_api/v2/Abonents/5552/"

Создание записи (POST)

curl -s -b cookies.txt -X POST \
  "https://billing.example.com/rest_api/v2/Abonents/" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Новый клиент",
    "contract_number": "99999",
    "email": "new@example.com",
    "tarif": 131
  }'
# Ответ: 201 Created + JSON объекта

Обновление записи (PUT)

curl -s -b cookies.txt -X PUT \
  "https://billing.example.com/rest_api/v2/Abonents/5552/" \
  -H "Content-Type: application/json" \
  -d '{"email": "updated@example.com"}'
# Ответ: 200 OK + обновлённый JSON

Удаление записи (DELETE)

curl -s -b cookies.txt -X DELETE \
  "https://billing.example.com/rest_api/v2/Abonents/5552/"
# Ответ: 204 No Content

Доступные модели

МодельОписание
AbonentsКлиенты
TarifТарифные планы
UslugaУслуги
UsersУчётные записи (логин/пароль/IP)
NasСерверы доступа (NAS/BRAS)
SwitchКоммутаторы
HomesАдреса (дерево: город→улица→дом)
IpPullIP-пулы
FinanceOperationsФинансовые операции
AdminAccountsЛицевые счета
HdskЗаявки HelpDesk
CardsКарты оплаты

Обещанный платёж

Содержание раздела

Специализированный эндпоинт для управления обещанными платежами клиентов.

GET — Статус обещанного платежа

curl -s -b cookies.txt \
  "https://billing.example.com/rest_api/v2/promise_pay/5552/"

Ответ:

{
  "abonent_id": 5552,
  "abonent_name": "Иванов Иван Иванович",
  "promise_pay": "90.00",
  "promise_date_end": "2026-03-15T12:00:00",
  "active_promises": [
    {
      "id": 3,
      "usluga_id": -4,
      "type": "prepay",
      "limit": "90.00",
      "end_time": "2026-03-15T12:00:00",
      "comment": "Обещанный платёж по предоплате до ...",
      "create_date": "2026-03-12T12:00:00"
    }
  ]
}

POST — Добавить обещанный платёж

# С параметрами по умолчанию (сумма и срок из настроек)
curl -s -b cookies.txt -X POST \
  "https://billing.example.com/rest_api/v2/promise_pay/5552/" \
  -H "Content-Type: application/json" \
  -d '{}'

# С явными параметрами
curl -s -b cookies.txt -X POST \
  "https://billing.example.com/rest_api/v2/promise_pay/5552/" \
  -H "Content-Type: application/json" \
  -d '{"limit": 500, "end_time": "2026-03-31", "postpay": false}'
ПараметрТипПо умолчаниюОписание
limitnumber90Сумма обещанного платежа (руб.)
end_timestring+3 дняДата окончания (YYYY-MM-DD или ISO 8601)
postpayboolfalsetrue = постоплата, false = предоплата

Ответ (201 Created):

{
  "status": "ok",
  "promise_id": 3,
  "abonent_id": 5552,
  "usluga_id": -4,
  "type": "prepay",
  "limit": "90",
  "end_time": "2026-03-15T12:00:00"
}

DELETE — Удалить обещанный платёж

# Удалить предоплату (по умолчанию)
curl -s -b cookies.txt -X DELETE \
  "https://billing.example.com/rest_api/v2/promise_pay/5552/"

# Удалить постоплату
curl -s -b cookies.txt -X DELETE \
  "https://billing.example.com/rest_api/v2/promise_pay/5552/?postpay=1"

Ответ (200):

{"status": "ok", "abonent_id": 5552, "message": "Promise pay deleted"}
Настройки по умолчанию хранятся в таблице Usluga:
ID -4 (предоплата): сумма 90 руб., срок 3 дня
ID -3 (постоплата): сумма 90 руб., срок 3 дня

Платёжные API (ЮKassa и Wallet One)

Содержание раздела

Биллинг поддерживает две платёжные системы: ЮKassa (REST API v3 и HTTP-протокол «старой» Yandex.Kassa) и Wallet One (W1). Все платежи проходят через единый сервис lk/services/payment.py.

Единый webhook: POST /lk/payments/webhook/ — автоматически определяет систему по полям запроса (WMI_MERCHANT_ID → W1, action=checkOrder|paymentAviso → ЮKassa HTTP, Content-Type: application/json → ЮKassa REST v3).
Единый инициатор: GET/POST /lk/payments/pay/ — создаёт платёж в активной системе (LK_PAYMENT_SYSTEM = yookassa | w1).
Mobile: POST /mobile-api/v1/finance/pay — возвращает redirect_url (REST v3) или form_post (HTTP-протокол).
Деньги в БД: хранятся как amount × 1010 (поле DB_MONEY_KOEF).

Полный список endpoint

МетодURLНазначениеАутентификация
Инициация платежа
GET/lk/payments/pay/?amount=<N>Страница оплаты (редирект/форма)Cookie (LK-сессия)
POST/lk/payments/pay/Создание платежа (amount, опц. system)Cookie + CSRF
POST/mobile-api/v1/finance/payСоздание платежа (JSON) — возвращает redirect_url или form_postJWT Bearer
Webhook (единая точка для всех ПС)
POST/lk/payments/webhook/ЮKassa REST v3 (JSON) — event: payment.succeededПодпись metadata (shop)
POST/lk/payments/webhook/ЮKassa HTTP — action=checkOrderMD5 (shopPassword)
POST/lk/payments/webhook/ЮKassa HTTP — action=paymentAvisoMD5 (shopPassword)
POST/lk/payments/webhook/Wallet One — WMI_MERCHANT_ID+WMI_SIGNATUREbase64(md5) (W1 secret)
Возврат клиента после оплаты
GET/lk/payments/result/Success/Fail landing для LKCookie (опционально)
GET/lk/payments/История платежей клиентаCookie
Админ-настройки
GET/POST/admin/settings/payment/Форма настроек ЮKassa/W1, кнопка «Проверить подключение»Admin cookie + CSRF
Внешние API (сервер биллинга → платёжная система)
POSThttps://api.yookassa.ru/v3/paymentsСоздание платежа в ЮKassa REST v3Basic(shopId:secretKey) + Idempotence-Key
POSThttps://yoomoney.ru/eshop.xmlФорма ЮKassa HTTP-протокол (браузер)—
POSThttps://wl.walletone.com/checkout/checkout/IndexФорма Wallet One (браузер)WMI_SIGNATURE
Единая точка webhook. Функция process_webhook(request) в lk/services/payment.py определяет источник по полям запроса:

Схема прохождения платежа

  1. Клиент вводит сумму на /lk/payments/pay/ или в мобильном приложении.
  2. Сервис рассчитывает сумму к оплате = amount × (1 + commission_rate). По умолчанию комиссия 0.045 (4.5 %), настраивается в LK_PAYMENT_COMMISSION.
  3. Для ЮKassa REST / W1 — редирект на форму оплаты; для ЮKassa HTTP — форма POST на yoomoney.ru/eshop.xml.
  4. После успешной оплаты платёжная система вызывает /lk/payments/webhook/.
  5. Webhook проверяет подпись, извлекает credit_amount (из metadata / кэша / расчёта), вызывает _credit_abonent(): account.ostatok += credit_amount × 1010 и создаёт запись в FinanceOperations.
sequenceDiagram
    participant Ab as Клиент (ЛК / Mobile)
    participant LK as Биллинг (lk/services/payment.py)
    participant PS as Платёжная система
(ЮKassa / W1) participant WH as /lk/payments/webhook/ participant DB as AdminAccounts + FinanceOperations Ab->>LK: amount = 500 ₽ LK->>LK: + комиссия 4.5% = 522.50 ₽ alt ЮKassa REST v3 / W1 LK-->>Ab: redirect_url → форма оплаты else ЮKassa HTTP (legacy) LK-->>Ab: form_post → автосабмит на yoomoney.ru end Ab->>PS: оплата на форме провайдера PS->>WH: webhook (XML+MD5 или JSON) WH->>WH: проверка подписи + идемпотентность WH->>DB: _credit_abonent()
ostatok += amount × 10¹⁰
+ FinanceOperations WH->>DB: авто-разблок b_negbal если ostatok ≥ 0 WH-->>PS: 200 OK / WMI_RESULT=OK Note over Ab,DB: зачисление идёт по credit_amount,
НЕ по сумме с комиссией

Инициация платежа

# LK — через форму (редирект для REST, или автосабмит form_post)
GET  /lk/payments/pay/?amount=500
POST /lk/payments/pay/         # amount=500&system=yookassa|w1

# Mobile API
curl -X POST "https://billing.example.com/mobile-api/v1/finance/pay" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amount": 500, "return_url": "https://app/return"}'

Ответ Mobile API (ЮKassa REST v3):

{
  "status": "ok",
  "redirect_url": "https://yoomoney.ru/checkout/payments/v2/contract?orderId=..."
}

Ответ Mobile API (HTTP-протокол — авто-сабмит формы на клиенте):

{
  "status": "ok",
  "form_post": {
    "action": "https://yoomoney.ru/eshop.xml",
    "fields": {"shopId": "100500", "scid": "555777", "sum": "522.50", ...}
  }
}

ЮKassa — REST API v3

Используется, если LK_YOOKASSA_SECRET_KEY начинается с live_ / test_ или длиннее 30 символов. Документация: yookassa.ru/developers/api.

Создание платежа (сервер → ЮKassa)

POST https://api.yookassa.ru/v3/payments
Authorization: Basic base64(shopId:secretKey)
Idempotence-Key: <uuid4>
Content-Type: application/json

{
  "amount": { "value": "522.50", "currency": "RUB" },
  "confirmation": { "type": "redirect", "return_url": "https://app/return" },
  "capture": true,
  "description": "Пополнение баланса. Договор: 100042",
  "metadata": {
    "abonent_id": 1042,
    "credit_amount": "500.00",
    "commission": "22.50"
  }
}

Ответ содержит confirmation.confirmation_url, куда нужно направить клиента.

Webhook (ЮKassa → биллинг)

POST /lk/payments/webhook/
Content-Type: application/json

{
  "event": "payment.succeeded",
  "object": {
    "id": "2b3f3a7b-000f-5000-9000-1a8a0b7e1c43",
    "status": "succeeded",
    "amount": { "value": "522.50", "currency": "RUB" },
    "metadata": {
      "abonent_id": "1042",
      "credit_amount": "500.00",
      "commission": "22.50"
    }
  }
}

Биллинг обрабатывает только event = payment.succeeded. Остальные события (payment.canceled, refund.succeeded) принимаются с HTTP 200, но действий не выполняется. Сумма к зачислению берётся из metadata.credit_amount; если её нет — вычисляется как paid_amount / (1 + commission_rate).

ЮKassa — HTTP-протокол (Yandex.Kassa legacy)

Используется, если настроен только shopId + scid + shopPassword (без API-ключа). Подробнее: yoomoney.ru — HTTP-протокол.

Форма оплаты (браузер → ЮKassa)

POST https://yoomoney.ru/eshop.xml
Content-Type: application/x-www-form-urlencoded

shopId=100500
&scid=555777
&sum=522.50
&customerNumber=1042
&orderNumber=1042-a4f90b12
&shopSuccessURL=https://billing.example.com/lk/payments/success/
&shopFailURL=https://billing.example.com/lk/payments/fail/
&cps_email=user@example.com
&cps_phone=+79991234567
&paymentType=

Webhook checkOrder (проверка заказа)

POST /lk/payments/webhook/
Content-Type: application/x-www-form-urlencoded

action=checkOrder
&orderSumAmount=522.50
&orderSumCurrencyPaycash=643
&orderSumBankPaycash=1001
&shopId=100500
&invoiceId=2190485900123
&customerNumber=1042
&md5=<UPPERCASE_MD5>

MD5 считается по строке, склеенной через ;:

md5 = UPPER(md5(
  action + ";" +
  orderSumAmount + ";" +
  orderSumCurrencyPaycash + ";" +
  orderSumBankPaycash + ";" +
  shopId + ";" +
  invoiceId + ";" +
  customerNumber + ";" +
  shopPassword
))

Ответ на checkOrder:

<?xml version="1.0" encoding="UTF-8"?>
<checkOrderResponse performedDatetime="2026-04-12T10:00:00"
   code="0" invoiceId="2190485900123" shopId="100500"/>

Webhook paymentAviso (подтверждение оплаты)

Аналогичный запрос с action=paymentAviso. После проверки MD5 биллинг зачисляет credit_amount = orderSumAmount / (1 + commission_rate) на customerNumber. Ответ:

<?xml version="1.0" encoding="UTF-8"?>
<paymentAvisoResponse performedDatetime="2026-04-12T10:00:00"
   code="0" invoiceId="2190485900123" shopId="100500"/>
codeЗначение
0Успех — продолжить
1MD5 не совпал — остановить обработку
100Зачисление не удалось — ЮKassa повторит запрос

Wallet One (W1)

Платёжная система walletone.com. URL формы: https://wl.walletone.com/checkout/checkout/Index.

Форма оплаты (браузер → W1)

POST https://wl.walletone.com/checkout/checkout/Index
Content-Type: application/x-www-form-urlencoded

WMI_MERCHANT_ID=123456
&WMI_PAYMENT_AMOUNT=522.50
&WMI_CURRENCY_ID=643
&WMI_DESCRIPTION=BASE64:0J/QvtC/0L7Qu9C90LXQvdC40LUg...
&WMI_PAYMENT_NO=1042-a4f90b12
&WMI_SUCCESS_URL=https://billing.example.com/lk/payments/success/
&WMI_FAIL_URL=https://billing.example.com/lk/payments/fail/
&WMI_CUSTOMER_EMAIL=user@example.com
&WMI_SIGNATURE=<base64_md5>

Подпись W1: берутся все поля, начинающиеся с WMI_ (кроме WMI_SIGNATURE), сортируются по ключу, значения конкатенируются (list-значения сортируются и склеиваются), в конец добавляется secret_key. Результат: base64(md5_raw(values + secret_key)).

Webhook (W1 → биллинг)

POST /lk/payments/webhook/
Content-Type: application/x-www-form-urlencoded

WMI_MERCHANT_ID=123456
&WMI_PAYMENT_AMOUNT=522.50
&WMI_PAYMENT_NO=1042-a4f90b12
&WMI_ORDER_STATE=Accepted
&WMI_SIGNATURE=<base64_md5>

Биллинг зачисляет сумму только при WMI_ORDER_STATE = Accepted. Ответы (plain text, без XML):

ОтветСмысл
WMI_RESULT=OKУспешно обработано (W1 не повторит)
WMI_RESULT=RETRY&WMI_DESCRIPTION=...Ошибка — W1 повторит попытку

Сумма зачисления (credit_amount) берётся из кеша (w1_order:{WMI_PAYMENT_NO}, TTL 24 ч), куда была записана при создании платежа. Если кеш пуст — вычисляется как paid_amount / (1 + commission_rate).

Настройки (.env или БД через админ-панель)

КлючОписание
LK_PAYMENT_SYSTEMАктивная система: yookassa или w1
LK_PAYMENT_COMMISSIONКомиссия (по умолчанию 0.045)
LK_YOOKASSA_SHOP_IDshopId (напр. 100500)
LK_YOOKASSA_SECRET_KEYREST-ключ (live_* / test_*) или shopPassword для HTTP-протокола
LK_YOOKASSA_SCIDscid для HTTP-протокола (напр. 555777)
LK_YOOKASSA_SUCCESS_URL / ..._FAIL_URLСтраницы возврата
LK_W1_MERCHANT_IDID магазина в W1
LK_W1_SECRET_KEYСекретный ключ для подписи
LK_W1_CURRENCYISO 4217 числовой код (643 = RUB)
LK_W1_SUCCESS_URL / ..._FAIL_URLСтраницы возврата

UI настроек: /admin/settings/payment/ — кнопки «Проверить подключение» для обеих систем.

Фискализация на стороне ЮKassa (54-ФЗ)

Биллинг передаёт фискальный чек (receipt) в каждый онлайн-платёж — ЮKassa сама пробивает чек и отправляет его покупателю. Работает для обоих протоколов и для всех точек оплаты (ЛК, мобильное приложение, гостевой Wi-Fi):

Чек формируется одной позицией «Пополнение лицевого счёта, договор N» на полную сумму платежа (с комиссией) — суммы чека и платежа всегда совпадают. Контакт покупателя выбирается каскадом: email клиента → телефон (11 цифр, E.164) → LK_YOOKASSA_RECEIPT_FALLBACK_EMAIL. Если контакта нет — платёж уходит без чека, а в лог пишется предупреждение.

КлючОписание
LK_YOOKASSA_RECEIPT_ENABLEDПередавать чек (по умолчанию 1)
LK_YOOKASSA_VAT_CODEСтавка НДС в чеке (1 = без НДС/УСН, 4 = НДС 20%)
LK_YOOKASSA_PAYMENT_SUBJECTПризнак предмета расчёта (по умолчанию service)
LK_YOOKASSA_PAYMENT_MODEПризнак способа расчёта (по умолчанию full_prepayment — пополнение баланса = предоплата услуг связи)
LK_YOOKASSA_RECEIPT_FALLBACK_EMAILEmail для чека, когда у клиента нет контактов (опционально)

Требование на стороне ЮKassa: в личном кабинете магазина должна быть включена опция «Чеки от ЮKassa» (фискализация силами ЮKassa по 54-ФЗ). Мультиорганизационные кассы используют ту же логику — чек уходит из кассы организации клиента.

Запись в FinanceOperations

После успешного зачисления создаётся запись в FinanceOperations:

ПолеЗначение
op_type_id23 — ЮKassa / ЮKassa-HTTP; 24 — W1 (тип «Оплата через платёжные системы»)
op_summacredit_amount × 1010 (положительное число)
abonent_idID клиента из metadata / customerNumber / WMI_PAYMENT_NO
descr«Онлайн-оплата (YooKassa/W1). Оплачено: X.XX, комиссия: Y.YY»
op_datetimezone.now()

Параллельно account.ostatok увеличивается на credit_amount × 1010. Операция выполняется в транзакции (@transaction.atomic).

SOAP API

Содержание раздела

SOAP-интерфейс на базе фреймворка Spyne. Используется для интеграции с платёжными системами, 1С и кассовым ПО.

WSDL

# Получить WSDL-описание сервисов
curl -s "https://billing.example.com/api/?wsdl"
curl -s "https://billing.example.com/api/fiscal/?wsdl"

Эндпоинты

URLСервисМетоды
/api/UserServiceget_user_hash
/api/v2/UserServiceget_user_hash
/api/1c/UserServiceget_user_hash
/api/userside/UserServiceget_user_hash
/api/collector/UserServiceget_user_hash
/api/cabinet/UserServiceget_user_hash
/api/fiscal/FiscalServicepay_usr_act2, pay_usr_act_import

get_user_hash — Аутентификация

curl -s -X POST "https://billing.example.com/api/" \
  -H "Content-Type: text/xml" \
  -d '<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:tns="billing.users">
  <soapenv:Body>
    <tns:get_user_hash>
      <tns:username>admin</tns:username>
      <tns:hash>sha1_hex_hash</tns:hash>
    </tns:get_user_hash>
  </soapenv:Body>
</soapenv:Envelope>'
ПараметрТипОписание
usernameStringИмя пользователя
hashStringSHA1(MD5(password) + salt)

Ответ: {hash: "32-byte-hex-session-token"} — токен для SSO, хранится 1 час.

pay_usr_act2 — Проведение платежа

curl -s -X POST "https://billing.example.com/api/fiscal/" \
  -H "Content-Type: text/xml" \
  -d '<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:tns="rpc.view.fiscal">
  <soapenv:Body>
    <tns:pay_usr_act2>
      <tns:hash_key>session_token</tns:hash_key>
      <tns:ACT>PAY</tns:ACT>
      <tns:CONTRACT_NUMBER>12345</tns:CONTRACT_NUMBER>
      <tns:SUM_IN>100.00</tns:SUM_IN>
      <tns:PAY_OPERATOR>Sberbank</tns:PAY_OPERATOR>
      <tns:COMMENT_IN>Оплата услуг</tns:COMMENT_IN>
    </tns:pay_usr_act2>
  </soapenv:Body>
</soapenv:Envelope>'
ПараметрТипОбязательныйОписание
hash_keyStringДаСессионный токен (из get_user_hash)
ACTStringДаТип операции: PAY, CHECK
CONTRACT_NUMBERStringДаНомер договора клиента
SUM_INStringДаСумма платежа
PAY_OPERATORStringДаНазвание оператора/платёжной системы
COMMENT_INStringНетКомментарий к платежу
PAY_ID_STRStringНетВнешний ID платежа

Web API (управление клиентами)

Содержание раздела

Эндпоинты для управления клиентами через HTTP POST. Требуют авторизацию (cookie) и CSRF-токен. Возвращают HTTP 302 (redirect) при успехе.

Блокировка клиента

CSRF=$(grep csrftoken cookies.txt | awk '{print $NF}')
curl -s -b cookies.txt -X POST \
  "https://billing.example.com/admin/abonents/block/5552/" \
  -d "csrfmiddlewaretoken=$CSRF" \
  -H "Referer: https://billing.example.com/admin/Abonents/5552/" \
  -o /dev/null -w "%{http_code}"
# 302 = успех

Разблокировка клиента

curl -s -b cookies.txt -X POST \
  "https://billing.example.com/admin/abonents/unblock/5552/" \
  -d "csrfmiddlewaretoken=$CSRF" \
  -H "Referer: https://billing.example.com/admin/Abonents/5552/"

Отключение / Восстановление

# Отключить клиента
curl -s -b cookies.txt -X POST \
  "https://billing.example.com/admin/abonents/reconnect/5552/" \
  -d "csrfmiddlewaretoken=$CSRF" \
  -H "Referer: https://billing.example.com/admin/Abonents/5552/"

# Восстановить удалённого клиента
curl -s -b cookies.txt -X POST \
  "https://billing.example.com/admin/Abonents/restore/5552/" \
  -d "csrfmiddlewaretoken=$CSRF" \
  -H "Referer: https://billing.example.com/admin/Abonents/5552/"

СОРМ API

Endpoint'ы для интеграции внешних систем (мониторинг, скрипты pre-deploy) с механизмом проверки готовности СОРМ-данных. Полное описание раздела — на странице Интеграция с СОРМ3 → СОРМ-метаданные.

Содержание раздела

GET /admin/equipment/sorm_list/validate/ — pre-flight проверка

Проверяет полноту СОРМ-данных перед запуском выгрузки. Возвращает JSON с двумя секциями:

Аутентификация: Django session cookie (@login_required). Любой залогиненный пользователь.

Запрос

curl -s -b cookies.txt \
  "https://billing.example.com/admin/equipment/sorm_list/validate/" \
  -H "Accept: application/json"

Ответ (200 OK)

{
  "ok": true,
  "has_problems": true,
  "meta_issues": [
    {
      "kind": "missing_field_code",
      "attribute_id": 13,
      "attribute_name": "Паспорт №",
      "message": "СОРМ-атрибут без sorm_field_code"
    },
    {
      "kind": "duplicate_field_code",
      "report_type": "ABONENT_LEGAL",
      "sorm_field_code": "director",
      "attribute_ids": [10, -219000],
      "attribute_names": ["Директор", "Директор"],
      "message": "В отчёте ABONENT_LEGAL sorm_field_code «director» используется у 2 реквизитов: Директор, Директор"
    }
  ],
  "data_issues": [
    {
      "report_type": "ABONENT_LEGAL",
      "field_code": "inn",
      "attribute_name": "ИНН",
      "attribute_id": 4,
      "missing": 241
    }
  ]
}

Поля meta_issues

kindОписание
missing_field_codeАтрибут с is_sorm=True, но sorm_field_code пустой. Builder его пропустит — данные потеряются в выгрузке
invalid_field_codefield_code не соответствует регэкспу ^[a-z][a-z0-9_]*$ (пробелы, кириллица, тире и т.д.)
duplicate_field_codeДва разных атрибута имеют один и тот же field_code в одном отчёте. Builder выберет один через LIMIT 1 непредсказуемо
exceptionВнутренняя ошибка builder при попытке валидации. Поле message содержит текст исключения

Поля data_issues

ПолеТипОписание
report_typestringТип СОРМ-отчёта где обнаружены пропуски: ABONENT_LEGAL, ABONENT, ABONENT_IDENT и т.д.
field_codestringТехническое имя поля (sorm_field_code)
attribute_namestringИмя реквизита из UserAttributes.NAME
attribute_idintPK атрибута
missingintСколько активных клиентов без заполненного значения этого поля
Юр-поля (ABONENT_LEGAL) проверяются только у юр.лиц (company=True); физ-поля (passport_series_number, birth_date) — только у физических лиц. Это исключает ложные срабатывания.

Использование в мониторинге

Endpoint удобен для интеграции с Zabbix / Grafana / собственными скриптами cron. Пример Bash-обёртки которая вернёт ненулевой exit code если есть проблемы:

#!/bin/bash
# sorm_check.sh — алертит если есть проблемы метаданных или критические пропуски
RESPONSE=$(curl -s -b /etc/sorm/cookies.txt \
  "https://billing.example.com/admin/equipment/sorm_list/validate/")
META=$(echo "$RESPONSE" | jq '.meta_issues | length')
DATA=$(echo "$RESPONSE" | jq '.data_issues | length')
if [ "$META" -gt 0 ]; then
  echo "CRITICAL: $META проблем метаданных"
  exit 2
fi
if [ "$DATA" -gt 0 ]; then
  echo "WARNING: $DATA полей с пропусками"
  exit 1
fi
echo "OK: все СОРМ-данные готовы"
exit 0

CRUD UserAttributes с СОРМ-метаданными

Endpoint /admin/dictionary/user_attributes_crud/[id]/ поддерживает GET/POST/DELETE и принимает 4 поля СОРМ-метаданных. Используется UI /admin/dictionary/UserAttributes/.

GET — получение метаданных одного атрибута

curl -s -b cookies.txt \
  "https://billing.example.com/admin/dictionary/user_attributes_crud/4/"

# Ответ:
{
  "ok": true,
  "id": 4,
  "name": "ИНН",
  "type_id": 1,
  "is_sorm": true,
  "sorm_field_code": "inn",
  "sorm_report_types": "ABONENT,ABONENT_LEGAL",
  "sorm_required": true,
  "sorm_alt_names": "",
  "use_count": 280,
  ...
}

POST — создать или обновить атрибут

curl -s -b cookies.txt -X POST \
  "https://billing.example.com/admin/dictionary/user_attributes_crud/4/" \
  -H "Content-Type: application/json" \
  -H "X-CSRFToken: $CSRF" \
  -d '{
    "name": "ИНН",
    "type_id": 1,
    "is_sorm": true,
    "sorm_field_code": "inn",
    "sorm_report_types": "ABONENT,ABONENT_LEGAL",
    "sorm_required": true,
    "sorm_alt_names": ""
  }'

Защита от случайного переименования

Если попытаться переименовать СОРМ-атрибут (изменить name), сервер возвращает HTTP 400 с флагом sorm_rename_blocked: true:

{
  "ok": false,
  "error": "«ИНН» — реквизит из СОРМ-отчётности. Переименование сломает SQL-запросы в billing/views/sorm.py (`WHERE NAME=...`). Подтвердите force_sorm_rename=true если уверены.",
  "sorm_rename_blocked": true
}

Чтобы всё-таки переименовать — повторить запрос с дополнительным полем "force_sorm_rename": true в JSON.

Валидация sorm_field_code

SormReportBuilder — Python API

Класс billing.services.sorm_sql.SormReportBuilder — программный API для генерации SQL и валидации метаданных. Используется внутри billing.views.sorm и Celery-задачи run_report_export, но доступен для скриптов и shell-сессий.

from billing.services.sorm_sql import (
    SormReportBuilder,
    validate_sorm_data,
)

# Сгенерировать SQL для отчёта ABONENT_LEGAL
sql = SormReportBuilder('ABONENT_LEGAL').build_sql()

# Получить порядок колонок CSV
codes = SormReportBuilder('ABONENT_LEGAL').get_field_codes()
# ['inn', 'kpp', 'ogrn', 'legal_address', ...]

# Список поддерживаемых типов отчётов
SormReportBuilder.supported_report_types()
# ['ABONENT_LEGAL']  # на только этот тип имеет builder-шаблон

# Валидация метаданных (без обращения к БД клиентов)
issues = SormReportBuilder.validate_meta()
# [{'kind': 'missing_field_code', 'attribute_id': 13, ...}, ...]

# Валидация заполнения данных у активных клиентов
data_issues = validate_sorm_data(['ABONENT_LEGAL'])
# [{'report_type': 'ABONENT_LEGAL', 'field_code': 'inn', 'missing': 241}, ...]

Исключения: SormReportBuilderError поднимается если тип отчёта не поддержан, либо нет ни одного СОРМ-атрибута с sorm_field_code для этого типа, либо у атрибута пустой field_code. В коде billing/views/sorm.py используется graceful fallback на захардкоженный legacy SQL — если builder упадёт, выгрузка продолжит работать.

AJAX-эндпоинты

Содержание раздела

JSON-эндпоинты для асинхронных запросов из веб-интерфейса. Требуют авторизацию (cookie).

Получить IP из пула

# Из любого свободного пула
curl -s -b cookies.txt "https://billing.example.com/admin/ajax/ippull_get/"

# Из конкретного пула (ID=1)
curl -s -b cookies.txt "https://billing.example.com/admin/ajax/ippull_get/1/"

Ответ: {"msg": "ok", "ip": "10.0.0.5", "mask": "255.255.255.0"}

Информация о MAC-адресе

curl -s -b cookies.txt \
  "https://billing.example.com/admin/ajax/user_getinfo/?user_id=123&cmd=GetMac"

Ответ: {"msg": "ok", "mac": "AA:BB:CC:DD:EE:FF"}

Порты коммутатора

curl -s -b cookies.txt \
  "https://billing.example.com/admin/ajax/user_get_switch_port_list/?user_id=123&switch_id=1"

Ответ: [{"pk": 1, "name": "Port 1", "selected": false}, ...]

Утилиты

МетодURLОписание
GET/admin/Abonents/get_bills/<filename>Скачать файл счёта
GET/admin/Abonents/download_operations/Скачать отчёт по операциям
GET/admin/getpdf/<abonent_id>/<item_id>/Генерация PDF-документа
GET/admin/Users/resolve_dns/<ip>/Обратный DNS-запрос

Mobile API

Содержание раздела

REST API для мобильного приложения клиента. Аутентификация через JWT-токены (без cookies/CSRF). Все суммы возвращаются в рублях (деление на 1010 выполняется на стороне сервера).

Базовый путь: /mobile-api/v1/
Авторизация: JWT Bearer token (access = 15 мин, refresh = 30 дней)
Rate limit: 30 запросов/мин на пользователя
Стек: Django REST Framework + djangorestframework-simplejwt
Public Mobile API — без авторизации
Помимо защищённых endpoints ниже, есть 4 публичных для конфига приложения при старте: GET /mobile-api/v1/branding, GET /mobile-api/v1/features, GET /mobile-api/v1/version/check, GET /mobile-api/v1/auth/methods (доступные способы входа). Кэшируются на стороне клиента 5 мин (Cache-Control). Подробное описание и сценарии использования — в разделе ЛК и мобильные → Public Mobile API.

Аутентификация

# Логин — получить JWT-токены
curl -s -X POST "https://billing.example.com/mobile-api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"contract": "100042", "password": "mypassword"}'

Ответ:

{
  "access": "eyJhbGciOiJIUzI1NiIs...",
  "refresh": "eyJhbGciOiJIUzI1NiIs..."
}

Поле contract принимает номер договора (с ведущим нулём и без) или логин из таблицы Users.

# Обновление access-токена
curl -s -X POST "https://billing.example.com/mobile-api/v1/auth/refresh" \
  -H "Content-Type: application/json" \
  -d '{"refresh": "eyJhbGciOiJIUzI1NiIs..."}'

Аккаунт

# Статус клиента (баланс, тариф, блокировки)
curl -s "https://billing.example.com/mobile-api/v1/account/status" \
  -H "Authorization: Bearer ACCESS_TOKEN"

Ответ:

{
  "abonent_id": 1042,
  "name": "Иванов Иван Иванович",
  "contract_number": "100042",
  "balance": "28.29",
  "tariff_name": "Домашний 300",
  "tariff_id": 139,
  "speed_mbit": null,
  "monthly_cost": "0.00",
  "is_blocked": false,
  "block_reason": "",
  "has_promise_pay": false,
  "promise_pay_end": null,
  "balance_until_date": null,
  "address": "Волгоград",
  "email": "user@example.com",
  "sms": "+79001234567",
  "notification": "Уважаемые клиенты! Плановые работы 20.03.",
  "last_payment": {"amount": "500.00", "date": "19.03.2026"}
}
# Список доступных тарифов
curl -s "https://billing.example.com/mobile-api/v1/account/tariffs" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# Смена тарифа
curl -s -X POST "https://billing.example.com/mobile-api/v1/account/tariff" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tariff_id": 140}'

# Смена пароля
curl -s -X POST "https://billing.example.com/mobile-api/v1/account/change_password" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"current_password": "old", "new_password": "new123", "confirm_password": "new123"}'

# Добровольная блокировка — статус
curl -s "https://billing.example.com/mobile-api/v1/account/voluntary_block" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# Добровольная блокировка — включить/выключить
curl -s -X POST "https://billing.example.com/mobile-api/v1/account/voluntary_block" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enable": true}'

Финансы

# История платежей (пагинация + фильтры)
curl -s "https://billing.example.com/mobile-api/v1/finance/history?period=month&page=1&per_page=25" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# История начислений (только отрицательные суммы)
curl -s "https://billing.example.com/mobile-api/v1/finance/charges" \
  -H "Authorization: Bearer ACCESS_TOKEN"

Параметры фильтрации /finance/history:

ПараметрОписание
periodmonth, 3month, year
from, toДаты в формате YYYY-MM-DD
pageНомер страницы (по умолчанию 1)
per_pageЗаписей на странице (макс. 100, по умолчанию 25)
# Обещанный платёж — статус
curl -s "https://billing.example.com/mobile-api/v1/finance/promise_pay" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# Обещанный платёж — активировать
curl -s -X POST "https://billing.example.com/mobile-api/v1/finance/promise_pay" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# Обещанный платёж — отменить
curl -s -X DELETE "https://billing.example.com/mobile-api/v1/finance/promise_pay" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# Создать платёж (YooKassa)
curl -s -X POST "https://billing.example.com/mobile-api/v1/finance/pay" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amount": "500.00", "system": "yookassa"}'
# Ответ (REST API v3): {"redirect_url": "https://yookassa.ru/checkout/..."}
# Ответ (HTTP-протокол): {"form_post": true, "action": "https://yoomoney.ru/eshop.xml", "fields": {...}}

Поддержка

# Список тикетов
curl -s "https://billing.example.com/mobile-api/v1/support/tickets" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# Создать тикет (POST на тот же URL)
curl -s -X POST "https://billing.example.com/mobile-api/v1/support/tickets" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Нет интернета", "body": "Не работает с утра"}'

# Детали тикета + сообщения
curl -s "https://billing.example.com/mobile-api/v1/support/tickets/59808" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# Ответ на тикет
curl -s -X POST "https://billing.example.com/mobile-api/v1/support/tickets/59808" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body": "Спасибо, проблема решена"}'

Услуги

# Список активных услуг
curl -s "https://billing.example.com/mobile-api/v1/services/list" \
  -H "Authorization: Bearer ACCESS_TOKEN"

# Вкл/выкл услугу
curl -s -X POST "https://billing.example.com/mobile-api/v1/services/toggle" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"service_id": 123, "enable": false}'

Push-уведомления

# Зарегистрировать FCM-токен
curl -s -X POST "https://billing.example.com/mobile-api/v1/push/register" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"token": "fcm_token_here", "platform": "android"}'

Сводная таблица эндпоинтов

МетодПутьОписание
POST/auth/loginЛогин → JWT-токены
POST/auth/refreshОбновление access-токена
GET/account/statusБаланс, тариф, блокировки
GET/account/tariffsДоступные тарифы
POST/account/tariffСмена тарифа
POST/account/change_passwordСмена пароля
GET/POST/account/voluntary_blockДобровольная блокировка
GET/finance/historyИстория платежей
GET/finance/chargesИстория начислений
GET/POST/DELETE/finance/promise_payОбещанный платёж
POST/finance/payСоздание платежа
GET/POST/support/ticketsСписок + создание тикетов
GET/POST/support/tickets/<id>Детали тикета + ответ
GET/services/listСписок услуг
POST/services/toggleВкл/выкл услуги
POST/push/registerРегистрация FCM-токена

Коды ошибок

HTTP-кодОписание
200Успех
201Создано (POST)
204Удалено (DELETE)
302Редирект (Web API — успех)
400Неверный запрос (ошибка валидации)
401Не авторизован
403Доступ запрещён (CSRF, READ_ONLY_MODE)
404Объект не найден
429Превышен лимит запросов (Mobile API: 30/мин)
500Внутренняя ошибка сервера
503Сервис временно недоступен (платёжная система)
READ_ONLY_MODE: Если в настройках биллинга включён режим «Только чтение» (VpnConst.READ_ONLY_MODE), все POST/PUT/DELETE запросы вернут 403 Forbidden.