PayKeeper Must-Have для MVP — детальный разбор

Детализация 7 hard-блокеров из BR 2.4. Каждый пункт — что есть сейчас, что не хватает, какой API/payload ожидаем, как это влияет на MVP, и какие вопросы задать команде PayKeeper.

Блокеры про cross-device push (планшет → терминал), обратную синхронизацию каталога и автоматизацию онбординга ЛК — новые, добавлены после обсуждения архитектуры и lifecycle ЛК. Sandbox убран (договоримся о dev-доступе отдельно). Блокер про terminal→store mapping перенесён в workaroundable (см. BR 2.4 §3.7).

Источники: OpenAPI-спеки в _assets/paykeeper/*.yaml + gap-analysis.md + business-summary.md + vision-pos-platform.md + бэклог PayKeeper (файл доработки кассы 3 в 1 (2).xlsx).


Блокер 1. Cross-device push: внешний планшет → терминал PayKeeper

Контекст и почему это блокер для MVP

Архитектура которую хотим:

┌──────────────────────────┐          ┌──────────────────────────────┐
│   Наш планшет            │          │ PayKeeper касса 3-в-1        │
│   (frontend-only app)    │          │ (стоковый терминал с их app) │
│                          │          │                              │
│ • Каталог / меню         │   ???    │ • Экран «ожидают оплаты»     │
│ • Сборка заказа          │ ───────► │ • Кассир нажимает «Оплатить» │
│ • Работа с клиентом      │          │ • Приём карты / СБП / ApplePay│
│ • Без PayKeeper SDK      │          │ • Фискализация + ОФД         │
│ • Без оплат вообще       │          │                              │
└──────────────────────────┘          └──────────────┬───────────────┘
                                                      │ webhook
                                                      ▼
                                              Наш Order Service

Ключевое ограничение: на нашем планшете нет PayKeeper-app, нет SDK. Коммуникация с ихним терминалом должна идти через их серверы или локальный протокол.

Что мы проверили — 10 возможных механизмов

Все проверено по OpenAPI-спекам + gap-analysis.md + vision-pos-platform.md:

МеханизмЕсть?Детали
M1Open tab / очередь заказовНет концепта «открытый счёт», «pending», «черновик»
M2Pre-register invoice (создать ID заранее, терминал подхватывает)paykeeper://mobile.app/invoice/ создаёт invoice в момент вызова; нет POST /invoices/register чтобы создать заранее
M3QR-сканер на терминале, читающий deep-linkmPOS показывает QR клиенту (СБП), но не сканирует входящие
M4Web-checkout (как YooKassa)Нет веб-страницы оплаты
M5Локальный API на mPOS-устройствеmPOS — standalone app, не слушает входящие
M6IMS-каталог как «костыль» для заказаЭто справочник товаров, не заказов — позиции в чеке будут неправильные
M7Сущность «заказ» в APIНет ни в одной спеке (только payment, receipt, product)
M8Push/WebSocket на терминалТолько исходящие webhook от PayKeeper к нам, не наоборот
M9ECR/ESP/BLE/NFC локальный протоколВ device/install есть поле connection_type (Ethernet/BT/WiFi), но протокол не документирован
M10SDK / plug-in для mPOS-appНет — только deep-link вызов снаружи

Вывод: ни один из 10 механизмов не реализован. Схема «отдельный планшет → стоковый PayKeeper-терминал» архитектурно невозможна на текущем API.

Прямые цитаты из документации PayKeeper

Из paykeeper-app-mpos.yaml:

«Приложение PayKeeper может быть вызвано из внешнего приложения для быстрой инициализации приёма платежа… Для вызова приложения используются deep link’и»

Из gap-analysis.md:

«сценарий «кассир берёт терминал и бьёт заказ сам» не поддерживается. Все заказы инициируются из нашего приложения, терминал только принимает оплату по готовому cart»

Из business-summary.md:

«Минимальный сценарий который работает сегодня: все платежи инициируются только из нашего приложения, терминал PK — это только “плати-и-печатай чек”, никакой самостоятельной работы»

Что просим у PayKeeper

Минимальный набор — push invoice к terminal

1. POST /api/v1/invoices/push — наш backend пушит готовый invoice на конкретный терминал.

POST https://api.paykeeper.ru/v1/invoices/push
Authorization: Bearer {api_token}
Content-Type: application/json
 
{
  "terminal_id": "mpos_T_00042",
  "external_invoice_id": "nirbi-4281",
  "expires_in_seconds": 1800,
  "cart": [
    {
      "name": "Капучино M + сироп карамель",
      "price": 350,
      "quantity": 1,
      "tax": "vat20",
      "item_type": "goods",
      "measure": "pcs"
    },
    {
      "name": "Круассан",
      "price": 200,
      "quantity": 1,
      "tax": "vat20",
      "item_type": "goods",
      "measure": "pcs"
    }
  ],
  "pay_amount": 550,
  "allowed_payment_methods": ["card", "sbp", "cash"],
  "customer_phone": "+79123456789",
  "customer_email": "customer@example.com",
  "callback_url": "https://nirbi.ru/webhooks/paykeeper",
  "metadata": {
    "nirbi_order_id": "...",
    "nirbi_store_id": "...",
    "nirbi_table_number": 5
  }
}
 
Response 201 Created:
{
  "result": "success",
  "data": {
    "invoice_id": "pk_inv_789",
    "status": "pending",
    "expires_at": "2026-04-22T10:30:00Z"
  }
}

2. Экран «ожидают оплаты» в mPOS-app — когда на терминал поступает invoice, он появляется в списке. Кассир видит:

  • Позиции (название + цена + количество)
  • Итого
  • Кнопки «Принять оплату» / «Отклонить»
  • Опциональную информацию (номер стола, телефон клиента)

3. Уведомление о новом invoice’е — один из вариантов:

  • Push-уведомление (FCM для Android)
  • Звуковой сигнал через long-polling
  • WebSocket-подписка со стороны mPOS
  • Периодический polling GET /terminals/{id}/pending каждые 3-5 сек

4. Lifecycle-webhook’и — уведомляют нас о судьбе invoice’а:

// invoice.delivered — появился на экране терминала
{ "event_type": "invoice.delivered", "invoice_id": "pk_inv_789", "terminal_id": "..." }
 
// invoice.accepted — кассир нажал «Принять оплату»
{ "event_type": "invoice.accepted", "invoice_id": "pk_inv_789", "user_id": "..." }
 
// invoice.expired — TTL вышел, никто не принял
{ "event_type": "invoice.expired", "invoice_id": "pk_inv_789" }
 
// invoice.rejected — кассир отклонил
{ "event_type": "invoice.rejected", "invoice_id": "pk_inv_789", "reason": "..." }
 
// payment.success — клиент оплатил (тот же existing webhook, + ссылка на invoice_id)
{ ..., "invoice_id": "pk_inv_789" }

5. Отмена invoice с нашей стороныDELETE /api/v1/invoices/{invoice_id} если заказ отменился.

Почему обходы не работают

  • Single-device (наш app на их кассе): решает проблему, но ломает архитектуру MVP — зачем тогда отдельный планшет вообще? Превращает кассира в официанта.
  • Ручной ввод суммы на терминале: нарушает 54-ФЗ — в чеке должны быть позиции, а не «прочие услуги 550₽».
  • Mock-товары в IMS-каталоге: создавать «товар» Order-#4281 на каждый заказ = гигантский мусор в их системе + всё равно нарушение 54-ФЗ (позиции ненастоящие).
  • QR с deep-link’ом: их mPOS не сканирует QR с deep-link’ами (только свои СБП-QR).

Что известно из бэклога PayKeeper (файл доработки кассы 3 в 1.xlsx от Рената)

В бэклоге ✓ (реализовано) помечены пункты которые прямо противоречат выводу публичных OpenAPI-спек:

СтрокаФормулировка из бэклогаЧто подсказывает
58«Подключение сайта или мобильного приложения» ✓Внешняя система умеет работать с кассой
59«Подключение iiko» ✓iiko — внешний POS-софт — уже подключён к PayKeeper
60«Подключение Yclients» ✓Ещё одна внешняя CRM подключается
61«Подключение МойСклад» ✓Внешний учётный сервис
62«Подключение Битрикс24» ✓Внешняя CRM
63«Фискализация чеков по оплатам на сайте» ✓Оплаты с сайта фискализируются через PayKeeper
64«Оплата заказов с сайта или внешней CRM на кассе» ✓Внешний источник → касса — формально это наш сценарий
83«Создание заказов и отправка на оплату на кассу» ✓ (раздел «Мобильное приложение PayKeeper»)Их собственный mobile app создаёт заказы и отправляет на кассу

Плюс цитата из vision-pos-platform.md:

«Paykeeper — ровно то же, чем сейчас являются Ingenico или АТОЛ Ф для iiko: умная железка без своего интеллекта»

Эта аналогия намекает на ECR-стиль интеграции: iikoFront подключается к Ingenico/АТОЛ по USB/Ethernet и командует «оплатить N₽». Если PayKeeper позиционирует себя аналогично — должен быть механизм с тем же интерфейсом.

Три возможных сценария как это устроено

СценарийОписаниеПодходит нам?
А. Same-deviceiiko-приложение (iiko.Waiter / iiko.Kiosk) ставится на кассу PayKeeper 3-в-1 как сторонний Android-app и через внутренний deep-link paykeeper://... инициирует оплату. Оба приложения на одном устройстве.❌ Нет — у нас планшет отдельный
Б. ECR/cross-device APIPayKeeper-касса поддерживает режим «сторонний POS» — слушает команды по локальной сети или через backend-API от внешней системы. Именно так iiko работает с Ingenico/АТОЛ.Да, идеально подходит. Просим документацию.
В. Online-оплата (remote fiscalization)«Оплата с сайта» = клиент платит на веб-странице, PayKeeper фискализирует в облаке, выдаёт электронный чек. Физический терминал не участвует.⚠ Частично — только для online-заказов (доставка / самовывоз с предоплатой), не для прилавка

По публичным спекам PayKeeper ECR / serial / WebSocket / local API не упоминается. Это может значить:

  • Таких механизмов нет (тогда iiko работает по сценарию А или В)
  • Они есть, но оформлены отдельным «интеграционным договором» вне публичного API-пакета (типично для премиум-партнёров)

Диагностические вопросы на встречу (приоритет!)

Эти 5 вопросов должны окончательно определить сценарий:

  1. «iiko.Waiter или iiko.Kiosk устанавливается на вашу кассу 3-в-1, или iiko подключается отдельным устройством через сеть / кабель?» — отличает Сценарий А от Б
  2. «Какой конкретно протокол/API используется в подключении iiko? Это deep-link внутри одного Android-устройства, ECR по USB/Ethernet, или REST-API на вашем backend?» — получаем конкретику
  3. «Строка 64 бэклога “Оплата заказов с сайта или внешней CRM на кассе” — это про оплату онлайн (клиент платит на сайте), или физическая касса принимает заказы от внешней CRM?» — отличает Сценарий В от Б
  4. «Если бы мы хотели подключить НАШ POS-софт аналогично iiko — по какой документации? Есть ли открытая инструкция для партнёров, или это индивидуальная интеграция по договору?» — прямой вопрос на применимость
  5. «Поддерживает ли ваша касса 3-в-1 ECR-интерфейс (как у Ingenico/АТОЛ) для сторонних POS-систем?» — если да, это готовое решение

Дополнительные вопросы (из прежней версии)

  1. Архитектурный: если механизма из п.1-5 нет для партнёров — есть ли в roadmap поддержка split-device-модели?
  2. Quick-win альтернатива: может быть есть возможность добавить QR-сканер в mPOS-app чтобы принимать deep-link’и из QR-кодов?
  3. Short-term workaround: если cross-device не делают — готовы ли официально поддерживать single-device сценарий (наш софт на их Android-кассе 3-в-1)?

Решение по MVP (варианты)

ВариантАрхитектураКогда применяется
AИспользуем Сценарий Б (ECR или cross-device API PayKeeper как для iiko)Подтвердилось на встрече что это есть и доступно партнёрам
BЖдём разработки публичного cross-device push APIЕсли механизма А нет — зависит от готовности PK приоритезировать
CSingle-device: наш софт на кассе PayKeeper 3-в-1Если A и B недоступны — меняем архитектуру MVP
DPlan B (iiko API, PayKeeper отложен)Если все варианты выше отпадают

Решение зависит от того, что услышим на встрече с Ренатом.


Блокер 2. Webhook на возвраты / отмены / чеки коррекции

Текущее состояние

В lk-paykeeper-vi-mpos.yaml документирован один webhook — на успешную оплату:

Endpoint: POST {merchant_configured_url} (партнёр указывает свой URL в ЛК)

Payload (form-urlencoded):

id                              — payment ID в PayKeeper
sum                             — сумма
clientid                        — наш client ID, переданный при инициации
orderid                         — наш order ID
key                             — HMAC-SHA1(id+sum+clientid+orderid+secret)
client_email                    — email клиента
client_phone                    — телефон клиента
service_name                    — название услуги
obtain_datetime                 — когда банк подтвердил
ps_id                           — ID платёжного канала
fop_receipt_key                 — ключ чека для URL
user_id, user_login             — кассир
mpos_terminal_serial_number     — серийник железа
mpos_merchant_id                — ID ТСП
mpos_terminal_id                — логический ID терминала

Других webhook-ов в API нет. Возврат делается через deep-link paykeeper://mobile.app/refund/ — асинхронный, результат не сигнализируется обратно. Только на экране mPOS.

Почему блокер

Без webhook’ов на возврат/отмену/коррекцию мы не узнаём:

  • Когда кассир провёл возврат → наш Order Service не закроет заказ со статусом refunded
  • Когда клиент закрыл экран оплаты → заказ висит в new вечно
  • Когда ФНС выпустила чек коррекции → мы не знаем об этом вообще

Следствия:

  • Бухгалтерия не может свести день — в ERP висят «успешные» заказы, которые по факту возвращены
  • Клиент жалуется «вы сняли дважды» — не можем проверить без ручной выгрузки CSV
  • Compliance — чек коррекции не отражён в нашем журнале, налоговая может задать вопросы

Обход: только ежедневная reconciliation через CSV — не real-time, дорого для бухучёта.

Что просим у PayKeeper

Четыре новых webhook’а (или один универсальный с полем event_type):

2.1. POST /webhooks/refund_success

{
  "event_type": "refund_success",
  "original_payment_id": 12345,
  "refund_id": 67890,
  "refund_amount": 300.00,
  "original_amount": 500.00,
  "remaining_refundable": 200.00,
  "refund_receipt_key": "abc123",
  "refund_fn_number": "9960...",
  "refund_fd_number": 43,
  "refund_fp_sign": "3791...",
  "cart": [...],
  "refunded_at": "2026-04-22T10:30:00Z",
  "initiated_by_user_id": "...",
  "initiated_by_user_login": "...",
  "mpos_terminal_id": "...",
  "mpos_merchant_id": "...",
  "key": "HMAC..."
}

2.2. POST /webhooks/refund_failed

{
  "event_type": "refund_failed",
  "original_payment_id": 12345,
  "attempted_amount": 300.00,
  "reason": "INSUFFICIENT_FUNDS_ON_TERMINAL" | "NETWORK_ERROR" | "CARD_REJECTED",
  "failed_at": "2026-04-22T10:30:00Z",
  "key": "HMAC..."
}

2.3. POST /webhooks/payment_cancelled

Сценарий: клиент открыл экран оплаты, но закрыл не оплатив.

{
  "event_type": "payment_cancelled",
  "invoice_id": "...",
  "clientid": "...",
  "orderid": "...",
  "sum": 500.00,
  "cancelled_at": "2026-04-22T10:35:00Z",
  "reason": "USER_CANCELLED" | "TIMEOUT",
  "key": "HMAC..."
}

2.4. POST /webhooks/correction

Сценарий: выпущен чек коррекции (по инициативе ФНС или кассира).

{
  "event_type": "correction",
  "correction_receipt_key": "...",
  "correction_type": "self" | "by_order",
  "original_payment_id": 12345,
  "correction_amount": 100.00,
  "correction_fn_number": "...",
  "correction_fd_number": 44,
  "correction_fp_sign": "...",
  "correction_reason": "...",
  "issued_at": "2026-04-22T11:00:00Z",
  "key": "HMAC..."
}

Вопросы на встречу

  • Какой проще реализовать — 4 отдельных webhook или один универсальный с полем event_type?
  • Retry-policy: если мы не ответили 200 OK — через сколько retry? Сколько раз? Куда складывать недоставленные?
  • Можно ли подтверждать получение асинхронно (ACK через отдельный endpoint)?
  • Формат сигнатуры key — тот же HMAC-SHA1 с secret, что и для success?

Блокер 3. GET /payments — API списка платежей (reconciliation)

Текущее состояние

Нет эндпоинта для получения списка платежей. В спеках есть:

  • GET /products/ — каталог (не платежи)
  • POST /products/import — массовый импорт товаров
  • GET /shops в Partner API — список ТСП (не платежи)

Никакого способа программно получить «какие платежи прошли у этого терминала с X по Y».

Единственная альтернатива упомянутая в business-summary.mdручная выгрузка CSV из ЛК. Не автоматизируется.

Почему блокер

Это fallback когда webhook потерялся. Сценарии потери webhook:

  • Рестарт нашего сервиса в часы работы
  • Network outage между PayKeeper и нами
  • Наш endpoint вернул 500 и PayKeeper перестал ретраить
  • Ошибка сети у мобильного интернета терминала

Без reconciliation-API мы никогда не уверены что журнал платежей в ERP полный. Это напрямую бьёт по бухгалтерии — «выручка» в нашей системе ≠ «выручка» в ОФД.

Что просим у PayKeeper

GET /api/v1/payments/

Query параметры:

from            — ISO datetime, начало периода (обязательный)
to              — ISO datetime, конец периода (обязательный)
status          — success | failed | refunded | cancelled | all (default: all)
terminal_id     — filter по терминалу (optional)
merchant_id     — filter по ТСП (optional)
limit           — пагинация (default: 100, max 1000)
cursor          — cursor-based пагинация (рекомендуется)

Response:

{
  "result": "success",
  "data": {
    "payments": [
      {
        "id": 12345,
        "sum": 500.00,
        "clientid": "...",
        "orderid": "...",
        "status": "success",
        "obtain_datetime": "...",
        "fop_receipt_key": "...",
        "fn_number": "...",
        "fd_number": 42,
        "fp_sign": "...",
        "shift_number": 1,
        "receipt_number_in_shift": 3,
        "mpos_terminal_id": "...",
        "mpos_merchant_id": "...",
        "refund_status": "none" | "partial" | "full",
        "refunded_amount": 0
      }
    ],
    "next_cursor": "..." | null
  }
}

Альтернатива (минималистичная): можем работать даже с простым списком без cursor-пагинации, если from/to ограничены сутками — объём платежей в точке за день ≤ 1000.

Вопросы на встречу

  • Реально ли это добавить к маю 2026? Или есть промежуточный вариант (например, выгружать тот же CSV через URL по API-токену)?
  • Есть ли уже внутренний endpoint такого типа, просто не опубликованный в партнёрской спеке?
  • Формат ответа — наш предложенный JSON или их привычный? Готовы адаптироваться.

Блокер 4. Фискальные реквизиты в webhook

Текущее состояние

В текущем webhook’е (см. блокер 2) из фискального набора есть только fop_receipt_key — ключ для формирования URL на страницу чека:

https://{tsp}.server.paykeeper.ru/receipt/{id}/{fop_receipt_key}

На эту страницу можно зайти и глазами увидеть ФН/ФД/ФП. Но это HTML, парсинг нестабилен, формат не документирован.

Отсутствуют в payload:

  • fn_number — серийник фискального накопителя
  • fd_number — номер фискального документа (в рамках ФН)
  • fp_sign — фискальный признак документа (подпись ФНС)
  • shift_number — номер смены
  • receipt_number_in_shift — порядковый номер чека в смене
  • offd_sync_status — статус отправки в ОФД

Почему блокер

54-ФЗ требует от нас, как от учётной системы, сопоставлять чеки в нашем журнале с чеками в ОФД по уникальной тройке (ФН, ФД, ФП). Без этих полей:

  • Бухгалтерия не может доказать, что чек из нашей ERP = чек из ОФД. Сводить реестры приходится вручную по суммам и датам — ненадёжно.
  • Налоговая проверка — не можем предъявить сквозную цепочку «заказ в ERP → платёж → чек в ОФД».
  • Детектирование расхождений — если кассир выдал чек через ЛК вручную (минуя ERP), мы никак этого не поймём без ФД/ФП.

Что просим у PayKeeper

Расширить payload текущего webhookucceess новыми полями:

{
  // ...существующие поля...
  "fn_number": "9960000000123456",
  "fd_number": 42,
  "fp_sign": "3791356401",
  "shift_number": 1,
  "receipt_number_in_shift": 3,
  "offd_sync_status": "sent" | "pending" | "failed",
  "receipt_datetime": "2026-04-22T10:00:00Z"  // время фискализации (может отличаться от obtain_datetime)
}

Альтернатива если поля добавить в webhook сложно:

GET /api/v1/payments/{id}/fiscal-data

Возвращает те же фискальные поля отдельным запросом.

Вопросы на встречу

  • Технически эти поля уже есть в их системе (чек же фискализируется) — это не новая логика, а только проброс в payload. Подтвердить.
  • Можно ли сделать это в ближайшем спринте? Мы как партнёр готовы принять расширение payload без breaking-change предупреждения.
  • Если будет отдельный endpoint вместо полей в webhook — какой rate-limit ожидать? Мы будем звать его на каждый webhook.

Блокер 5. API смен ФН

Текущее состояние

Ни одного endpoint’а про смены во всех 6 OpenAPI-спеках. Ни shift, ни смена, ни Z-report, ни X-report, ни FN-status.

mPOS-приложение кассы очевидно умеет открывать/закрывать смену (иначе бы не работала фискализация), но делается это только вручную через UI касса-в-руке. Никакого API/webhook.

Почему блокер

Главное ограничение 54-ФЗ: ФН блокирует кассу через 24 часа без закрытия смены.

Сценарии что происходит:

  • Кассир открыл смену утром в 08:00
  • Смена должна быть закрыта до 08:00 следующего дня
  • Если кассир ушёл, не закрыв, или забыл — в 08:01 касса блокируется
  • Клиенты в момент блокировки — просто разворачиваются и уходят
  • Менеджер обнаруживает проблему через жалобы

Без API смен мы:

  • Не знаем заранее что смена сейчас заблокируется
  • Не можем программно закрыть смену (e.g. если все кассиры забыли)
  • Не узнаём что смена открылась/закрылась — не можем зажечь индикатор в админке
  • Не получаем Z-отчёт в структурированном виде (для бухучёта)

Что просим у PayKeeper

5.1. GET /api/v1/mpos/terminal/{terminal_id}/shift

Response:

{
  "result": "success",
  "data": {
    "terminal_id": "...",
    "shift_status": "open" | "closed",
    "shift_number": 42,
    "opened_at": "2026-04-22T08:00:00Z",
    "opened_by_user_login": "cashier1",
    "transactions_count": 17,
    "total_sum": 8500.00,
    "total_cash": 3000.00,
    "total_card": 5500.00,
    "time_remaining_seconds": 28800,      // до 24h-блокировки
    "fn_expiry_warning": false,
    "fn_expiry_date": "2027-04-01T00:00:00Z"  // когда сам ФН (железка) истекает
  }
}

5.2. POST /api/v1/mpos/terminal/{terminal_id}/shift/close

Программное закрытие смены. Полезно для ночного auto-close в нашей системе.

Response:

{
  "result": "success",
  "data": {
    "shift_number": 42,
    "closed_at": "2026-04-22T20:00:00Z",
    "z_report_fd_number": 43,
    "z_report_fp_sign": "...",
    "transactions_count": 17,
    "total_sum": 8500.00,
    "breakdown_by_method": {
      "cash": 3000.00,
      "card": 5500.00,
      "sbp": 0
    }
  }
}

5.3. Webhook shift_opened / shift_closed / shift_expiring

// shift_opened
{
  "event_type": "shift_opened",
  "terminal_id": "...",
  "shift_number": 42,
  "opened_at": "...",
  "opened_by_user_login": "..."
}
 
// shift_closed (с Z-отчётом inline)
{
  "event_type": "shift_closed",
  "terminal_id": "...",
  "shift_number": 42,
  "closed_at": "...",
  "z_report": {
    "fd_number": 43,
    "fp_sign": "...",
    "transactions_count": 17,
    "total_sum": 8500.00,
    "breakdown": {...}
  }
}
 
// shift_expiring — за 1 час до 24h-блокировки
{
  "event_type": "shift_expiring",
  "terminal_id": "...",
  "shift_number": 42,
  "opened_at": "2026-04-22T08:00:00Z",
  "will_expire_at": "2026-04-23T08:00:00Z",
  "minutes_remaining": 60
}

Вопросы на встречу

  • Смены — вообще есть у PayKeeper на уровне API, или только UI?
  • Можем ли мы сейчас хотя бы узнать статус смены через внутренний запрос (неофициальный)?
  • Возможность force-close программно — это новая логика, или mPOS это уже умеет?
  • X-отчёт (промежуточный, без обнуления) тоже нужен или только Z?

Блокер 6. Обратная синхронизация каталога (PayKeeper → ERP)

Контекст

Каталог товаров в PayKeeper — редактируемый. Владелец или администратор мерчанта через ЛК PayKeeper могут:

  • Добавлять новые товары (POST /products/)
  • Менять цену, НДС, название (PATCH /product/{id})
  • Добавлять/редактировать категории-теги (POST /tags/, PATCH /tag/{id})
  • Делать массовый импорт через POST /products/import
  • Синхронизировать из внешних систем (1С, Yclients, МойСклад, Битрикс24 — коннекторы у PK есть)

Текущее состояние

Нет ни одного способа узнать что в каталоге PayKeeper что-то поменялось:

МеханизмСтатус
Webhook product.changed / category.changed❌ Нет
Поле updated_at / last_modified на товарах❌ Нет (не можем сравнивать временные метки)
Endpoint GET /products?modified_since={ISO}❌ Нет
Флаг catalog_lock / read-only режим❌ Нет

Единственная альтернатива: забирать весь каталог целиком через GET /products/ и сравнивать с нашим. Дорого и бессмысленно если товаров много.

Почему блокер

Если наш ERP периодически пушит каталог в PayKeeper (наш основной поток), а мерчант параллельно правит цены в ЛК — при следующем push’е мы их правки затрём. Типичные сценарии:

  1. Владелец изменил цену в ЛК. В понедельник с 09:00 новая цена 350₽ в PK, в нашем ERP всё ещё 300₽. В 03:00 ночного пересчёта наш push прилетает — цена снова 300₽. Мерчант звонит: «вы сломали мой прайс».

  2. Владелец добавил сезонный товар в ЛК (напр. «Яблочный пирог»). Через сутки наш push прилетает с полным каталогом без этого товара — PayKeeper удаляет товар из своего каталога, кассир больше не может его пробить.

  3. Мерчант использует 1С-коннектор параллельно с нашим ERP. В ЛК товары из 1С и из нашей админки — перемешиваются и конфликтуют. Ни один из push’ей не знает про чужие правки.

  4. Реальные чеки содержат товар которого нет в нашей ERP. Выручка в кассе 450₽ за «Торт», а в нашем каталоге «Торта» нет вообще. Бухгалтерия не может сопоставить. Compliance-риск.

Что просим у PayKeeper

Вариант 1 — push-модель (предпочтительно)

Webhook catalog.product.changed:

{
  "event_type": "catalog.product.changed",
  "action": "created" | "updated" | "deleted",
  "product_id": "pk_prod_789",
  "changed_fields": ["price", "name"],
  "changed_by_user_id": "lk_user_456",
  "changed_at": "2026-04-22T11:00:00Z",
  "source": "lk_ui" | "ims_api" | "1c_connector" | "yclients",
  "new_values": {
    "name": "Торт Наполеон",
    "price": 450
  }
}

Аналогично catalog.category.changed.

Вариант 2 — pull-модель (минимальный фолбэк)

Endpoint GET /products?modified_since={ISO}&limit=100&cursor=...:

Возвращает только товары, изменённые после указанной метки. Требует чтобы PayKeeper хранил updated_at на каждой записи.

Вариант 3 — catalog lock (радикальный)

POST /api/v1/catalog/lock / POST /api/v1/catalog/unlock — флаг который делает ЛК-редактирование запрещённым для этой ТСП. Только API-запись через наш IMS-ключ работает. UI показывает «каталог управляется внешней системой».

Это проще реализовать со стороны PayKeeper, но жёстко ограничивает мерчанта — не для всех ОК.

Обход на нашей стороне

Если ни один из вариантов от PayKeeper не появится:

  • Контрактное SLA — в договоре с франшизой прописать: «редактирование каталога в ЛК PayKeeper запрещено, все правки через админку НИРБИ». Нарушение = мы не отвечаем за расхождения.
  • Nightly full reconciliation — еженочно GET /products/, diff с нашим каталогом, алерт в Slack/email оператору при расхождении. Оператор разбирается вручную.
  • Защитный механизм при push’е — перед push’ем сначала прочитать текущий каталог PayKeeper. Если есть товары которых нет у нас — не удалять их, а пометить в ЛК тегом external_edit и ждать ручного разрешения конфликта.

Минусы обхода: ручная работа + лаг до суток + всё равно возможны конфликты в момент между чтением и записью.

Вопросы на встречу

  1. Webhook на каталог — есть в roadmap? В бэклоге у вас есть «Обмен с 1С», «Подключение Yclients» — как они узнают о изменениях? Может быть тот же канал подойдёт нам.
  2. Можно ли флагом отключить ЛК-редактирование каталога для конкретной ТСП?
  3. updated_at на товарах технически есть в БД? Если да — готовы ли отдать в API?
  4. Когда мерчант делает правку через 1С-коннектор — как PayKeeper это различает от правки через ЛК? Есть ли внутренний audit log?

Блокер 7. Автоматизация онбординга ЛК

Контекст

Когда мы регистрируем новую ТТ через Partner API (POST /client/add), PayKeeper создаёт ТСП и возвращает paykeeper_id. Это ещё не готовый ЛК. Дальше происходит:

POST /client/add → paykeeper_id
  ↓ (AutoInstaller в фоне)
Провижининг ЛК (создаётся хост myshop.server.paykeeper.ru)
Настройка ОФД
Отправка железки-терминала мерчанту
Активация ЛК
Мерчант получает email с доступом
Мерчант логинится в ЛК
Мерчант вручную прописывает настройки webhook

От POST /client/add до момента «в ЛК всё настроено, webhook летит к нам» — проходят часы или дни, и в процессе есть ручные шаги.

Текущее состояние — три проблемы

Проблема 1. Нет webhook’а «ЛК готов»

PayKeeper не уведомляет нас когда провижининг ЛК завершён. Узнать готовность можно только:

  • Polling GET /api/requests — запрашиваем статус задачи из AutoInstaller каждые N секунд. Статусы: in_queuein_progressinstalled / failed.
  • Polling GET /api/request_config_fiscal — то же для настройки ОФД.
  • Polling GET /device/status — для статуса отгрузки/монтажа железа.

Работает, но неудобно — надо держать scheduler, крутить polling часами.

Проблема 2. Webhook URL прописывается мерчантом вручную

Чтобы к нашему backend’у прилетал webhook об оплате, в настройках ТСП должен быть прописан наш callback URL (напр. https://nirbi.ru/webhooks/paykeeper/{store_id}).

API чтобы сделать это программно — НЕТ. В POST /client/add можно передать только legal_name, site, контакты.

Значит мерчант должен:

  1. Открыть ЛК
  2. Зайти в «Настройки уведомлений»
  3. Вбить URL руками
  4. Сохранить

Если забыл — в нашем ERP ни одного платежа, хотя касса работает.

Проблема 3. HMAC-secret копируется мерчантом вручную

PayKeeper подписывает webhook’и HMAC-SHA1 по формуле:

key = SHA1(id + sum + clientid + orderid + secret)

Секрет генерируется на стороне PayKeeper при создании ТСП. Показывается в ЛК мерчанту. API для программного получения секрета — НЕТ. Мерчант должен:

  1. Открыть ЛК
  2. Скопировать secret из соответствующего поля
  3. Вставить в нашу админку в поле «секрет PayKeeper»

Без секрета мы не можем проверить подпись → либо принимаем все webhook’и без проверки (риск подмены), либо отбрасываем все (теряем платежи).

Почему блокер

Ручные шаги + человеческий фактор = потеря платежей. При массовом подключении франшизы (20+ ТТ за неделю) вероятность что оператор где-то забыл настроить webhook URL — высокая.

Симптом: касса работает, чеки пробиваются, деньги идут — а в нашем ERP на этой ТТ тишина. Обнаруживается на сверке в конце дня / недели, к тому моменту — десятки пропущенных платежей.

Что просим у PayKeeper

1. Расширение POST /client/add

Разрешить передавать webhook_url и external_id при создании ТСП, получать webhook_secret в ответе:

// Request
{
  "legal_name": "ООО Кофейня №1",
  "site": "...",
  "contact_email": "...",
  "contact_phone": "...",
  "service_type": "...",
  "webhook_url": "https://nirbi.ru/webhooks/paykeeper",
  "external_id": "store_uuid_from_nirbi",
  "webhook_success_path": "/success",
  "webhook_refund_path": "/refund"
}
 
// Response
{
  "paykeeper_id": "210209-027-44",
  "webhook_url": "https://nirbi.ru/webhooks/paykeeper",
  "webhook_secret": "auto-generated-hmac-secret",
  "hostname": "myshop.server.paykeeper.ru",
  "status": "provisioning"
}

2. Webhook lk.created / onboarding.completed

Когда ЛК активирован и готов принимать платежи — уведомляют нас:

{
  "event_type": "lk.created",
  "paykeeper_id": "210209-027-44",
  "hostname": "myshop.server.paykeeper.ru",
  "external_id": "store_uuid_from_nirbi",
  "activated_at": "2026-04-22T14:00:00Z"
}

Избавляет от polling’а GET /api/requests.

3. GET /api/v1/client/{paykeeper_id}/secret

Программное чтение текущего secret’а — на случай потери / rotation’а:

{
  "paykeeper_id": "210209-027-44",
  "webhook_secret": "current-active-secret",
  "rotated_at": "2026-04-22T14:00:00Z"
}

Обход на нашей стороне (для MVP без изменений PayKeeper)

В админке НИРБИ после нажатия «Создать ТТ» — wizard с 4 шагами:

Шаг 1 — автоматический:

  • POST /client/add → сохраняем paykeeper_id
  • Polling GET /api/requests → ждём installed
  • Polling GET /device/status → ждём отгрузку/монтаж
  • Показываем прогресс-бар оператору (может занять часы)

Шаг 2 — инструкция мерчанту:

  • Генерируем email/SMS с доступами к ЛК
  • Отправляем мерчанту: «зайдите в ЛК → Настройки → пропишите URL: https://nirbi.ru/webhooks/paykeeper/{store_id}»

Шаг 3 — ввод secret:

  • Оператор / мерчант вводит secret из ЛК в нашу админку
  • Сохраняем в store_paykeeper_mapping.webhook_secret

Шаг 4 — smoke-test:

  • Просим мерчанта пробить тестовый чек (50₽)
  • Если webhook пришёл за 30 сек → помечаем ТТ active
  • Если не пришёл → показываем диагностику «проверьте настройки webhook в ЛК»

Плюс monitoring alert «по этой ТТ нет webhook’ов уже 24ч» — чтобы обнаружить забытые настройки.

Вопросы на встречу

  1. Расширение POST /client/add для webhook_url и external_id — есть в планах?
  2. Уведомления о завершении провижининга ЛК — возможно сделать?
  3. API чтения secret — критично или есть принципиальная позиция «только в UI мерчанту»?
  4. Какой сейчас средний срок от POST /client/add до реально работающего ЛК? Часы? Дни? Это важно для UX нашего онбординг-wizard’а.

Итоговая таблица «что просим»

Hard blockers — запросы к PayKeeper

БлокерСтатус по бэклогу PayKeeper
1Cross-device push (планшет→терминал)🟡 Возможно уже есть — строки 64, 83, 59 бэклога помечены ✓ (CRM/iiko/mobile app). Уточнить у Рената.
2Webhook возвратов/коррекций🟡 UI-операции в ЛК есть (возвраты, коррекции), но webhook’и не упомянуты. Уточнить.
3GET /payments🟡 «Realtime данные об операциях» помечено ✓. Скорее всего API есть — уточнить публичен ли.
4Фиск-поля в webhook⚠ Фискализация работает, но состав payload webhook’а по бэклогу не понятен. Уточнить.
5API смен ФН❌ Отчёты (X/Z) есть, но программного API смен в бэклоге не видно. Блокер сохраняется.
6Webhook на изменения в каталоге ЛК❌ Нет webhook’ов, нет updated_at, нет catalog-lock. Если мерчант / 1С / Yclients поменяют каталог — мы не узнаем, наш push затрёт. Блокер.
7Автоматизация онбординга ЛК❌ 3 ручных шага при каждой новой ТТ: polling готовности, ввод webhook URL мерчантом, copy-paste secret’а. Не webhook’а lk.created, нет webhook_url в POST /client/add, нет API чтения secret’а. При массовом онбординге — высокий риск пропущенных платежей.

Workaroundable — решаем у себя

ЧтоРешениеBR ref
Модификаторы / иерархия / цены по ТТ / стоп-листыDeep-link с готовым cartBR 2.4 §3.1-3.4
Роли кассировМаппим наши permissions в flat-flagsBR 2.4 §3.5
Error details в bulk importМелкие батчиBR 2.4 §3.6
terminal_id → store_id mappingТаблица store_paykeeper_mapping в Store Service + wrapper Partner APIBR 2.4 §3.7

От PayKeeper для п.6 — только 2 уточнения: стабильность ID и связь paykeeper_idmpos_merchant_id.


Что будет если PayKeeper не успеет (Plan B)

См. 02-ADR/ADR-016 Переход на PayKeeper — там зафиксировано: если ключевые API не готовы к запуску, активируем Plan B — интеграция по схеме iiko через iiko API. PayKeeper откладывается.

Из 7 блокеров критичны для Plan A (PayKeeper) в порядке убывания:

  1. Cross-device push (блокер 1)архитектурный блокер. Если нет — MVP либо меняет архитектуру (single-device), либо уходит в Plan B. По бэклогу возможно уже есть — уточнить.
  2. API смен ФН (блокер 5) — прямой риск простоя, без обхода
  3. Фиск-поля в webhook (блокер 4) — compliance-блокер, без вариантов
  4. Webhook возвратов (блокер 2) — без него расходится учёт
  5. GET /payments (блокер 3) — reconciliation fallback
  6. Webhook на каталог (блокер 6) — без него правки в ЛК будут конфликтовать с нашим push’ем
  7. Автоматизация онбординга (блокер 7) — не критично для первой ТТ, но критично для масштабирования франшизы

Триггер Plan B: если PayKeeper не подтверждает разработку или наличие блокера 1 — ADR-016 Plan B активируется.

Альтернативный путь для Plan A (если PayKeeper отказывается от cross-device): пересмотр архитектуры MVP на single-device, где наш софт работает на кассе 3-в-1 PayKeeper как единое приложение. Теряем разделение «официант / кассир», но сохраняем PayKeeper как эквайера.


Ссылки