Как выставлять Kaspi-счета из 1С через API ApiPay?

Обновлено 6 июля 2026 · Решение проблем · Версия в Markdown
Содержание
  1. Сценарий: счета и фискализация позиций из 1С
  2. Маппинг данных: номенклатура и документы
  3. Выставление счёта: одиночное и пакетное
  4. Отслеживание оплаты: поллинг (основной путь) и вебхуки (опция)
  5. Возвраты
  6. Синхронизация каталога
  7. Чек-лист запуска: sandbox → прод
  8. Частые ошибки
  9. Частые вопросы

Сценарий: счета и фискализация позиций из 1С

1С остаётся системой учёта, а приём денег уходит в Kaspi без POS-терминала. Поток:

Документ 1С → POST /invoices (или bulk) → покупателю приходит push в Kaspi → покупатель оплатил → 1С узнаёт об оплате (поллинг/вебхук) → проводит платёж по external_order_id.

Если в чек Kaspi нужны позиции (фискализация, Нацкаталог ntin/gtin/barcode), номенклатура заранее заводится в каталог ApiPay, и счёт создаётся корзиной cart_items — механику каталога держит отдельная статья Каталог, корзина и Нацкаталог. Глубокое «почему у 1С нет готового модуля» — на витрине ApiPay для 1С; здесь — только как собрать интеграцию.

Одна поверхность — клиентский X-API-Key (/api/v1). Для платформ, которые онбордят чужих мерчантов, есть отдельный Partner API (X-Partner-Key) — см. документацию. Для интеграции своей 1С он не нужен.

Маппинг данных: номенклатура и документы

Это стержень 1С-интеграции. Две независимые связки, которые нельзя путать.

(а) Номенклатура 1С ↔ товар каталога ApiPay — через external_ref. Код (или GUID) номенклатуры 1С кладёте в external_ref при создании товара (POST /catalog). Потом точечно читаете GET /catalog?external_refs[]=… и получаете двусторонний матчинг «номенклатура ↔ товар каталога». Подробно — в статье про каталог.

(б) Документ 1С ↔ счёт — через два поля-ссылки. Их постоянно путают:

Поле Назначение Ограничения Поведение
external_order_id Свободная ссылка на документ 1С для матчинга оплаты. Возвращается в счёте и в вебхуке — по ней 1С находит документ и проводит платёж. ≤255 Не уникально, дублей не ловит
external_order_id_idempotency Ключ идемпотентности проведения: номер/GUID документа 1С. Повторное проведение того же документа не создаёт второй счёт. ≤191, пусто = выкл. Дубль → 409 duplicate_idempotency_key с телом { invoice_id, status } уже созданного счёта. Уникально в пределах организации

Рецепт для 1С: кладите идентификатор документа в оба поля — external_order_id для поиска документа по вебхуку/GET, external_order_id_idempotency для защиты от дубля при повторном проведении или сетевом ретрае. 409 — это не ошибка, а «счёт уже есть»: возьмите invoice_id/status из тела и продолжайте.

Важно: HTTP-заголовок Idempotency-Key — отдельный механизм, это не он. Идемпотентность счёта — через body-поле external_order_id_idempotency.

Выставление счёта: одиночное и пакетное

Одиночный — POST /invoices. Обязателен phone_number (8XXXXXXXXXX). Опционально: amount (min 0.01, max 99999999.99 — обязателен, если нет cart_items), description (≤500), external_order_id, external_order_id_idempotency, kaspi_connection_id (выбор кассира), cart_items (для каталог-организации), discount_percentage (1–99).

curl -X POST https://api.apipay.kz/api/v1/invoices \
  -H "X-API-Key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
  -d '{
    "phone_number": "8XXXXXXXXXX",
    "amount": 15000,
    "description": "Оплата по счёту №123 от 01.07.2026",
    "external_order_id": "1c-doc-000000123",
    "external_order_id_idempotency": "1c-doc-000000123"
  }'

Псевдокод из 1С (HTTP-сервис / внешняя обработка): собираете тело, шлёте POST, из ответа 201 сохраняете id в реквизит документа.

// 1С (условный BSL)
Запрос = Новый HTTPЗапрос("/api/v1/invoices");
Запрос.Заголовки.Вставить("X-API-Key", КлючAPI);
Запрос.Заголовки.Вставить("Content-Type", "application/json");
Запрос.УстановитьТелоИзСтроки(ЗаписатьJSON(ПараметрыСчёта)); // с external_order_id = НомерДокумента
Ответ = HTTPСоединение.ОтправитьДляОбработки(Запрос);
// 201 → сохранить invoice_id у документа; 409 → счёт уже есть, взять invoice_id из тела

Пакетный — POST /invoices/bulk (отдельный лимит 20/min). Тело: invoices (массив 1–100), опц. общий kaspi_connection_id (один кассир на батч). Ответ 201: { created, duplicates, failed, invoices: [ { index, status: "created"|"duplicate"|"failed", … } ] } — результат по каждому элементу по индексу. Структурная ошибка тела (например элемент без phone_number) отклоняет весь батч атомарно (422, частичного применения нет).

Рецепт: ночная выгрузка пачки неоплаченных документов → один bulk (≤100) → разобрать invoices[] по index, сохранить id у каждого документа. duplicate = документ уже выставлялся (сработал external_order_id_idempotency).

Отслеживание оплаты: поллинг (основной путь) и вебхуки (опция)

У 1С часто нет публичного веб-адреса (крутится в локальной сети / на терминальном сервере), поэтому основной путь — поллинг.

Поллинг GET /invoices/{id} — лимит 1000/min (заменяет общий 200/min именно под 1С). Читает из БД/кэша, не бьёт в Kaspi; терминальные счета кэшируются 24 часа. Ответ 200 — полный объект счёта + items[]. Пока счёт в processing, в объекте видны last_kaspi_error_code/last_kaspi_error_message — диагностика без ожидания вебхука.

Пакетная проверка POST /invoices/status/check { invoice_ids: [...] } — опросить сразу все неоплаченные счета одним запросом; ответ { invoices: [ { id, status, kaspi_invoice_id, amount, error_message, updated_at } ] }.

Паттерн регламентного задания 1С:

1. Выбрать документы с неоплаченными счетами (свои invoice_id).
2. POST /invoices/status/check со всеми invoice_ids (или поштучно GET /invoices/{id}).
3. Для каждого терминального статуса:
     paid                      → провести оплату по external_order_id;
     cancelled/expired/error   → снять резерв / пометить неоплаченным.
4. Реагировать на ПОСЛЕДНИЙ статус.

Легитимны переходы cancelled → paid и expired → paid (клиент оплатил в последний момент — «деньги получены»). error → paid невозможен; error → pending — реконсиляция (счёт на самом деле прошёл, следуйте последнему статусу).

Вебхуки — опция. Если 1С опубликована в интернет (веб-сервер публикации, обратный прокси), можно принимать paid-вебхук и проводить оплату мгновенно; иначе поллинг самодостаточен. Подпись — X-Webhook-Signature: sha256=<hex>, HMAC-SHA256 по сырому телу; отвечать 200 за ≤5 с, дедуп по (invoice.id, invoice.status). Полная настройка приёмника, ретраи и circuit breaker — в статье Вебхуки ApiPay: настройка и проверка подписи; здесь не дублируем.

Возвраты

POST /invoices/{id}/refund: по умолчанию — полный возврат; частичный — по сумме amount или позиционный return_items[] (1–100; на позицию catalog_item_id + ровно одно из count или amount, не превышая доступное). Ответ 201{ message, refund, invoice: { id, total_refunded, available_for_refund, pending_refund_amount } }; результат приходит асинхронно (вебхук/поллинг, completed/failed+error_code).

Важно: статуса refunded у счёта нет. Полный возврат оставляет счёт paid + is_fully_refunded=true; первый частичный переводит paid → partially_refunded. Детали и причины отказов — Возвраты Kaspi через API.

Синхронизация каталога

Нужна, только если счёт должен нести позиции в чек Kaspi. Создание батчем POST /catalog (≤100, external_ref = код номенклатуры 1С, при маркировке — ntin/gtin); в рабочем режиме создание асинхронное (202), поэтому статус подтверждаете точечным чтением GET /catalog?external_refs[]=…. Инкрементальная выгрузка изменённого — GET /catalog?updated_after=<iso> (включая удалённые), keyset-экспорт больших каталогов — ?cursor=. Важно: при обычном PATCH не передавайте ntin/gtin — затрёте идентичность Нацкаталога. Полная механика каталога, скан штрихкода и 4 режима чтения — в статье Каталог, корзина и Нацкаталог.

Чек-лист запуска: sandbox → прод

Сначала прогон в песочнице (детерминированные симуляции без реального Kaspi), потом рабочий режим. Что песочница даёт и что меняется при переходе — в статье Песочница и рабочий режим.

  • Симуляции жизненного цикла: POST /invoices/{id}/simulate-status { status: paid|cancelled|expired|error } на sandbox-счёте — прогнать все ветки проведения.
  • Магические номера lookup (POST /clients/check): 87770000001 → есть Kaspi ("Иван И."), 87770000002 → нет Kaspi.
  • Проверка доставки вебхуков (если используете): GET /webhook-logs?invoice_id=….
  • Идемпотентность проведения: один и тот же документ, проведённый дважды, не создаёт второй счёт (409).
  • Обработка 429/Retry-After: уважайте заголовок, снижайте темп.
  • Тайм-зоны: счета/возвраты/каталог — UTC +00:00; только GET /tariff и GET /account/health+05:00. Частый баг 1С — сдвиг времени проведения.
  • Матчинг: проведение строго по external_order_id; дедуп проводки по invoice.id + status.

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

  • Путать external_order_id и external_order_id_idempotency. Первый — метка для поиска документа, второй — ключ от дублей (409). Обычно оба равны номеру документа.
  • Пересоздавать счёт в processing. Система ретраит сама — получите два живых счёта. Ждите терминальный статус, смотрите last_kaspi_error_*.
  • Парсить сумму как число. В ответах суммы — строки ("15000.00").
  • Сдвиг времени проведения. Даты счетов — UTC; переводите в Asia/Almaty на стороне 1С.
  • Ждать paid после error. error → paid невозможен; реагируйте на последний статус.

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

Есть готовый модуль/обработка для 1С?

Нет. Это доработка конфигурации силами вашего 1С-специалиста: HTTP-запросы из 1С к REST ApiPay. Почему готового модуля нет — на витрине ApiPay для 1С.

Как 1С без публичного адреса узнаёт об оплате?

Поллингом. GET /invoices/{id} имеет лимит 1000/min специально под 1С и читает из кэша, не нагружая Kaspi. Вебхуки — опция, если 1С опубликована в интернет.

Что вернётся при повторном проведении того же документа?

409 duplicate_idempotency_key с invoice_id и статусом уже созданного счёта — если вы передали external_order_id_idempotency. Это норма при ретрае: работайте с существующим счётом.

Клиент оплатил после отмены — как это выглядит?

Статус может пройти cancelled → paid (или expired → paid). Реагируйте на последний статус — деньги получены, проводите оплату.

Почему счёт долго висит в processing?

При троттлинге Kaspi это легитимно; система сама ретраит. Не пересоздавайте — смотрите last_kaspi_error_code/last_kaspi_error_message в GET /invoices/{id}.

Как принять в чек позиции номенклатуры?

Через каталог: завести номенклатуру (POST /catalog, external_ref = код 1С) и выставлять счёт корзиной cart_items. Механика — в статье Каталог, корзина и Нацкаталог.

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

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

Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он подключит приём платежей примерно за 15 минут. Настраивает ИИ, а не вы.

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