Paykeeper Adapter — API
Три контура эндпоинтов
Public webhooks (для PayKeeper) — валидация MD5/HMAC подписи. Admin (для Admin BFF) — Bearer JWT. Internal (для других наших сервисов) —
X-Service-Token.
Public webhooks — входящие от PayKeeper
Эти endpoint’ы публикуются в ЛК PK при онбординге. Валидация подписи — см. раздел Auth ниже.
POST /pk-webhooks/informer/{account_id}
Webhook успешной оплаты (legacy PK informer).
| Параметр | Значение |
|---|---|
| Auth | MD5-подпись в поле key тела; см. PK answers и research §6.4 |
| Content-Type | application/x-www-form-urlencoded |
Path Parameters
| Параметр | Тип | Описание |
|---|---|---|
account_id | uuid | ID PK-аккаунта (для маршрутизации и подбора informer_seed) |
Request Body (form-data)
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
id | string | Да | PK payment ID |
sum | number | Да | Сумма |
clientid | string | Да | Значение, переданное при создании инвойса |
orderid | string | Да | Packed bridge-ID base64url(store_id:order_number) |
key | string(32) | Да | MD5 подпись, ^[a-f0-9]{32}$ |
service_name | string | Нет | JSON с cart + callback |
client_email | string | Нет | |
client_phone | string | Нет | |
ps_id | integer | Нет | ID платёжной системы |
batch_date | string | Нет | Флаг 2-stage hold |
fop_receipt_key | string | Нет | Ключ чека 54-ФЗ |
bank_id | string | Нет | ID привязки карты |
bank_payer_id | string | Нет | |
card_number | string | Нет | Маскированный номер |
card_holder | string | Нет | |
card_expiry | string | Нет | |
bank_operation_datetime | string | Нет |
Response 200
Plain text, обязательный формат: OK <md5(id + informer_seed)>.
Пример: OK a1b2c3d4e5f67890a1b2c3d4e5f67890.
Errors
Plain text (PK ретраит при любом non-200 или неверном формате).
| HTTP | Body | Причина |
|---|---|---|
| 400 | Missing required fields | Не переданы id / sum / clientid / orderid / key |
| 400 | Hash mismatch | MD5-подпись не совпала |
| 400 | Account not found | account_id в URL не существует |
| 404 | Order not found | Не нашли наш заказ по orderid |
| 422 | Sums mismatch | Сумма в webhook не равна orders.total |
POST /pk-webhooks/refund/{account_id}
Webhook завершённого возврата. Формат полностью аналогичен informer — та же MD5-подпись, тот же формат ответа.
Валидируется тем же алгоритмом. Отличие — ищется существующий paykeeper_payment по pk_payment_id, создаётся paykeeper_refund status=done, публикуется paykeeper.payment.refunded.
POST /pk-webhooks/receipt/{account_id}
Callback финального статуса чека 54-ФЗ (§8.13 в PK документации). Другая схема подписи — HMAC-SHA256.
| Параметр | Значение |
|---|---|
| Auth | HMAC-SHA256 подпись в поле sign; см. research §6.6 |
| Content-Type | application/x-www-form-urlencoded |
Request Body
Все поля из ответа /info/receipts/byid/ (см. PK JSON API §8.4) + поле sign.
Response 200
Plain text OK (без суффикса — здесь требования PK менее строгие, при ошибке просто не ретраим).
Admin — для Admin BFF
Префикс: /internal/paykeeper/* в Adapter. Admin BFF проксирует как /api/v1/admin/paykeeper/*.
GET /internal/paykeeper/accounts
Список PK-аккаунтов в scope пользователя.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.read) |
Query Parameters
| Параметр | Тип | Описание |
|---|---|---|
legal_entity_id | uuid | Фильтр по ЮЛ (опц.) |
status | string | active / suspended / all (default all) |
page | int | default 1 |
per_page | int | default 20, max 100 |
Response 200
{
"data": [
{
"id": "uuid",
"legal_entity_id": "uuid",
"legal_entity_name": "ООО Ромашка",
"pk_server_host": "example.server.paykeeper.ru",
"pk_login": "admin",
"paykeeper_id": "140221-031-1",
"status": "active",
"terminals_count": 3,
"onboarded_at": "2026-04-23T10:00:00Z"
}
],
"meta": { "page": 1, "per_page": 20, "total": 5 }
}Пароль и seed не возвращаются.
GET /internal/paykeeper/accounts/{id}
Детали PK-аккаунта.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.read) |
Response 200
{
"data": {
"id": "uuid",
"legal_entity_id": "uuid",
"pk_server_host": "example.server.paykeeper.ru",
"pk_login": "admin",
"paykeeper_id": "140221-031-1",
"status": "active",
"webhook_urls": {
"informer": "https://erp-test.nirbi.ru/pk-webhooks/informer/{id}",
"refund": "https://erp-test.nirbi.ru/pk-webhooks/refund/{id}",
"receipt": "https://erp-test.nirbi.ru/pk-webhooks/receipt/{id}"
},
"onboarded_at": "2026-04-23T10:00:00Z",
"last_token_at": "2026-04-23T14:30:00Z"
}
}POST /internal/paykeeper/accounts
Создать PK-аккаунт.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.manage) |
| Content-Type | application/json |
Request Body
{
"legal_entity_id": "uuid",
"pk_server_host": "example.server.paykeeper.ru",
"pk_login": "admin",
"pk_password": "secret",
"informer_seed": "my-secret-seed",
"paykeeper_id": "140221-031-1"
}Создание сопровождается автоматическим тестом: GET /info/settings/token/ через переданные креды. При ошибке — возврат 422 без сохранения.
Response 201
Идентично GET /internal/paykeeper/accounts/{id}.
Errors
| Code | HTTP | Описание |
|---|---|---|
VALIDATION_ERROR | 400 | Невалидные поля |
ACCOUNT_EXISTS | 409 | Для этого legal_entity_id уже есть аккаунт |
PK_CONNECTION_FAILED | 422 | Проверка соединения не прошла |
FORBIDDEN | 403 | Нет integrations.manage |
PATCH /internal/paykeeper/accounts/{id}
Обновить аккаунт (любое поле, включая ротацию пароля/seed).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.manage) |
Request Body
Partial JSON. При смене pk_login/pk_password/pk_server_host — повторная проверка соединения.
Response 200
Идентично GET.
POST /internal/paykeeper/accounts/{id}/suspend
Приостановить интеграцию (status=suspended).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.manage) |
Response 200
{ "data": { "id": "uuid", "status": "suspended" } }POST /internal/paykeeper/accounts/{id}/resume
Возобновить интеграцию.
DELETE /internal/paykeeper/accounts/{id}
Soft delete. Запрещено если есть открытые инвойсы (pk_status IN (created,sent)) или незакрытые возвраты (status=started).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.manage) |
Response 204
Errors
| Code | HTTP | Описание |
|---|---|---|
ACCOUNT_HAS_OPEN_OPERATIONS | 422 | Есть открытые инвойсы или возвраты |
POST /internal/paykeeper/accounts/{id}/test-connection
Повторная проверка соединения с PK (запрос GET /info/settings/token/).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.manage) |
Response 200
{ "data": { "ok": true, "checked_at": "2026-04-23T14:30:00Z" } }Response 422
{ "error": { "code": "PK_CONNECTION_FAILED", "message": "Неверные учётные данные" } }GET /internal/paykeeper/terminals
Список привязок терминалов.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.read) |
Query Parameters
| Параметр | Тип | Описание |
|---|---|---|
account_id | uuid | Фильтр по аккаунту (опц.) |
store_id | uuid | Фильтр по ТТ (опц.) |
Response 200
{
"data": [
{
"id": "uuid",
"account_id": "uuid",
"store_id": "uuid",
"store_name": "Пекарня №5",
"pk_terminal_id": "TERM-001",
"pk_mpos_merchant_id": "MERCH-123",
"label": "Касса 1",
"status": "active"
}
]
}POST /internal/paykeeper/terminals
Создать привязку ТТ ↔ терминал.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.manage) |
Request Body
{
"account_id": "uuid",
"store_id": "uuid",
"pk_terminal_id": "TERM-001",
"pk_mpos_merchant_id": "MERCH-123",
"label": "Касса 1"
}Response 201
Идентично элементу GET.
Errors
| Code | HTTP | Описание |
|---|---|---|
VALIDATION_ERROR | 400 | — |
TERMINAL_STORE_EXISTS | 409 | Для этой ТТ уже есть привязка |
TERMINAL_PK_ID_EXISTS | 409 | pk_terminal_id уже используется другой ТТ |
STORE_NOT_IN_SCOPE | 403 | ТТ не принадлежит ЮЛ аккаунта |
PATCH /internal/paykeeper/terminals/{id}
Обновить привязку.
DELETE /internal/paykeeper/terminals/{id}
Удалить привязку. Запрещено если есть открытые инвойсы для этой ТТ.
POST /internal/paykeeper/accounts/{id}/resync-catalog
Запуск manual full re-sync каталога для аккаунта (BR 3.4). Создаёт запись в pk_catalog_sync_runs со status=running и запускает процесс: получает GET /internal/catalog/full-snapshot из Catalog Service, сравнивает hash’и, кладёт дельты в pk_outbox.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.manage) |
Response 202 Accepted
{
"data": {
"sync_run_id": "uuid",
"status": "running",
"trigger": "manual",
"started_at": "2026-04-24T10:00:00Z"
}
}Errors
| Code | HTTP | Описание |
|---|---|---|
NOT_FOUND | 404 | Аккаунт не найден |
ACCOUNT_NOT_ACTIVE | 422 | status != active |
SYNC_ALREADY_RUNNING | 409 | Для этого account уже есть pk_catalog_sync_runs.status=running |
FORBIDDEN | 403 | Нет integrations.manage |
GET /internal/paykeeper/accounts/{id}/catalog-sync-status
Агрегированный статус синхронизации каталога для аккаунта (BR 3.4). Используется в UI админки для блока «Каталог в PayKeeper».
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.read) |
Response 200
{
"data": {
"account_id": "uuid",
"last_run": {
"id": "uuid",
"trigger": "manual | cron | webhook_missed",
"started_at": "2026-04-24T03:00:00Z",
"finished_at": "2026-04-24T03:00:08Z",
"status": "success | partial | failed | running",
"products_upserted": 2,
"products_deleted": 0,
"errors_count": 0
},
"totals": {
"pk_products_synced": 142,
"erp_products_total": 47,
"diverged_count": 0
}
}
}Вычисление totals:
pk_products_synced— COUNT(paykeeper_products WHERE account_id=X AND status='active'). Это общее количество записей в ЛК PK — включая развёрнутые из модификаторов варианты и addon’ы.erp_products_total— количество неудалённых товаров в ERP для этой франшизы (запрос к Catalog Service, кэш 60 сек). Справочный показатель «сколько исходных товаров породило текущий набор PK-продуктов».diverged_count— товары, у которых текущийhashотличается от актуального по ERP (stale records). Индикатор что нужен re-sync.
Если прогонов ещё не было — last_run: null.
Errors
| Code | HTTP | Описание |
|---|---|---|
NOT_FOUND | 404 | Аккаунт не найден |
GET /internal/paykeeper/accounts/{id}/catalog-sync-runs
История прогонов синхронизации каталога (BR 3.4).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.read) |
Query Parameters
| Параметр | Тип | Default | Описание |
|---|---|---|---|
limit | int | 20 | Max 100 |
since | datetime | — | Вернуть прогоны с started_at >= since |
Response 200
{
"data": [
{
"id": "uuid",
"trigger": "cron",
"started_at": "2026-04-24T03:00:00Z",
"finished_at": "2026-04-24T03:00:08Z",
"status": "success",
"products_upserted": 2,
"products_deleted": 0,
"errors_count": 0
}
],
"meta": {
"limit": 20,
"total": 156
}
}GET /internal/paykeeper/accounts/{id}/catalog-sync-runs/{run_id}
Детали прогона, включая полный errors_json — для модалки «Журнал прогонов → Ошибки» (BR 3.4).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.read) |
Response 200
{
"data": {
"id": "uuid",
"account_id": "uuid",
"trigger": "manual",
"started_at": "2026-04-24T10:00:00Z",
"finished_at": "2026-04-24T10:00:05Z",
"status": "partial",
"products_upserted": 45,
"products_deleted": 0,
"errors_count": 2,
"last_error": "PK ims-api returned 429 Too Many Requests",
"errors_json": [
{
"erp_product_id": "uuid",
"variant_kind": "structural_variant",
"variant_sku": "abc-123:size-30",
"message": "Rate limited by PK, retrying via outbox"
}
]
}
}Errors
| Code | HTTP | Описание |
|---|---|---|
NOT_FOUND | 404 | Run не найден для этого account’а |
POST /internal/paykeeper/accounts/{id}/employees/preview
Получить список пользователей ЛК PK с матчингом на наших сотрудников (BR 3.5). Используется первым шагом wizard’а импорта.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.manage AND employees.edit) |
| Content-Type | application/json |
Path Parameters
| Параметр | Тип | Описание |
|---|---|---|
id | uuid | ID PK-аккаунта |
Request Body
Пустое тело (либо опционально {}).
Action
- Проверить что аккаунт
active(иначеACCOUNT_NOT_ACTIVE) - Запрос
GET /info/organization/users/в{pk_server_host}с Basic auth (pk_login/pk_passwordизpaykeeper_accounts) - Для каждого pk_user в response:
- lookup в
paykeeper_usersпо(account_id, pk_user_id)→ если есть →match_status="already_linked" - иначе если
pk_user.emailне пустой —GET /internal/users/by-email(User Service) → если найден →match_status="matched_email" - иначе →
match_status="new"
- lookup в
Response 200
{
"data": [
{
"pk_user_id": "1",
"pk_login": "admin",
"pk_email": "spad20@yandex.ru",
"pk_fio": "",
"pk_admin": true,
"pk_invoices_only": false,
"match_status": "new",
"matched_employee": null,
"linked_employee_id": null
},
{
"pk_user_id": "6",
"pk_login": "et",
"pk_email": "et@paykeeper.ru",
"pk_fio": "ETA",
"pk_admin": true,
"pk_invoices_only": false,
"match_status": "matched_email",
"matched_employee": {
"id": "uuid",
"email": "et@paykeeper.ru",
"first_name": "Иван",
"last_name": "Петров"
},
"linked_employee_id": null
}
]
}| Поле | Тип | Описание |
|---|---|---|
pk_user_id | string | id из PK API |
pk_login | string | login пользователя в ЛК PK |
pk_email | string | null | email из PK API (может быть null или пустой) |
pk_fio | string | null | ФИО единой строкой из PK |
pk_admin | boolean | Флаг админа PK |
pk_invoices_only | boolean | Флаг ЛК «только счета» |
match_status | string | new / matched_email / already_linked |
matched_employee | object | null | Существующий employee при matched_email |
linked_employee_id | uuid | null | ID связанного employee при already_linked |
Errors
| Code | HTTP | Описание |
|---|---|---|
ACCOUNT_NOT_FOUND | 404 | PK-аккаунт не найден |
ACCOUNT_NOT_ACTIVE | 422 | Аккаунт suspended |
PK_CONNECTION_FAILED | 422 | PK API недоступен или вернул ошибку |
FORBIDDEN | 403 | Нет требуемых permissions |
POST /internal/paykeeper/accounts/{id}/employees/import
Пакетный импорт сотрудников из ЛК PK по решениям, принятым владельцем в wizard’е (BR 3.5).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.manage AND employees.edit) |
| Content-Type | application/json |
Path Parameters
| Параметр | Тип | Описание |
|---|---|---|
id | uuid | ID PK-аккаунта |
Request Body
{
"decisions": [
{
"pk_user_id": "1",
"pk_login": "admin",
"action": "create_new",
"matched_employee_id": null,
"employee_data": {
"first_name": "Иван",
"last_name": "Петров",
"email": "ivan@example.com",
"password": null,
"generate_password": true,
"phone": "+79991112233",
"pin": "1234",
"is_courier": false,
"roles": [
{ "role_id": "uuid", "store_ids": ["uuid"] }
]
}
},
{
"pk_user_id": "6",
"pk_login": "et",
"action": "link_existing",
"matched_employee_id": "uuid",
"employee_data": null
},
{
"pk_user_id": "5",
"pk_login": "koala",
"action": "skip",
"matched_employee_id": null,
"employee_data": null
}
]
}| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
decisions[] | array | Да | Не пустой |
decisions[].pk_user_id | string | Да | id из preview |
decisions[].pk_login | string | Да | login из preview (для журнала) |
decisions[].action | string | Да | create_new / create_with_alt_email / link_existing / update_existing / skip |
decisions[].matched_employee_id | uuid | Условно | Обязателен для link_existing / update_existing |
decisions[].employee_data | object | Условно | Обязателен для create_new / create_with_alt_email / update_existing |
employee_data.first_name | string | Да | |
employee_data.last_name | string | Да | |
employee_data.email | string | Да | Уникален в рамках franchise_id |
employee_data.password | string | null | Условно | Если generate_password=false — обязательно |
employee_data.generate_password | boolean | Да | true — adapter генерирует и шлёт reset-link на email |
employee_data.phone | string | null | Нет | |
employee_data.pin | string | null | Нет | 4 цифры |
employee_data.is_courier | boolean | Нет | default false |
employee_data.roles[] | array | Нет | [{ role_id, store_ids[] }] |
Action (синхронно, timeout 60s)
- Создать
paykeeper_user_importsсstatus=running,initiated_by_user_idиз JWT,users_total = decisions.length - Per-decision:
create_new/create_with_alt_email→POST /api/v1/employees(User Service) сemployee_data+franchise_idиз JWT → INSERTpaykeeper_users→users_created++link_existing→ INSERTpaykeeper_users(employee_id=matched_employee_id)→users_linked++update_existing→PATCH /api/v1/employees/{matched_employee_id}(только не-пустые поля) + INSERTpaykeeper_users→users_updated++skip→ запись вerrors_json: {pk_user_id, pk_login, action: "skipped", message: "skipped by owner"}→users_skipped++- При ошибке (User Service вернул 4xx/5xx) — запись в
errors_json→users_errored++
- UPDATE
paykeeper_user_importsс финальными счётчиками +status=success(errored=0) /partial(errored>0 и created+linked+updated>0) /failed(всё errored)
Response 200
{
"data": {
"import_run_id": "uuid",
"status": "partial",
"users_total": 4,
"users_created": 2,
"users_linked": 1,
"users_updated": 0,
"users_skipped": 1,
"users_errored": 0,
"started_at": "2026-04-27T14:30:00Z",
"finished_at": "2026-04-27T14:30:08Z"
}
}Errors
| Code | HTTP | Описание |
|---|---|---|
VALIDATION_ERROR | 400 | Невалидные decisions (пустой массив, отсутствуют обязательные поля) |
ACCOUNT_NOT_FOUND | 404 | PK-аккаунт не найден |
ACCOUNT_NOT_ACTIVE | 422 | Аккаунт suspended |
FORBIDDEN | 403 | Нет требуемых permissions |
Не async-202
Импорт обычно ≤30 пользователей и укладывается в 30 секунд. Делаем синхронно. Async-вариант не нужен.
GET /internal/paykeeper/accounts/{id}/employees/imports
История прогонов импорта сотрудников из ЛК PK (BR 3.5). Используется в админке для модалки «Журнал импортов».
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.read) |
Query Parameters
| Параметр | Тип | Default | Описание |
|---|---|---|---|
limit | int | 20 | Max 100 |
since | datetime | — | Фильтр started_at >= since |
Response 200
{
"data": [
{
"id": "uuid",
"trigger": "manual",
"initiated_by_user_id": "uuid",
"initiated_by_user_name": "Иван Петров",
"started_at": "2026-04-27T14:30:00Z",
"finished_at": "2026-04-27T14:30:08Z",
"status": "partial",
"users_total": 4,
"users_created": 2,
"users_linked": 1,
"users_updated": 0,
"users_skipped": 1,
"users_errored": 0
}
],
"meta": { "limit": 20, "total": 12 }
}Errors
| Code | HTTP | Описание |
|---|---|---|
ACCOUNT_NOT_FOUND | 404 |
GET /internal/paykeeper/accounts/{id}/employees/imports/{run_id}
Детали прогона импорта, включая полный errors_json (BR 3.5).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.read) |
Response 200
{
"data": {
"id": "uuid",
"account_id": "uuid",
"trigger": "manual",
"initiated_by_user_id": "uuid",
"initiated_by_user_name": "Иван Петров",
"started_at": "2026-04-27T14:30:00Z",
"finished_at": "2026-04-27T14:30:08Z",
"status": "partial",
"users_total": 4,
"users_created": 2,
"users_linked": 1,
"users_updated": 0,
"users_skipped": 1,
"users_errored": 0,
"last_error": null,
"errors_json": [
{
"pk_user_id": "5",
"pk_login": "koala",
"action": "skipped",
"message": "skipped by owner"
}
]
}
}Errors
| Code | HTTP | Описание |
|---|---|---|
NOT_FOUND | 404 | Run не найден для этого account’а |
GET /internal/paykeeper/accounts/{id}/logs
Журнал webhook’ов для аккаунта.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (integrations.read) |
Query Parameters
| Параметр | Тип | Описание |
|---|---|---|
endpoint | string | informer / refund / receipt (опц.) |
since | datetime | Фильтр по времени |
only_invalid | boolean | Только с signature_valid=false |
page, per_page | int | Пагинация |
Response 200
{
"data": [
{
"id": "uuid",
"endpoint": "informer",
"signature_valid": true,
"dedup_key": "123456",
"processed": true,
"created_at": "2026-04-23T14:32:10Z"
}
],
"meta": { "page": 1, "per_page": 20, "total": 124 }
}Internal — для межсервисных вызовов
GET /internal/paykeeper/accounts/by-legal-entity/{legal_entity_id}
Получить статус PK-интеграции для ЮЛ (использует Admin BFF при построении карточки ЮЛ).
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Response 200
{
"data": {
"account_id": "uuid | null",
"status": "active | suspended | not_configured"
}
}GET /internal/paykeeper/terminals/by-store/{store_id}
Получить привязку терминала для ТТ (для Order Service при маршрутизации инвойса).
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Response 200
{
"data": {
"terminal_id": "uuid | null",
"account_id": "uuid | null",
"pk_terminal_id": "string | null"
}
}POST /internal/paykeeper/refunds/{id}/retry
Повторная попытка застрявшего возврата (worker dead-letter recovery).
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Общее — ответы и ошибки
Формат успеха
- Объект:
{ "data": { ... } } - Список:
{ "data": [...], "meta": { "page", "per_page", "total" } } - Создание: HTTP 201
- Удаление: HTTP 204 без тела
Формат ошибки
{
"error": {
"code": "ERROR_CODE",
"message": "Человекочитаемое описание",
"details": [{ "field": "pk_password", "message": "Обязательно" }]
}
}Общие коды ошибок
| Code | HTTP | Причина |
|---|---|---|
VALIDATION_ERROR | 400 | Невалидные данные запроса |
UNAUTHORIZED | 401 | Нет / невалидный JWT |
FORBIDDEN | 403 | Нет нужного permission’а |
NOT_FOUND | 404 | Ресурс не найден |
CONFLICT | 409 | Дубликат (UNIQUE constraint) |
BUSINESS_RULE_VIOLATION | 422 | Бизнес-правило не позволяет |
PK_CONNECTION_FAILED | 422 | Ошибка при вызове PK (сеть / 401 / 500) |
INTERNAL_ERROR | 500 |