Как платформе вроде МоегоСклада интегрироваться с ApiPay?

Обновлено 15 августа 2026 · Для вашего бизнеса · Версия в Markdown
Содержание
  1. Три модели, и это разные интеграции
  2. Границы ответственности
  3. Шаг 1. Организация клиента и приём Kaspi
  4. Шаг 2. Секрет подписи входящих
  5. Шаг 3. Синхронизация состояния
  6. Шаг 4. Журнал — чтобы не писать в поддержку
  7. Шаг 5. Состояние клиентов одним запросом
  8. Шаг 6. Анкета клиента
  9. Песочница
  10. Вебхуки
  11. Частые ошибки
  12. Частые вопросы

Три модели, и это разные интеграции

ApiPay сосуществует в трёх режимах, и первое, что стоит сделать, — определить свой.

Модель Кто платит ApiPay Признак «это ваш случай»
Реферальная Клиент сам Вы приводите клиентов ссылкой и не управляете их аккаунтами
Операционная Клиент, но счета выставляете вы Вы ведёте онбординг, но подписку клиент оплачивает сам
Договорная (эта статья) Платформа по договору Клиент платит вам, у вас свой биллинг, ApiPay остаётся на стороне платформы

В договорной модели вам включают режим назначения тарифа: тариф клиенту вы не оплачиваете поштучно, а объявляете. Включает его ApiPay по договору — самостоятельной ручки нет. Проверить, включён ли режим, можно в GET /api/partner/health: account.tariff_billing_mode должен быть assignment.

Границы ответственности

  • Ваша сторона: кто из клиентов подписан, до какой даты, на каком плане; продление, приостановка, возвраты, любые скидки и промо.
  • Наша сторона: работает ли приём Kaspi у клиента, каков его суточный лимит счетов, одобрена ли анкета, доставлены ли вебхуки.

⚠️ Суточный лимит и название тарифа мы берём из договорных условий, а не из вашего запроса. Прислали своё значение — вернём его в блоке ignored рядом с тем, что реально применено. Так расхождение видно сразу, а не выясняется из жалобы клиента на лимит.

Шаг 1. Организация клиента и приём Kaspi

Заведение организации, подключение кассира по SMS-коду и выпуск per-org ключа описаны отдельно — повторять не будем: «Полностью автоматический приём Kaspi для клиентов» и «Подключение организации к партнёру».

Здесь важно одно: external_id — ваш идентификатор клиента в вашей системе. Он же ключ идемпотентности: повторный POST с тем же external_id вернёт ту же организацию, а не создаст вторую.

Шаг 2. Секрет подписи входящих

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

Секрет выдаёт ApiPay по запросу, показывается он один раз. Проверить, что дверь открыта и что ваше окружение подписывает тем же секретом:

curl https://api.apipay.kz/api/partner/health -H "X-Partner-Key: $KEY"
"account": {
  "tariff_billing_mode": "assignment",
  "inbound_sync": {"enabled": true, "secret_configured": true,
                   "secret_hint": "••••a1b2", "accepting": true}
}

Смотрите на accepting — это и есть «дверь открыта». secret_hint — последние 4 символа выданного секрета: удобно сверить, тем ли подписывает стенд и тем ли прод.

⚠️ Проверяйте состояние здесь, а не боевым запросом: при закрытой двери запрос отвечает 401/403, и отличить «не тот секрет» от «приём выключен» по ответу нельзя.

Шаг 3. Синхронизация состояния

SECRET='<inbound_secret>'
ORG=829
PATH_="/api/partner/organizations/$ORG/subscription"
BODY='{"state":"active","tier":"business","paid_through":"2026-09-12T18:50:12+05:00","cause":"autoprolongation"}'
TS=$(date +%s)
SIG=$(printf '%s.PUT.%s.%s' "$TS" "$PATH_" "$BODY" \
      | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)

curl -X PUT "https://api.apipay.kz$PATH_" \
  -H "X-Partner-Key: $KEY" \
  -H "X-ApiPay-Timestamp: $TS" \
  -H "X-ApiPay-Signature: sha256=$SIG" \
  -H 'Content-Type: application/json' \
  --data-raw "$BODY"

Подписывается строка из четырёх частей через точку: метка времени, метод, путь и сырое тело.

⚠️ Перевыдача секрета вступает в силу сразу: старая подпись перестаёт приниматься в тот же момент. Плановую смену согласовывайте заранее и меняйте значение в окружении одновременно.

Метод и путь входят в подпись обязательно. Организация приезжает в пути, и подпись только по телу его не покрывает — подписывайте все четыре части.

Тело подписывается сырым. Отправляйте ровно ту строку, которую подписали: --data-raw "$BODY", а не пересборку JSON библиотекой и не «красивое» форматирование перед отправкой. Одна лишняя пробельная позиция — 401 invalid_signature, и выглядит это невоспроизводимой ошибкой «на стенде работает, в проде нет».

Почему состояние, а не события

У вашей платформы на входе уже лежит состояние: Vendor API МоегоСклада присылает PUT со статусом и причиной, а не ленту событий. Событийный приём означал бы, что вы кодируете состояние в события, а мы собираем его обратно, — с потерями на каждом шаге.

Декларативная форма даёт даром то, что в событийной строится механизмами:

  • повтор безопасен — тот же PUT дважды не удлиняет подписку;
  • пропущенная доставка чинится следующей — очередь ретраев не нужна;
  • порядок доставки не важен — побеждает то, что вы прислали последним;
  • ключ идемпотентности не нужен — повтор состояния ни на чём не «залипает».

Что происходит от того, что вы прислали

Прислали Что произошло Ответ
state: active, дата дальше текущей Срок продлён, тариф выставлен 200, result: applied
state: active, та же дата Ничего — состояние уже такое 200, result: unchanged
state: active, дата раньше текущей Ничего: срок не укорачивается 200, result: unchanged
state: active, тир ниже текущего Отказ — понижает человек 409 tier_downgrade_not_allowed
state: suspended / cancelled Записано в журнал, тариф не тронут 200, result: noted

Отзыва тарифа нет — это решение, а не пробел. Отключение живого мерчанта от приёма денег мы не делаем по машинному запросу. Выданный период истечёт сам, поэтому синхронизируйте помесячно: тогда приостановка у вас становится приостановкой у нас максимум через месяц, без единого отдельного вызова.

Ответ 200 всегда показывает, что реально действует:

{ "success": true, "sync_id": 8123, "result": "applied",
  "organization_id": 829, "external_id": "crm-client-42",
  "applied": { "tier": "business", "tier_label": "Бизнес", "daily_limit": 100,
               "limits_source": "partner_grid",
               "expires_at": "2026-09-12T23:50:12+05:00", "status": "active" },
  "ignored": { "daily_limit": { "sent": 999, "applied": 100 } } }

Шаг 4. Журнал — чтобы не писать в поддержку

Каждый вызов, включая отказы, попадает в журнал:

curl "https://api.apipay.kz/api/partner/subscription-syncs?result=rejected&from=2026-08-01" \
  -H "X-Partner-Key: $KEY"

Фильтры: organization_id, result, state, окно from/to по времени приёма. Карточка одной записи (/subscription-syncs/{id}) дополнительно отдаёт исходное тело — видно, что именно ушло с вашей стороны.

⚠️ Неизвестное значение фильтра — 422 с именем поля, а не пустая страница: молчаливый ноль неотличим от «мы вам ничего не присылали».

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

Списки и агрегаты устроены так, чтобы не опрашивать каждого клиента по отдельности:

  • GET /api/partner/healthorganizations.kyc — сколько клиентов в каком состоянии анкеты;
  • GET /api/partner/organizations?kyc_status=required,needs_changes — кто именно;
  • карточка организации несёт kyc_status и блок tariff (tier, tier_label, daily_limit, limits_source) — по какому тарифу и с каким суточным лимитом клиент работает прямо сейчас.

Сумм в карточке нет: цена — в GET /api/partner/organizations/{id}/tariff.

Шаг 6. Анкета клиента

До одобрения анкеты у клиента действует суточный потолок счетов — это и есть барьер, о который спотыкаются интеграторы. Заполнить анкету за клиента нельзя: в ней есть юридическое подтверждение о неторговле запрещённым, а даёт его тот, у кого факты. Вы выдаёте клиенту ссылку, он заполняет анкету сам — подробно в статье «KYC клиента через Partner API».

Песочница

Онбординг клиента целиком (организация → кассир → счёт → вебхук → возврат) проходится в песочнице на мок-контуре: ни одного реального вызова Kaspi и ни одной SMS.

⚠️ Синхронизация подписки и выдача ссылки на анкету — исключение: это денежные двери, и на тестовой организации они отвечают 409 test_organization. Отлаживайте их на одной боевой организации — она доступна после перевода партнёра в production, до этого боевые вызовы отбиваются 403 production_access_required. Подпись, коды отказов и журнал работают на ней ровно так же, а state: suspended тариф не трогает вовсе. Проверить саму подпись, не задевая тариф, дешевле всего заведомо неверным телом: 422 в ответе означает, что подпись уже сошлась.

Вебхуки

На ваш webhook_url приходят события по клиентам: tariff.activated (клиент оплатил тариф по вашему счёту), kyc.status_changed (решение по анкете), invoice.status_changed и invoice.refunded (движение по счетам клиента).

Секрет вебхуков — другой, не тот, которым вы подписываете входящую синхронизацию. Исходящие подписываем мы (X-Webhook-Signature), входящие подписываете вы (X-ApiPay-Signature). Это две разные роли и два разных ключа; путаница между ними — самая частая причина «подпись не сходится».

Детали проверки подписи — «Настройка вебхуков».

Частые ошибки

Ответ Что не так Что делать
401 invalid_signature Тело пересобрано перед отправкой либо не тот секрет Отправлять подписанную строку байт-в-байт; сверить secret_hint в /health
401 signature_expired Разъехались часы отправителя Синхронизировать время на сервере, повторить с новой меткой
401 inbound_not_configured Секрет ещё не выдан Запросить у ApiPay; состояние видно в /health
403 inbound_sync_disabled Приём выключен тумблером Подпись верна — вопрос к ApiPay, не к коду
409 test_organization Пробуете на тестовой организации Отлаживать на боевой (см. «Песочница»)
409 claimed_paying_organization Клиент платит ApiPay сам Перевод на договорные условия оформляет ApiPay
422 subscription_terms_required При state: active нет tier или paid_through Дослать оба поля
413 payload_too_large Слишком большое тело Не класть в partner_context всё подряд
Полный перечень кодов — на странице /errors, партнёрская дока — /partners.

    Частые вопросы

    Интеграция с ИИ

    Не настраивайте вручную — поручите ИИ

    Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он настроит интеграцию за вас: получит доступы, создаст тестовый счёт в песочнице и подключит вебхуки. Техническую часть делает ИИ, а не вы.

    Для разработчика. Полная спецификация API — apipay.kz/docs. Эта статья в Markdown — /guides/integraciya-apipay-s-moyskladom.md. Справочник фактов для ИИ-агента — apipay.kz/llms.txt.