Перейти к содержанию

Changelog — Merchant API v1

Журнал изменений контракта api/openapi.yaml. Правится только здесь; в портал документации (docs/portal/docs/changelog.md) файл копируется скриптом docs/portal/build.sh.

Формат записи: заголовок ## ГГГГ-ММ-ДД — <версия> и список изменений по группам. Версия совпадает с info.version в api/openapi.yaml. Суффикс -draft означает, что контракт ещё не заморожен (заморозка — критерий выхода из этапа M0); после заморозки любое изменение вносится только через ADR (см. api/README.md).

Группы: Добавлено — совместимое расширение (новое поле, значение enum, операция); Изменено — несовместимое изменение (допустимо только в черновике; в /v1 после заморозки запрещено); Исправлено — описания, примеры, опечатки без изменения поведения; Устарело — элемент помечен deprecated, продолжает работать.

Клиенты обязаны игнорировать неизвестные поля ответов и неизвестные значения справочников (FR-API-011): именно это позволяет расширять контракт без смены версии.

2026-09-14 — 1.0.0

Правки описаний без изменения схем: добавленные ограничения return_url и оговорка про одноразовость confirmation_url. Формы полей, коды ответов и обязательность не менялись, поэтому ADR не требуется (api/README.md п. 3) и версия остаётся прежней.

Добавлено

  • ConfirmationRequestRedirect.return_url и ConfirmationRequestQr.return_url: в description перечислены ограничения, которые касса уже применяет на входе, — без учётных данных в адресе, без пробелов и управляющих символов, только публичный адрес (непубличные диапазоны и внутренние имена отклоняются). Отказ — 400 invalid_request с parameter: confirmation.return_url. Ограничение новое: такие адреса удовлетворяли опубликованной схеме (^https://, format: uri, maxLength: 2048) и до сих пор принимались. Причина — адрес выдаётся не только мерчанту в объекте платежа, но и браузеру плательщика: по нему рисуется кнопка возврата в магазин на странице результата KASSA (FR-CHK-005), то есть произвольная навигация с домена pay.{domain}. Адрес приводится к канонической форме до проверки, поэтому нестандартная запись IPv4 проверяется по тому, во что она разворачивается: https://2130706433/ — это 127.0.0.1 и будет отклонён, а нестандартная запись публичного адреса принимается и сохраняется канонической. В тестовом контуре (ключ test_) сняты все ограничения по досягаемости адреса — непубличные диапазоны, внутренние имена и любые записи локальных адресов: https://localhost:3000/return принимается, мерчант на этапе интеграции разрабатывает локально (FR-API-012). Требования https, отсутствия учётных данных и отсутствия управляющих символов не снимаются ни в одном контуре.
  • PaymentLinkCreate.return_url: то же правило объявлено, но ещё не применяется — операции платёжных ссылок отвечают 501 not_implemented до этапа M5, и проверка появится вместе с ними. Отказ там назовёт parameter: return_url, а не confirmation.return_url: в теле этой операции объекта confirmation нет, return_url — поле верхнего уровня. Поведение операции этой записью не меняется.
  • Confirmation.confirmation_url: сказано прямо, что адрес возвращается только в ответе на создание платежа. Он содержит одноразовый токен страницы оплаты, в базе кассы лежит лишь его хеш, и восстановить адрес последующим GET /payments/{payment_id} невозможно. Поведение не менялось — контракт о нём молчал.

Исправлено

  • ^https:// у return_url начал соблюдаться. Паттерн объявлен записью от 2026-09-10 (п. 5), но генератор клиента теряет его при format: uri, и правило держало только ограничение базы: javascript:, data: и http:// доходили до COMMIT и давали 500 internal_error вместо 400. Теперь — 400 invalid_request с parameter, как и обещает справочник ошибок. Схема не менялась.

2026-09-10 — 1.0.0

Заморозка состава Merchant API v1 под модель A «касса — получатель платежа» (ADR-0006, ADR-0007, CORRECTION-001-merchant-of-record.md). Пакет из 35 правок сводит воедино TZ FR-API-014 п. 1–15 и §12.2–12.4, CORRECTION-001 §4, финальное ревью контракта и 20 ошибок линтера. Дальнейшие изменения /v1 — только совместимые и только новой записью в этом файле.

Добавлено

  • Схема SignedAmount — сумма с допустимым ведущим минусом; применяется только в ответах Balance.available и Settlement.net_amount (отрицательный баланс — штатное состояние модели A). Balance.payable остаётся неотрицательным Amount.
  • Общая схема Metadata (до 16 ключей, ключ ≤ 64 символов — MetadataKey, значение ≤ 512 символов — MetadataValue) вместо четырёх расходящихся описаний в PaymentCreate, RefundCreate, PaymentLinkCreate, PayoutCreate.
  • Пять кодов ошибок TZ §12.4: limits_exceeded (422, портфельные лимиты), insufficient_merchant_balance (422), payments_suspended (422), evidence_deadline_passed (409), not_implemented (501). Граница с amount_limit (только пер-транзакционные min/max) описана явно; одноимённое cancellation_details.reason = limits_exceeded — другой справочник.
  • Полный состав баланса модели A: Balance.deposit, pending_refunds, disputed, test; все поля обязательны; зафиксировано, что available уже уменьшен на резерв, возвраты в пути и удержания по спорам.
  • Схемы SettlementStatus и FrozenReason — состояние расчётов мерчанта: payouts_frozen, frozen_reason, frozen_at, unfrozen_at, settlement_delay_days, next_settlement_at. Тот же фрагмент возвращается в Shop.settlement.status и является объектом событий settlement.frozen / settlement.unfrozen.
  • Поля магазина: receipt_mode (agent / merchant_issues / none), settlement.reserve — политика rolling reserve массивом по валютам (currency, percent_bps, days), settlement.status; settlement_model стал обязательным и readOnly; у Shop.id появился паттерн ^sh_[0-9A-Z]{26}$.
  • Разделы расчёта: reserve_held_amount, reserve_released_amount, deposit_withheld_amount, bank_fines_amount, chargeback_fees_amount, disputes_amount, settlement_delay_days, test; границы периода period_from / period_to объявлены включительными.
  • Объект спора: схемы Dispute, DisputeKind, DisputeStatus, DisputeEvidence — раньше события dispute.* были объявлены, а объекта в контракте не было.
  • Операции споров (x-stage: M2, до этапа отвечают 501 not_implemented): GET /disputes с фильтрами payment_id, status, kind, created_at_gte, created_at_lt; GET /disputes/{dispute_id}; двухшаговая передача файлов POST /disputes/{dispute_id}/evidence/uploads (схемы DisputeEvidenceUploadCreate, DisputeEvidenceUpload) и POST /disputes/{dispute_id}/evidence (схема DisputeEvidenceCreate); тег disputes; параметр пути DisputeId; лимиты доказательств TZ §12.3 (PDF/JPEG/PNG, 10 МБ, 10 файлов, комментарий ≤ 4096).
  • Три типа событий TZ §12.2: dispute.evidence_required, settlement.frozen, settlement.unfrozen.
  • Схема ответа WebhookEndpointAfterUpdate для PATCH /webhook-endpoints/{endpoint_id}: при rotate_secret: true новый secret возвращается один раз — до этого ротация была неосуществима по контракту.
  • Поля платежа: merchant_display_name (обязательное, только чтение), statement_descriptor (только чтение), charged_back_amount, pending_refunds_amount, disputed_amount, fee_refunded_amount и производное refundable_amount — именно оно ограничивает сумму возврата.
  • Возврат объектами того, что принимается на входе: PaymentLink получил min_amount, max_amount, order_id, capture, return_url, metadata, test; Payoutorder_id, metadata, test; Refundtest.
  • Ответ 200 на повтор идемпотентного запроса у всех POST (/refunds, /payment-links, /webhook-endpoints, /payouts, /events/{event_id}/resend, /test/payments/{payment_id}/simulate, обе операции доказательств); для операций, чей первый ответ и так 200, правило записано в description.
  • Фильтры расчётов period_from, period_to, currency, status; фильтры событий created_at_gte, created_at_lt.
  • Пары «HTTP + код ошибки» в описаниях POST /payments, POST /refunds, GET /balance, GET /settlements и всех операций споров.
  • Паттерны идентификаторов там, где были свободные строки: Refund.payment_id, PaymentLink.payments[], Shop.id, все пути споров.
  • Refund.order_id (необязательное, readOnly, maxLength: 128) — по итогам ревью контракта. Касса копирует значение из Payment.order_id возвращаемого платежа; в запросе поле не принимается. До этого Refund был единственным объектом без идентификатора заказа (он есть у Payment, PaymentLink, Payout, Dispute), и обработчик, написанный по гайду «сопоставляйте с заказом по data.object.order_id» (docs/portal/docs/webhooks.md §4 п. 4), падал на первом же событии refund.succeeded / refund.canceled. Совместимое добавление; ожидание рынка — docs/01-competitor-api-analysis.md §10 п. 4.

Изменено

Три исправления черновика, разрешённые TZ FR-API-014 (после этой записи состав /v1 заморожен):

  1. Balance.available и Settlement.net_amount переведены с Amount на SignedAmount (FR-API-014 п. 2): беззнаковый паттерн не выражал долг мерчанта перед кассой.
  2. Из PaymentCreate удалено поле statement_descriptor (FR-API-014 п. 8, FR-PAY-011): дескриптор формирует касса и возвращает в Payment.statement_descriptor (только чтение); переданное мерчантом значение молча игнорировалось.
  3. Из Receipt удалено поле send_by_kassa (FR-API-014 п. 9, FR-RCP-002): режим фискализации — настройка магазина Shop.receipt_mode, а не поле запроса; одним полем запроса отключался обязательный модуль.

Два сужения типа без изменения поведения (называются явно, чтобы changelog и TZ FR-API-014 не разошлись):

  1. Error.code переведён из свободной строки с прозаическим справочником в закрытый enum ErrorCode; состав кодов расширен, ни один прежний код не удалён и не переименован.
  2. У исходящих URL мерчанта (WebhookEndpointCreate.url, WebhookEndpointUpdate.url, ConfirmationRequest.return_url для redirect и qr, PaymentLinkCreate.return_url) добавлены pattern: '^https://' и maxLength: 2048 — правило «только https» перенесено из прозы в схему.

Кроме того: Event.data получил обязательный object_type и дискриминатор (EventData, EventDataPayment, EventDataRefund, EventDataPaymentLink, EventDataPayout, EventDataSettlement, EventDataSettlementStatus, EventDataDispute) — тип объекта больше не разбирается по префиксу id; next_cursor внесён в required всех списочных ответов (клиент отличает «конец списка» от «поле не отдано»); Event.test, Payout.test, Refund.test, Settlement.test, Balance.test стали обязательными; info.version1.0.0 (снят суффикс -draft).

Три отклонения от CORRECTION-001 §4, принятые сознательно: доля резерва задана в базисных пунктах (percent_bps, а не percent — CLAUDE.md запрещает проценты дробным числом); удержанная сумма резерва живёт в Balance.reserve и Balance.deposit, а не в политике магазина (held_amount в Shop не вводится — политика описывает правило, баланс описывает деньги); политика резерва — массив по валютам, потому что каскад и реестр выплат работают строго в пределах одной валюты.

Исправлено

  • Пять описаний во flow-mapping, где незакавыченная запятая создавала ключ-мусор внутри схемы (ошибка struct в redocly): Customer.ip, PaymentCreate.statement_descriptor, RefundCreate.receipt, Settlement.report_url, PayoutCreate.destination.card_token; заодно закавычены описания Customer.tax_id, PaymentMethodData.saved/id, CancellationDetails.provider_code, Confirmation.confirmation_url/confirmation_data, WebhookEndpointUpdate.rotate_secret, WebhookEndpointWithSecret.secret, PaymentLinkCreate.amount/reusable.
  • Одиннадцать операций без summary: GET /payment-links/{link_id}, POST /payment-links/{link_id}/deactivate, GET /events/{event_id}, POST /webhook-endpoints, GET /webhook-endpoints, GET|PATCH|DELETE /webhook-endpoints/{endpoint_id}, GET /settlements/{settlement_id}, GET /payouts, GET /payouts/{payout_id}.
  • Вебхукам добавлены operationId (deliverEvent, authorizeRequest) и тег events; снято наследование глобального security: [basicAuth] — доставка вебхука не требует Basic-аутентификации мерчанта. Ключ раздела authorize_request переименован в authorize-request (kebab-case, требование paths-kebab-case); тип события остаётся payment.authorize_request — имя ключа в документе на провод не выходит.
  • В описание доставки вебхука внесено: подпись считается по сырым байтам тела; в период ротации секрета заголовок несёт две подписи v1, достаточно совпадения любой; тип объекта разбирается по data.object_type.
  • GET /balance больше не называется «балансы магазина» и не обещает пустой список в технологической модели: значения — уровня мерчанта, модель по умолчанию — agent.
  • Customer.ip и PaymentCreate.client_ip ограничены 45 символами; ReceiptItem.supplier.* — длинами полей, а описание приведено к режиму Shop.receipt_mode = agent.
  • Все инлайновые объекты и перечисления вынесены в именованные схемы components/schemas (по итогам ревью контракта). Формат JSON не изменился ни в одном месте: правится только структура документа, поэтому запись здесь, а не в группе «Изменено». Проверено сравнением полностью раскрытых (dereferenced) схем всех операций до и после правки — единственное отличие раскрытых схем — новое поле Refund.order_id.

    Причина: datamodel-codegen даёт инлайновым схемам имена по имени поля и разводит совпадения числовым суффиксом. Инлайновый Shop.settlement занимал имя Settlement, а настоящая схема расчёта становилась SettlementModel1 — рядом с SettlementModel (перечисление agent/technology) и SettlementStatus (состояние заморозки); Receipt.customer занимал имя Customer1 при существующей схеме Customer; PaymentLink.payments[] давал Payment1; семь инлайновых перечислений status превращались в StatusStatus6. Код M1–M5 импортировал бы SettlementModel1, имея в виду выписку. TS-клиента дефект не касался (openapi-typescript сохраняет имена компонентов) — он был виден только в Python.

    Новые схемы: SettlementModel, ReceiptMode, ReservePolicy, ShopSettlement, ShopCapabilities, Locale, ConfirmationRequestRedirect, ConfirmationRequestQr, ConfirmationRequestEmbedded, ConfirmationType, CardBrand, CardDetails, SbpDetails, CancellationParty, CancellationReason, ReceiptPaymentSubject, ReceiptPaymentMode, ReceiptItemSupplier, ReceiptCustomer, ReceiptState, PaymentMethodRequest, ThreeDSecureDetails, AuthorizationDetails, PaymentId, PaymentLinkStatus, EventDeliveryStatus, EventDelivery, WebhookEndpointDesiredStatus, WebhookEndpointStatus, SettlementState, EvidenceContentType, EvidenceFileId, PayoutDestinationCardToken, PayoutDestinationSbp, PayoutDestinationBankAccount, PayoutDestination, PayoutStatus. Заодно устранены четыре дубля: тип файла доказательства (EvidenceContentType) и SettlementState (в схеме Settlement и в фильтре GET /settlements) описаны по одному разу.

    PaymentId и EvidenceFileId применяются только там, где инлайновый тип породил бы безымянную модель, — в элементах массивов и в параметре пути payment_id; в скалярных полях объектов паттерн остаётся записанным на месте, чтобы генератор давал str, а не обёртку RootModel.

2026-09-10 — 1.0.0-draft

Первая публикация черновика контракта Merchant API v1 (ADR-0003 «Стиль публичного Merchant API»).

Добавлено

  • Аутентификация HTTP Basic shop_id:secret_key; ключи test_/live_ с регионом (test_ru_…, live_kz_…); секрет показывается один раз.
  • Заголовок Idempotency-Key (алиас Idempotence-Key) на всех POST и PATCH; срок действия 24 часа; конфликты 409 idempotency_conflict и 409 idempotency_in_progress.
  • Ресурсы: /shop; /payments (создание, список, чтение, capture, cancel); /refunds; /payment-links (создание, список, чтение, deactivate); /payment-methods; /events (список, чтение, resend); /webhook-endpoints (создание, список, чтение, изменение с ротацией секрета, удаление); /settlements; /balance; /payouts (x-stage: M5); /test/payments/{payment_id}/simulate (только тестовые ключи).
  • Объект Amount {value, currency} — строка с двумя знаками после точки; валюты RUB, KZT, USD, EUR.
  • Статусы платежа pending → waiting_for_capture → succeeded | canceled, возврата pending → succeeded | canceled; справочник cancellation_details.reason (TZ §12.1).
  • Способы оплаты bank_card, sbp, kaspi, mir_pay, sber_pay, t_pay, apple_pay, google_pay, installments; в ответе — только маскированные данные карты (first6, last4, brand).
  • Подтверждение оплаты redirect (чекаут KASSA), qr, embedded (M5); в ответе — confirmation_url или confirmation_data.
  • Чеки: объект Receipt с профилями ru (ФФД 1.2) и kz; receipt_status в платеже и возврате.
  • События (TZ §12.2) и вебхуки: заголовки X-Kassa-Event-Id, X-Kassa-Signature: t=<unix>,v1=<hmac_sha256>; окно проверки 300 с; повторы 1 м, 5 м, 15 м, 1 ч, 4 ч, 12 ч, 24 ч, 48 ч, 72 ч; синхронный вебхук payment.authorize_request.
  • Единый формат ошибок Error {type, code, description, parameter?, request_id} и справочник кодов в Error.code.
  • Курсорная пагинация (limit 1–100, по умолчанию 20; cursor; next_cursor); заголовок X-Request-Id в каждом ответе; временные метки ISO 8601 в UTC с суффиксом Z.