Сценарий: счета и фискализация позиций из 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. Механика — в статье Каталог, корзина и Нацкаталог.