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).

ПараметрЗначение
AuthMD5-подпись в поле key тела; см. PK answers и research §6.4
Content-Typeapplication/x-www-form-urlencoded

Path Parameters

ПараметрТипОписание
account_iduuidID PK-аккаунта (для маршрутизации и подбора informer_seed)

Request Body (form-data)

ПолеТипОбяз.Описание
idstringДаPK payment ID
sumnumberДаСумма
clientidstringДаЗначение, переданное при создании инвойса
orderidstringДаPacked bridge-ID base64url(store_id:order_number)
keystring(32)ДаMD5 подпись, ^[a-f0-9]{32}$
service_namestringНетJSON с cart + callback
client_emailstringНет
client_phonestringНет
ps_idintegerНетID платёжной системы
batch_datestringНетФлаг 2-stage hold
fop_receipt_keystringНетКлюч чека 54-ФЗ
bank_idstringНетID привязки карты
bank_payer_idstringНет
card_numberstringНетМаскированный номер
card_holderstringНет
card_expirystringНет
bank_operation_datetimestringНет

Response 200

Plain text, обязательный формат: OK <md5(id + informer_seed)>.

Пример: OK a1b2c3d4e5f67890a1b2c3d4e5f67890.

Errors

Plain text (PK ретраит при любом non-200 или неверном формате).

HTTPBodyПричина
400Missing required fieldsНе переданы id / sum / clientid / orderid / key
400Hash mismatchMD5-подпись не совпала
400Account not foundaccount_id в URL не существует
404Order not foundНе нашли наш заказ по orderid
422Sums 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.

ПараметрЗначение
AuthHMAC-SHA256 подпись в поле sign; см. research §6.6
Content-Typeapplication/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 пользователя.

ПараметрЗначение
AuthBearer JWT (integrations.read)

Query Parameters

ПараметрТипОписание
legal_entity_iduuidФильтр по ЮЛ (опц.)
statusstringactive / suspended / all (default all)
pageintdefault 1
per_pageintdefault 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-аккаунта.

ПараметрЗначение
AuthBearer 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-аккаунт.

ПараметрЗначение
AuthBearer JWT (integrations.manage)
Content-Typeapplication/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

CodeHTTPОписание
VALIDATION_ERROR400Невалидные поля
ACCOUNT_EXISTS409Для этого legal_entity_id уже есть аккаунт
PK_CONNECTION_FAILED422Проверка соединения не прошла
FORBIDDEN403Нет integrations.manage

PATCH /internal/paykeeper/accounts/{id}

Обновить аккаунт (любое поле, включая ротацию пароля/seed).

ПараметрЗначение
AuthBearer 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).

ПараметрЗначение
AuthBearer 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).

ПараметрЗначение
AuthBearer JWT (integrations.manage)

Response 204

Errors

CodeHTTPОписание
ACCOUNT_HAS_OPEN_OPERATIONS422Есть открытые инвойсы или возвраты

POST /internal/paykeeper/accounts/{id}/test-connection

Повторная проверка соединения с PK (запрос GET /info/settings/token/).

ПараметрЗначение
AuthBearer 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

Список привязок терминалов.

ПараметрЗначение
AuthBearer JWT (integrations.read)

Query Parameters

ПараметрТипОписание
account_iduuidФильтр по аккаунту (опц.)
store_iduuidФильтр по ТТ (опц.)

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

Создать привязку ТТ ↔ терминал.

ПараметрЗначение
AuthBearer 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

CodeHTTPОписание
VALIDATION_ERROR400
TERMINAL_STORE_EXISTS409Для этой ТТ уже есть привязка
TERMINAL_PK_ID_EXISTS409pk_terminal_id уже используется другой ТТ
STORE_NOT_IN_SCOPE403ТТ не принадлежит ЮЛ аккаунта

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.

ПараметрЗначение
AuthBearer JWT (integrations.manage)

Response 202 Accepted

{
  "data": {
    "sync_run_id": "uuid",
    "status": "running",
    "trigger": "manual",
    "started_at": "2026-04-24T10:00:00Z"
  }
}

Errors

CodeHTTPОписание
NOT_FOUND404Аккаунт не найден
ACCOUNT_NOT_ACTIVE422status != active
SYNC_ALREADY_RUNNING409Для этого account уже есть pk_catalog_sync_runs.status=running
FORBIDDEN403Нет integrations.manage

GET /internal/paykeeper/accounts/{id}/catalog-sync-status

Агрегированный статус синхронизации каталога для аккаунта (BR 3.4). Используется в UI админки для блока «Каталог в PayKeeper».

ПараметрЗначение
AuthBearer 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

CodeHTTPОписание
NOT_FOUND404Аккаунт не найден

GET /internal/paykeeper/accounts/{id}/catalog-sync-runs

История прогонов синхронизации каталога (BR 3.4).

ПараметрЗначение
AuthBearer JWT (integrations.read)

Query Parameters

ПараметрТипDefaultОписание
limitint20Max 100
sincedatetimeВернуть прогоны с 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).

ПараметрЗначение
AuthBearer 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

CodeHTTPОписание
NOT_FOUND404Run не найден для этого account’а

POST /internal/paykeeper/accounts/{id}/employees/preview

Получить список пользователей ЛК PK с матчингом на наших сотрудников (BR 3.5). Используется первым шагом wizard’а импорта.

ПараметрЗначение
AuthBearer JWT (integrations.manage AND employees.edit)
Content-Typeapplication/json

Path Parameters

ПараметрТипОписание
iduuidID PK-аккаунта

Request Body

Пустое тело (либо опционально {}).

Action

  1. Проверить что аккаунт active (иначе ACCOUNT_NOT_ACTIVE)
  2. Запрос GET /info/organization/users/ в {pk_server_host} с Basic auth (pk_login/pk_password из paykeeper_accounts)
  3. Для каждого 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"

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_idstringid из PK API
pk_loginstringlogin пользователя в ЛК PK
pk_emailstring | nullemail из PK API (может быть null или пустой)
pk_fiostring | nullФИО единой строкой из PK
pk_adminbooleanФлаг админа PK
pk_invoices_onlybooleanФлаг ЛК «только счета»
match_statusstringnew / matched_email / already_linked
matched_employeeobject | nullСуществующий employee при matched_email
linked_employee_iduuid | nullID связанного employee при already_linked

Errors

CodeHTTPОписание
ACCOUNT_NOT_FOUND404PK-аккаунт не найден
ACCOUNT_NOT_ACTIVE422Аккаунт suspended
PK_CONNECTION_FAILED422PK API недоступен или вернул ошибку
FORBIDDEN403Нет требуемых permissions

POST /internal/paykeeper/accounts/{id}/employees/import

Пакетный импорт сотрудников из ЛК PK по решениям, принятым владельцем в wizard’е (BR 3.5).

ПараметрЗначение
AuthBearer JWT (integrations.manage AND employees.edit)
Content-Typeapplication/json

Path Parameters

ПараметрТипОписание
iduuidID 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_idstringДаid из preview
decisions[].pk_loginstringДаlogin из preview (для журнала)
decisions[].actionstringДаcreate_new / create_with_alt_email / link_existing / update_existing / skip
decisions[].matched_employee_iduuidУсловноОбязателен для link_existing / update_existing
decisions[].employee_dataobjectУсловноОбязателен для create_new / create_with_alt_email / update_existing
employee_data.first_namestringДа
employee_data.last_namestringДа
employee_data.emailstringДаУникален в рамках franchise_id
employee_data.passwordstring | nullУсловноЕсли generate_password=false — обязательно
employee_data.generate_passwordbooleanДаtrue — adapter генерирует и шлёт reset-link на email
employee_data.phonestring | nullНет
employee_data.pinstring | nullНет4 цифры
employee_data.is_courierbooleanНетdefault false
employee_data.roles[]arrayНет[{ role_id, store_ids[] }]

Action (синхронно, timeout 60s)

  1. Создать paykeeper_user_imports с status=running, initiated_by_user_id из JWT, users_total = decisions.length
  2. Per-decision:
    • create_new / create_with_alt_emailPOST /api/v1/employees (User Service) с employee_data + franchise_id из JWT → INSERT paykeeper_usersusers_created++
    • link_existing → INSERT paykeeper_users(employee_id=matched_employee_id)users_linked++
    • update_existingPATCH /api/v1/employees/{matched_employee_id} (только не-пустые поля) + INSERT paykeeper_usersusers_updated++
    • skip → запись в errors_json: {pk_user_id, pk_login, action: "skipped", message: "skipped by owner"}users_skipped++
    • При ошибке (User Service вернул 4xx/5xx) — запись в errors_jsonusers_errored++
  3. 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

CodeHTTPОписание
VALIDATION_ERROR400Невалидные decisions (пустой массив, отсутствуют обязательные поля)
ACCOUNT_NOT_FOUND404PK-аккаунт не найден
ACCOUNT_NOT_ACTIVE422Аккаунт suspended
FORBIDDEN403Нет требуемых permissions

Не async-202

Импорт обычно ≤30 пользователей и укладывается в 30 секунд. Делаем синхронно. Async-вариант не нужен.


GET /internal/paykeeper/accounts/{id}/employees/imports

История прогонов импорта сотрудников из ЛК PK (BR 3.5). Используется в админке для модалки «Журнал импортов».

ПараметрЗначение
AuthBearer JWT (integrations.read)

Query Parameters

ПараметрТипDefaultОписание
limitint20Max 100
sincedatetimeФильтр 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

CodeHTTPОписание
ACCOUNT_NOT_FOUND404

GET /internal/paykeeper/accounts/{id}/employees/imports/{run_id}

Детали прогона импорта, включая полный errors_json (BR 3.5).

ПараметрЗначение
AuthBearer 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

CodeHTTPОписание
NOT_FOUND404Run не найден для этого account’а

GET /internal/paykeeper/accounts/{id}/logs

Журнал webhook’ов для аккаунта.

ПараметрЗначение
AuthBearer JWT (integrations.read)

Query Parameters

ПараметрТипОписание
endpointstringinformer / refund / receipt (опц.)
sincedatetimeФильтр по времени
only_invalidbooleanТолько с signature_valid=false
page, per_pageintПагинация

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 — для межсервисных вызовов

Получить статус PK-интеграции для ЮЛ (использует Admin BFF при построении карточки ЮЛ).

ПараметрЗначение
AuthX-Service-Token

Response 200

{
  "data": {
    "account_id": "uuid | null",
    "status": "active | suspended | not_configured"
  }
}

GET /internal/paykeeper/terminals/by-store/{store_id}

Получить привязку терминала для ТТ (для Order Service при маршрутизации инвойса).

ПараметрЗначение
AuthX-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).

ПараметрЗначение
AuthX-Service-Token

Общее — ответы и ошибки

Формат успеха

  • Объект: { "data": { ... } }
  • Список: { "data": [...], "meta": { "page", "per_page", "total" } }
  • Создание: HTTP 201
  • Удаление: HTTP 204 без тела

Формат ошибки

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Человекочитаемое описание",
    "details": [{ "field": "pk_password", "message": "Обязательно" }]
  }
}

Общие коды ошибок

CodeHTTPПричина
VALIDATION_ERROR400Невалидные данные запроса
UNAUTHORIZED401Нет / невалидный JWT
FORBIDDEN403Нет нужного permission’а
NOT_FOUND404Ресурс не найден
CONFLICT409Дубликат (UNIQUE constraint)
BUSINESS_RULE_VIOLATION422Бизнес-правило не позволяет
PK_CONNECTION_FAILED422Ошибка при вызове PK (сеть / 401 / 500)
INTERNAL_ERROR500