Пангалинк сегодня (2026): архитектура iPizza и облачные альтернативы
Восемнадцать лет назад я написал про пангалинк статью, в которой фигурировали эстонские кроны, банки Hanza и Sampo, и подключение за 750 крон. Тот текст сейчас читается как археология: половины банков нет, валюты нет, тестовый сервис закрылся, а рядом выросла целая индустрия, которая делает то же самое, но по-другому.
При этом сам протокол iPizza живее всех живых. Поэтому имеет смысл написать заново: что это за зверь архитектурно, почему он выглядит именно так, и что стоит выбирать в 2026 году.
Проблема, которую решает пангалинк
Начну с базовой вещи, потому что она неочевидна, если вы не жили в этой области.
Когда вы платите картой, деньги идут по карточной сети: магазин -> эквайер -> Visa/Mastercard -> банк-эмитент. Это дорого (1-3% с транзакции), медленно в расчётах (деньги идут дни) и обратимо: клиент может через месяц сделать chargeback, и магазин останется без товара и без денег.
Банковская ссылка (эст. pangalink) идёт другим путём. Это не карта. Это обычный банковский перевод со счёта на счёт, только заполненный за клиента и подтверждённый им в его же интернет-банке. Магазин получает настоящий SEPA-перевод. Отсюда свойства:
- дёшево (сейчас это единицы центов за транзакцию, не проценты)
- деньги приходят сразу, если банк в системе мгновенных платежей
- необратимо: chargeback как понятия не существует, перевод есть перевод
Последний пункт объясняет, почему в Эстонии, Латвии, Литве, Финляндии и Польше локальные банковские ссылки победили карты в интернет-магазинах, а в США и Британии их практически нет. Там карточная инфраструктура пришла раньше и заняла место.
Архитектура: браузер как транспорт
Вот главная идея, которую надо понять про iPizza. Она странная, и это осознанная странность 2000-х годов.
Магазин никогда не разговаривает с банком напрямую. Нет никакого API-вызова, нет исходящего HTTP-запроса из вашего сервера в банк, нет ключей API и токенов. Вместо этого:
- Магазин рисует на странице обычную HTML-форму со скрытыми полями
- Поля подписаны приватным RSA-ключом магазина
actionформы указывает на URL банка- Клиент нажимает кнопку, его браузер отправляет форму в банк
- Банк проверяет подпись, показывает клиенту предзаполненный платёж
- Клиент подтверждает
- Банк отправляет браузер клиента обратно в магазин с новой формой, подписанной уже ключом банка
- Магазин проверяет подпись банка и засчитывает заказ
sequenceDiagram
autonumber
participant U as Браузер клиента
participant S as Сервер магазина
participant B as Банк
U->>S: GET /checkout
S-->>U: HTML с подписанной формой<br/>(VK_SERVICE=1012, VK_AMOUNT, VK_MAC)
Note over S,B: между сервером магазина и банком<br/>прямого канала НЕТ
U->>B: POST формы (браузер, не сервер)
B->>B: проверка VK_MAC<br/>публичным ключом магазина
B-->>U: логин и подтверждение платежа
U->>B: подтверждает
B-->>U: HTML-форма назад в магазин<br/>(VK_SERVICE=1111, VK_MAC ключом банка)
U->>S: POST /return
S->>S: проверка VK_MAC<br/>публичным ключом банка
S-->>U: заказ оплачен
Модель безопасности здесь называется «подписанные сообщения через недоверенный транспорт». Браузер клиента - это почтальон, которому не доверяют, поэтому конверт запечатан криптографической подписью.
Почему так, а не нормальный API? Потому что спецификация родом из начала 2000-х. Тогда у среднего эстонского интернет-магазина не было ни статического IP, ни возможности принимать входящие соединения от банка, ни понятия про OAuth. Зато у всех был браузер и умение отрендерить <form>. Банкам это было удобно: не нужно держать API-шлюз, не нужно управлять сессиями магазинов, вся интеграция помещается в PDF на 10 страниц.
У этого дизайна есть неожиданное практическое следствие, которое я много раз наблюдал: на странице оплаты формы для всех банков уже подписаны и лежат рядом. Никакой JavaScript не нужен, никакого AJAX. Клик по кнопке «SEB» это и есть отправка платежа. Ваш сервер между кликом и банком не участвует вообще, и узнаёт о происходящем только когда клиент вернётся.
Протокол iPizza по полочкам
Все поля начинаются с префикса VK_. Это наследие оригинальной спецификации Hansapank, и что именно означает сокращение, в текущих документах банков уже не пишут.
Каждое сообщение это набор полей плюс подпись. Тип сообщения задаётся номером сервиса:
| Сервис | Направление | Что означает |
|---|---|---|
1011 |
магазин -> банк | Платёж, где магазин сам задаёт счёт и имя получателя |
1012 |
магазин -> банк | Платёж, где счёт получателя берётся из договора с банком |
1111 |
банк -> магазин | Платёж прошёл |
1911 |
банк -> магазин | Платёж не прошёл |
4011 / 4012 |
магазин -> банк | Запрос аутентификации клиента |
3012 / 3013 |
банк -> магазин | Ответ с личными данными клиента |
На практике почти все используют 1012: реквизиты магазина уже прошиты в договоре, и подделать получателя нельзя даже теоретически. 1011 нужен редко, например при агентских схемах.
Основные поля платёжного запроса:
| Поле | Смысл |
|---|---|
VK_SERVICE |
номер сервиса, 1012 |
VK_VERSION |
версия алгоритма подписи, 008 или 009 |
VK_SND_ID |
ID магазина, выданный банком |
VK_STAMP |
ваш идентификатор запроса, до 20 символов |
VK_AMOUNT |
сумма, точка как разделитель, без разделителя тысяч |
VK_CURR |
EUR |
VK_REF |
номер ссылки платежа (viitenumber) с контрольной цифрой 7-3-1 |
VK_MSG |
текст, который клиент увидит в банке |
VK_RETURN / VK_CANCEL |
куда вернуть браузер после успеха и неудачи |
VK_DATETIME |
время в ISO 8601 с таймзоной |
VK_LANG |
EST, ENG или RUS |
VK_ENCODING |
кодировка, по умолчанию UTF-8 |
VK_MAC |
сама подпись, base64 |
В ответе 1111 добавляются VK_T_NO (номер платёжного поручения в банке), VK_SND_ACC и VK_SND_NAME (счёт и имя плательщика) и важный флаг VK_AUTO.
VK_AUTO: единственный намёк на server-to-server
VK_AUTO=N означает «это клиент вернулся к вам в браузере». VK_AUTO=Y означает «клиент к вам не дошёл, но платёж прошёл, и банк уведомляет вас сам».
Это существенно. Клиент может закрыть вкладку сразу после подтверждения платежа, и тогда единственное, что скажет вам об оплате, это автоматический ответ банка. Обработчик возврата обязан быть идемпотентным: он вполне может получить и VK_AUTO=N, и VK_AUTO=Y по одному заказу.
Как считается VK_MAC
Тут спрятана деталь, на которой спотыкаются все, кто пишет свою реализацию.
Подпись считается не по «строке параметров как в GET-запросе». Значения полей склеиваются в фиксированном порядке, и перед каждым значением ставится его длина в символах, отформатированная как трёхзначное число:
p(x1) || x1 || p(x2) || x2 || ... || p(xn) || xn
где p("EXAMPLE") даёт 007, а пустое поле даёт 000. Полученная строка хешируется и подписывается приватным RSA-ключом.
Зачем такая экзотика? Это защита от подмены границ полей. Если бы поля просто склеивались, то сумма 10 с сообщением 0 EUR дала бы ту же строку, что сумма 100 с сообщением EUR. Префикс длины делает разбор однозначным. Ровно та же идея, что в length-prefixed encoding в бинарных протоколах.
Два варианта алгоритма:
008: RSA + SHA-1. Историческая версия, до сих пор массово в проде009: RSA + SHA-512. Появилась позже, поддерживается банками сегодня
Если вы делаете интеграцию сейчас, берите 009. SHA-1 в 2026 году в новом коде это плохой сигнал, даже если конкретно здесь атака на коллизии не даёт очевидного выигрыша.
Порядок полей при подписи задаётся спецификацией, а не порядком в форме. Для каждого сервиса свой список. Это самая частая причина «подпись не сходится, а всё вроде правильно».
Аутентификация: пангалинк как логин
Помимо оплаты, банки продают аутентификацию: сервисы 4011/4012 уходят в банк, 3012/3013 возвращают имя, фамилию и личный код (isikukood) клиента. Это использовалось для входа в госсервисы и порталы, когда ID-карта была неудобна.
Здесь есть отдельная ловушка. При проверке ответа мало проверить подпись, надо ещё проверить:
VK_REC_ID- что сообщение адресовано именно вам, а не другому магазинуVK_DATETIME- что оно не старше плюс-минус 5 минутVK_NONCEв3013- что это ответ на ваш конкретный запрос
Без этих проверок подписанный банком ответ можно переиграть: он валиден по подписи вечно и для кого угодно. Классическая replay-атака.
Забавно, что в списке кодов средств аутентификации (VK_TOKEN) видна вся история эстонской электронной идентичности: 1 - ID-карта, 2 - Mobiil-ID, 5 - бумажная карточка одноразовых кодов, 6 - PIN-калькулятор, 9 - Smart-ID, 12 - биометрия.
Где здесь можно порезаться
Раз уж мы говорим про деньги, перечислю то, что ломается на практике.
Не проверять подпись ответа. Это фатально и это происходит регулярно. Обработчик возврата это публичный эндпоинт, к которому приходит POST от третьей стороны. У него нет вашей сессии и нет CSRF-токена, поэтому обе защиты приходится отключить. После этого подпись остаётся единственным, что отделяет постороннего человека от бесплатного заказа. Кто угодно может послать вам VK_SERVICE=1111 с чужим номером заказа.
Не сверять сумму. Проверили подпись, но не сравнили VK_AMOUNT с суммой заказа у себя. Банк подписал ровно то, что клиент подтвердил, а клиент мог подтвердить платёж на другую сумму, если вы дали ему сервис 1011 или у вас есть другая дыра.
Не проверять VK_REF или VK_STAMP против своего заказа. Атака: взять валидный подписанный ответ по своему заказу на 1 евро и попробовать применить его к чужому заказу на 1000.
Считать успехом сам факт возврата на success URL. Реальный случай, который я видел: банк возвращает клиента на success-адрес, но с VK_AMOUNT равным нулю, потому что платёж на самом деле не прошёл. Верить надо содержимому сообщения, а не тому, на какой из двух URL вас привели.
Не быть идемпотентным. Из-за VK_AUTO вы получите два уведомления по одному платежу. Если каждое создаёт платёж в базе, у вас будет двойная оплата в отчётности.
Общее правило: success URL и failure URL это не сигнал, это просто два роутинга. Сигнал только внутри подписанного сообщения.
Сертификаты: главный источник боли
Теперь про то, что интересовало отдельно: насколько автоматизирован выпуск ключей.
Ответ короткий: никак не автоматизирован. Вообще. В 2026 году.
Процесс выглядит так:
- Заключить договор с банком.
- Сгенерировать RSA-пару:
openssl genrsa 2048. - Сделать CSR или self-signed сертификат:
openssl req -new. - Отправить публичную часть в банк через интернет-банк или почтой.
- Дождаться, пока человек в банке активирует сертификат. Это занимает от нескольких часов до нескольких дней.
- Получить сертификат банка.
- Положить оба файла на сервер.
- Через несколько лет всё протухнет, и никто не напомнит.
Конкретика на примере требований LHV: RSA минимум 2048 бит, формат X.509 PEM, срок действия не больше 10 лет, self-signed сертификат принимается. Coop Pank в своей инструкции рекомендует 4096 бит и отдельную пару, не связанную с TLS-сертификатом сервера.
Две команды, которые составляют весь «выпуск»:
openssl genrsa 2048 > privkey.pem
openssl req -new -key privkey.pem -out cert-req.pem
Дальше cert-req.pem уезжает в банк, а вы ждёте письма.
Насколько это редкая операция, хорошо видно по тому, что Эстонский союз электронной коммерции в 2025 году рекомендовал своим членам веб-генератор ключей от Zone.ee: страница, которая генерирует пару прямо в браузере. Не потому что openssl сложный, а потому что этим занимаются раз в несколько лет и каждый раз заново вспоминают синтаксис.
Сравните с тем, как это выглядит у облачных провайдеров: заходите в личный кабинет, жмёте кнопку, получаете access key и secret key. Тридцать секунд, сами, в любое время суток, в двух окружениях (sandbox и production). Никакой криптографии на вашей стороне вообще.
Отдельно про истечение срока. Это то, на чём люди горят. Сертификат живёт до 10 лет, то есть его выдал человек, который давно уволился, документации нет, мониторинга срока нет, письма из банка приходят на почтовый ящик, которого больше не существует. В один прекрасный день оплата в магазине просто перестаёт работать. Если вы наследуете такую интеграцию, первым делом посмотрите даты в сертификатах и поставьте на них алерт.
Что изменилось с 2007 года
Чтобы не переписывать старую статью, вот дельта.
| Тогда | Сейчас |
|---|---|
| Hanza / Hansapank | Swedbank (переименован в 2008) |
| Sampo Pank -> Danske | ушли из Эстонии, портфель у LHV |
| Nordea со своим протоколом SOLOPMT | слились с DNB в Luminor, SOLOPMT мёртв |
| Krediidipank | Coop Pank |
| Кроны, подключение за 750-1000 крон плюс абонплата | Евро, у LHV подключение бесплатно и порядка 0.05 евро за транзакцию |
Только 008 (SHA-1) |
Есть 009 (SHA-512) |
| Публичный эмулятор pangalink.net | Сервис закрыт, остался исходник на GitHub и сторонние зеркала |
| Договор с каждым банком отдельно | Один договор с агрегатором на все банки |
| Никакой альтернативы | PSD2 и целый рынок провайдеров |
Отдельно отмечу экономику. Раньше банковская ссылка была услугой для тех, кому не жалко тысячи крон в год. Сейчас у LHV это 5 центов за транзакцию без абонентской платы. Барьер входа исчез, и вместе с ним исчезла причина писать интеграцию с каждым банком руками.
Три способа принимать банковские платежи сегодня
Это, пожалуй, главный практический вывод статьи.
flowchart TB
subgraph A["А. Прямой iPizza"]
A1["Магазин"] -->|VK_MAC своим ключом| A2["Swedbank"]
A1 -->|VK_MAC своим ключом| A3["SEB"]
A1 -->|VK_MAC своим ключом| A4["LHV"]
end
style A fill:#f8d7da,stroke:#dc3545
flowchart TB
subgraph B["Б. Агрегатор банклинков"]
B1["Магазин"] -->|один iPizza-договор| B2["LHV / Maksekeskus / EveryPay"]
B2 --> B3["все банки"]
end
style B fill:#fff3cd,stroke:#ffc107
flowchart TB
subgraph C["В. PSD2-агрегатор"]
C1["Магазин"] -->|REST + JWT| C2["Montonio"]
C2 -->|Open Banking API| C3["все банки ЕС"]
end
style C fill:#d4edda,stroke:#28a745
А. Прямой iPizza с каждым банком
Вы подписываете договор с каждым банком отдельно, генерируете ключи для каждого, храните сертификаты каждого, и поддерживаете свою реализацию протокола.
Плюсы: минимальная комиссия, деньги приходят прямо на ваш счёт, никакого посредника в цепочке.
Минусы: N договоров, N сертификатов с N сроками истечения, ваш код отвечает за криптографию платежей, и всё это ради протокола, который никто не развивает.
Когда оправдано: большой оборот, где доли цента с транзакции превращаются в деньги, и есть команда, которая это сопровождает. Либо унаследованная интеграция, которая работает и её не трогают.
Б. Агрегатор поверх банковских ссылок
Вы подписываете один договор, и получаете кнопки всех банков. LHV прямо продаёт это как «одно соглашение = все банклинки»: LHV, Swedbank, SEB, Luminor, Coop, Citadele, Artea, Revolut, причём по всей Балтии. Похожую роль играют Maksekeskus (бренд MakeCommerce) и EveryPay.
Технически часть из них внутри работает по тем же банковским ссылкам, но вам это не видно.
Ключевое отличие для разработчика: у Maksekeskus и EveryPay аутентификация по паре «идентификатор плюс секрет», а не по сертификатам. Вся история с openssl и письмами в банк исчезает.
В. PSD2-агрегатор
Это архитектурно другая вещь, и на неё стоит смотреть отдельно.
Директива PSD2 обязала все европейские банки открыть API, через которые лицензированный провайдер может инициировать платёж от имени клиента. Такой провайдер называется PISP (Payment Initiation Service Provider). Банки обязаны его пускать, потому что это закон, а не коммерческая договорённость.
Montonio работает именно так. У них лицензия платёжного учреждения (Литва, №51 от 2020 года, право оказывать услуги в Эстонии трансгранично), они зарегистрированы как PISP, и метод оплаты у них в API так и называется: paymentInitiation.
Что это даёт по сравнению с iPizza:
- Покрытие не ограничено эстонскими банками. PSD2 работает по всему ЕС, поэтому в списке появляются Revolut, Wise, N26 и польские банки
- Нет договора с банками вообще. Ни с одним. Договор только с провайдером
- Обычный REST API, а не HTML-формы
Архитектура интеграции с Montonio выглядит принципиально иначе:
sequenceDiagram
autonumber
participant U as Браузер
participant S as Сервер магазина
participant M as Montonio
participant B as Банк
S->>M: POST /api/orders<br/>тело = JWT, подписанный HS256 общим секретом
M-->>S: paymentUrl
S-->>U: редирект на paymentUrl
U->>M: страница выбора банка
M->>B: инициация платежа по PSD2 API
U->>B: подтверждение в банке
B-->>M: результат
par возврат клиента
M-->>U: редирект на returnUrl?order-token=JWT
U->>S: GET returnUrl
and вебхук
M->>S: POST notificationUrl<br/>{ orderToken: JWT }
end
S->>S: проверить подпись JWT<br/>и paymentStatus == PAID
Обратите внимание на симметрию с iPizza, несмотря на совершенно разный вид:
| iPizza | Montonio | |
|---|---|---|
| Инициация | HTML-форма через браузер | server-to-server POST |
| Формат сообщения | плоские VK_* поля |
JWT |
| Подпись | RSA, асимметричная | HS256, симметричный общий секрет |
| Ключи | сертификаты из банка вручную | access key и secret в личном кабинете |
| Уведомление в обход клиента | VK_AUTO=Y на тот же URL |
отдельный вебхук на notificationUrl |
| Что проверять | VK_MAC, сумму, VK_REF |
подпись JWT, paymentStatus, merchantReference |
Главный смысловой сдвиг: симметричная подпись вместо асимметричной. В iPizza банк не знает вашего приватного ключа, поэтому подписанное вами сообщение доказывает авторство. В JWT с HS256 обе стороны знают один секрет, поэтому это доказывает только то, что сообщение пришло от кого-то, кто знает секрет. Для схемы «мы уже доверяем друг другу по договору» этого достаточно, но секрет надо беречь так же, как приватный ключ.
Второй сдвиг: вебхук отдельным каналом вместо флага в общем сообщении. Это честнее: у вас два разных эндпоинта с разной семантикой, а не один эндпоинт, который должен догадываться по флагу, кто к нему пришёл.
Библиотеки по языкам
Здесь придётся сказать неприятную правду, но сначала уточнение по конкретным ссылкам, про которые меня спрашивали.
Shmarkus/Banklink это PHP, а не Node.js. Небольшая библиотека, около 5 звёзд, последняя активность в 2024. Если нужен PHP, лучше брать renekorss/Banklink: он живее, там 36 звёзд, обновлялся в 2025, поддерживает и старый, и новый iPizza, плюс шлюзы Nets Estonia и другие.
Voog/ipizza это Ruby, и это как раз хороший выбор. Обновлялся в 2025, это фактический стандарт для Rails-проектов в Эстонии.
Сводка:
| Язык | Что есть | Состояние | Вердикт |
|---|---|---|---|
| PHP | renekorss/Banklink | живая, обновлялась в 2025 | брать эту |
| PHP | Shmarkus/Banklink | 2024, маленькая | альтернатива |
| Ruby | Voog/ipizza | живая, обновлялась в 2025 | брать |
| Java | nortal/banklink | последний коммит в 2021 | использовать как референс |
| Node.js | tonistiigi/ipizza (npm ipizza) |
последняя публикация в 2014 | мёртвая |
| Go | ничего | - | нет вообще |
Для Node.js и Go живых библиотек под iPizza нет. Не «есть, но заброшенные», а именно нет. Единственное, что находится в npm по слову banklink, это либо неродственные проекты, либо пакет 2014 года.
Что с этим делать. Два варианта, и оба нормальные.
Вариант первый: написать самому. Это звучит страшнее, чем есть. Весь протокол это примерно 150 строк:
- собрать словарь
VK_*полей - склеить значения в порядке из спецификации с трёхзначным префиксом длины
crypto.sign/rsa.SignPKCS1v15этой строки- отрендерить форму
- на возврате повторить склейку и
crypto.verifyпубличным ключом банка
Никаких HTTP-клиентов, никакого стейта, никаких зависимостей кроме стандартной криптографии. В Go это crypto/rsa и crypto/sha512, в Node это встроенный crypto. По-хорошему, для iPizza библиотека почти и не нужна: она экономит вам таблицу порядка полей, а не сложную логику.
Обязательно возьмите порядок полей из актуальной спецификации, а не из чужого кода. LHV публикует её открыто, это самый удобный источник.
Вариант второй, и для новых проектов на Node или Go он честнее: не писать iPizza вообще. Взять Montonio или другого агрегатора, где интеграция это POST с JSON и проверка JWT, для чего библиотеки есть на любом языке. Вы теряете несколько центов с транзакции и приобретаете отсутствие криптографии в своём коде и отсутствие сертификатов в своей жизни.
Мой практический совет: если вы стоите перед выбором с нуля и у вас не миллионные обороты, не пишите iPizza. Единственная причина сегодня иметь дело с VK_MAC это унаследованная интеграция, которую нельзя выключить.
Как это тестировать
Раньше был публичный pangalink.net - эмулятор всех эстонских банков, где можно было прогнать полный цикл без договора. Сервис закрылся, но исходники остались и разворачиваются локально, а также живут в сторонних зеркалах. Для отладки своей реализации подписи это до сих пор лучший инструмент: он показывает, какую именно строку он ожидал получить перед хешированием, а это ровно то место, где всё ломается.
У облачных провайдеров с этим проще: у Montonio полноценный sandbox с отдельным набором ключей, доступный сразу после регистрации, до всякого одобрения бизнеса. Вебхуки локально тестируются через ngrok.
Что выбирать
Коротко, по ситуациям.
Новый магазин на готовой платформе (WooCommerce, Magento, PrestaShop, Shopify): берите готовый плагин агрегатора. Montonio, LHV и Maksekeskus все дают плагины под популярные платформы. Кода вы не пишете вообще.
Новый проект на своём коде, Node или Go: Montonio или другой агрегатор с REST API. Писать iPizza при отсутствии живых библиотек это добровольно взять на себя поддержку криптографии ради экономии центов.
Нужны не только эстонские банки (Польша, Финляндия, международные необанки): только PSD2-провайдер. iPizza это локальный протокол, за пределами Балтии его нет.
Большой оборот, есть команда, важна каждая доля процента: прямой iPizza или договор с банком-агрегатором вроде LHV. Но заведите мониторинг сроков сертификатов до того, как он вам понадобится.
Унаследованная работающая интеграция: не трогайте, но проверьте три вещи. Что подпись ответа действительно проверяется. Что сумма сверяется с заказом. Что вы знаете, когда истекают сертификаты.
Ссылки
- Техническая спецификация пангалинка от LHV - самый доступный первоисточник, с таблицами всех сервисов и описанием MAC008/MAC009
- Банковская ссылка LHV - условия агрегатора
- Bank Link SEB
- Документация Montonio - хороший пример того, как сегодня выглядит нормальный платёжный API
- Pangalink.net на GitHub - эмулятор для локальной отладки
- Генератор ключей от Zone.ee
- Моя старая статья 2007 года - как это выглядело во времена крон и Nordea