Граф Wiki

API

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

Содержание страницы
Базовый 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

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

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

Логин через 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.

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

# 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": "267304", "scid": "2487841", "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": "Пополнение баланса. Договор: 0114",
  "metadata": {
    "abonent_id": 2712,
    "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": "2712",
      "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=267304
&scid=2487841
&sum=522.50
&customerNumber=2712
&orderNumber=2712-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=267304
&invoiceId=2190485900123
&customerNumber=2712
&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="267304"/>

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="267304"/>
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=2712-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=2712-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 (напр. 267304)
LK_YOOKASSA_SECRET_KEYREST-ключ (live_* / test_*) или shopPassword для HTTP-протокола
LK_YOOKASSA_SCIDscid для HTTP-протокола (напр. 2487841)
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/ — кнопки «Проверить подключение» для обеих систем.

Запись в 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 ниже, есть 3 публичных для конфига приложения при старте: GET /mobile-api/v1/branding, GET /mobile-api/v1/features, GET /mobile-api/v1/version/check. Кэшируются на стороне клиента 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": "0828", "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": 2712,
  "name": "Богданова Татьяна Петровна",
  "contract_number": "0114",
  "balance": "28.29",
  "tariff_name": "2025_SmIT30new",
  "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": {...}}

Поддержка (FreeScout)

# Список тикетов
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.