{
  "openapi": "3.1.0",
  "info": {
    "title": "AgentPay API",
    "version": "2026-09-07",
    "description": "REST API для приёма платежей через Kaspi: счета с push-уведомлением, платёжные ссылки без телефона, журнал событий и подписанные вебхуки. Ключи `sk_test_*` включают песочницу — Kaspi не вызывается, деньги не списываются. Человекочитаемая документация: https://agentpay.kz/docs, она же в markdown для AI-агентов: https://agentpay.kz/api/docs.md.",
    "contact": {
      "name": "AgentPay",
      "url": "https://agentpay.kz/docs"
    }
  },
  "externalDocs": {
    "description": "Документация AgentPay API",
    "url": "https://agentpay.kz/docs"
  },
  "servers": [
    {
      "url": "https://agentpay.kz/api",
      "description": "AgentPay"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Invoices",
      "description": "Разовые счета: push в Kaspi на телефон клиента + ссылка на оплату."
    },
    {
      "name": "Payment links",
      "description": "Платёжные ссылки без телефона клиента."
    },
    {
      "name": "Events",
      "description": "Неизменяемый журнал событий: списки, догон, SSE-стрим."
    },
    {
      "name": "Webhooks",
      "description": "Подписанные HTTP-уведомления о событиях и их переотправка."
    },
    {
      "name": "Sandbox",
      "description": "Песочница (`sk_test_*`): симуляция оплаты и просрочки."
    }
  ],
  "paths": {
    "/v1/invoices": {
      "post": {
        "operationId": "createInvoice",
        "tags": [
          "Invoices"
        ],
        "summary": "Создать разовый счёт",
        "description": "Kaspi шлёт push на телефон клиента, плюс ты получаешь `pay_link_url` для отправки любым каналом (WhatsApp, SMS, чат). `Idempotency-Key` обязателен. В песочнице (`sk_test_*`) счёт сразу возвращается со `status: \"success\"`, `mode: \"test\"` и `kaspi_invoice_id` вида `test_…`; push не уходит, кассир не нужен. Лимит: 60 запросов в минуту.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "description": "Параметры счёта.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceCreateRequest"
              },
              "example": {
                "client_phone": "77001234567",
                "amount_kzt": 1500,
                "comment": "Бронь #123",
                "external_id": "BK-2026-000123",
                "metadata": {
                  "room": "12A",
                  "guests": 2,
                  "source": "site"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Счёт создан и отправлен в Kaspi.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — валидация тела не прошла (в `message` — какое поле); `invalid_json` — тело не JSON; `idempotency_key_required` — нет заголовка Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "`no_active_cashier` — у тенанта нет активного кассира (боевой режим); `cashier_not_ready` — кассир есть, но status != active; `idempotency_conflict` — тот же Idempotency-Key с другим телом.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`auth_expired` / `banned` / `rate_limited` / `network_error` / `invalid_response` / `error` — Kaspi отклонил выпуск. Ресурс сохранён со статусом ошибки (детали в поле `error`), перечитай его GET-ом по `id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "get": {
        "operationId": "listInvoices",
        "tags": [
          "Invoices"
        ],
        "summary": "Список счетов",
        "description": "Новые первыми, cursor-пагинация. Платёжные ссылки сюда не попадают — у них свой список. `paid_after` / `created_after` — строго после указанного момента; это основа инкрементальной сверки (1С: `status=paid&paid_after=<метка>`). Лимит: 120 запросов в минуту.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Фильтр по статусу.",
            "schema": {
              "$ref": "#/components/schemas/InvoiceStatus"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "Фильтр по виду счёта.",
            "schema": {
              "type": "string",
              "enum": [
                "one_time",
                "subscription"
              ]
            }
          },
          {
            "name": "external_id",
            "in": "query",
            "required": false,
            "description": "Точное совпадение с твоим `external_id`.",
            "schema": {
              "type": "string",
              "maxLength": 128
            }
          },
          {
            "name": "created_after",
            "in": "query",
            "required": false,
            "description": "ISO-8601: только счета, созданные строго после этого момента.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "paid_after",
            "in": "query",
            "required": false,
            "description": "ISO-8601: только счета, оплаченные строго после этого момента.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Страница списка.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceList"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — некорректный query-параметр.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/invoices/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "operationId": "getInvoice",
        "tags": [
          "Invoices"
        ],
        "summary": "Один счёт",
        "description": "Счёт по UUID. 404, если не найден, принадлежит другому тенанту, другому режиму (live/test) или является платёжной ссылкой. Лимит: 120 запросов в минуту.",
        "responses": {
          "200": {
            "description": "Счёт.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — счёт не существует, принадлежит другому тенанту или другому режиму.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/invoices/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "operationId": "cancelInvoice",
        "tags": [
          "Invoices"
        ],
        "summary": "Отменить неоплаченный счёт",
        "description": "Тело пустое. `Idempotency-Key` обязателен. Применим только к разовым счетам в статусе `pending|success`. В песочнице Kaspi не вызывается — переход сохраняется сразу. Порождает `invoice.cancelled` + вебхук. Лимит: 60 запросов в минуту.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Счёт отменён.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "400": {
            "description": "`idempotency_key_required` — нет заголовка Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — счёт не существует, принадлежит другому тенанту или другому режиму.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`wrong_kind` — cancel применим только к разовым счетам; `not_sent` — счёт не был принят Kaspi, отменять нечего; `already_paid` — счёт уже оплачен, делай возврат; `already_cancelled` — счёт уже отменён; `no_cashier` — у счёта нет привязанного кассира; `idempotency_conflict` — тот же ключ с другим телом.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "`kaspi_error` — Kaspi отклонил отмену.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/invoices/{id}/refunds": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "operationId": "refundInvoice",
        "tags": [
          "Invoices"
        ],
        "summary": "Возврат оплаченного счёта",
        "description": "Возвращается полная сумма. Тело пустое. `Idempotency-Key` обязателен. Только для счетов в статусе `paid`. В песочнице Kaspi не вызывается — переход сохраняется сразу. Порождает `invoice.refunded` (при ошибке Kaspi — `invoice.refund_failed`) + вебхук. Лимит: 60 запросов в минуту.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Возврат проведён.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "400": {
            "description": "`idempotency_key_required` — нет заголовка Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — счёт не существует, принадлежит другому тенанту или другому режиму.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`not_paid` — refund только для `paid`; `invalid_state` — у счёта нет kaspi id / суммы / кассира; `idempotency_conflict` — тот же ключ с другим телом.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "`kaspi_error` — Kaspi отклонил возврат (порождает `invoice.refund_failed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payment_links": {
      "post": {
        "operationId": "createPaymentLink",
        "tags": [
          "Payment links"
        ],
        "summary": "Создать платёжную ссылку",
        "description": "Оплата без телефона клиента: ты отправляешь ссылку сам (WhatsApp, чат, QR на экране), клиент открывает pay.kaspi.kz и платит. Push от Kaspi не отправляется. Один Kaspi-токен = один платёж. `Idempotency-Key` обязателен. Поля `external_id` и `metadata` — те же, что у счетов. Отправляй клиенту `pay_page_url` (стабильная страница), а не одноразовый `pay_url`. Лимит: 60 запросов в минуту.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "description": "Параметры ссылки.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentLinkCreateRequest"
              },
              "example": {
                "amount_kzt": 5000,
                "comment": "Абонемент, июль",
                "external_id": "ORD-42"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Ссылка создана, Kaspi-токен выпущен.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLink"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — валидация тела не прошла (в `message` — какое поле); `invalid_json` — тело не JSON; `idempotency_key_required` — нет заголовка Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "`no_active_cashier` — у тенанта нет активного кассира (боевой режим); `cashier_not_ready` — кассир есть, но status != active; `idempotency_conflict` — тот же Idempotency-Key с другим телом.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`auth_expired` / `banned` / `rate_limited` / `network_error` / `invalid_response` / `error` — Kaspi отклонил выпуск. Ресурс сохранён со статусом ошибки (детали в поле `error`), перечитай его GET-ом по `id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "get": {
        "operationId": "listPaymentLinks",
        "tags": [
          "Payment links"
        ],
        "summary": "Список платёжных ссылок",
        "description": "Новые первыми, cursor-пагинация. Лимит: 120 запросов в минуту.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Фильтр по статусу.",
            "schema": {
              "$ref": "#/components/schemas/InvoiceStatus"
            }
          },
          {
            "name": "external_id",
            "in": "query",
            "required": false,
            "description": "Точное совпадение с твоим `external_id`.",
            "schema": {
              "type": "string",
              "maxLength": 128
            }
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Страница списка.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkList"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — некорректный query-параметр.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/payment_links/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "operationId": "getPaymentLink",
        "tags": [
          "Payment links"
        ],
        "summary": "Одна платёжная ссылка",
        "description": "Ссылка по UUID. Пока ссылка открыта (status `success`), запрос синхронно опрашивает Kaspi (on-demand reconcile): статус в ответе актуальный, переход (`paid` и т.д.) сохраняется и порождает событие + вебхук. В ответ добавляется поле `kaspi_status_raw` — сырой ответ Kaspi. Лимит: 120 запросов в минуту.",
        "responses": {
          "200": {
            "description": "Платёжная ссылка (с актуальным статусом).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaymentLink"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "kaspi_status_raw": {
                          "description": "Сырой ответ Kaspi на запрос статуса. Присутствует только когда опрос выполнялся (боевая открытая ссылка); при ошибке опроса — `{ \"error\": \"<текст>\" }`."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — ссылка не существует, принадлежит другому тенанту или другому режиму.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/payment_links/{id}/refresh": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "operationId": "refreshPaymentLink",
        "tags": [
          "Payment links"
        ],
        "summary": "Перевыпустить Kaspi-токен",
        "description": "Kaspi-токен одноразовый: если клиент открыл экран оплаты и закрыл его не заплатив, тот же `pay_url` больше не откроется. Этот эндпоинт выпускает новый токен за той же ссылкой: `id` и `pay_page_url` не меняются, `pay_url` — новый. Вытесненный токен остаётся под наблюдением сервера: первый платёж засчитывается, повторный автоматически возвращается (`invoice.duplicate_refunded`). Тело пустое, `Idempotency-Key` не требуется. Порождает `invoice.sent` с `data.refreshed: true`. Лимит: 60 запросов в минуту.",
        "responses": {
          "200": {
            "description": "Токен перевыпущен.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLink"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — ссылка не существует, принадлежит другому тенанту или другому режиму.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`invalid_state` — ссылка закрыта (оплачена / просрочена / отменена / возвращена); `no_cashier` — у ссылки нет привязанного кассира.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "`kaspi_error` — Kaspi отклонил выпуск нового токена.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/events": {
      "get": {
        "operationId": "listEvents",
        "tags": [
          "Events"
        ],
        "summary": "Список событий",
        "description": "Новые первыми, cursor-пагинация. Это тот же журнал, из которого шлются вебхуки — `type=invoice.paid&created_after=<последняя обработанная метка>` догоняет пропущенные события после простоя. Лимит: 120 запросов в минуту.",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Фильтр по типу события.",
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          {
            "name": "resource_id",
            "in": "query",
            "required": false,
            "description": "Фильтр по UUID ресурса (счёта, ссылки, подписки, кассира).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "created_after",
            "in": "query",
            "required": false,
            "description": "ISO-8601: только события, созданные строго после этого момента.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "Страница списка.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventList"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — некорректный query-параметр.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/events/stream": {
      "get": {
        "operationId": "streamEvents",
        "tags": [
          "Events"
        ],
        "summary": "Стрим событий (SSE)",
        "description": "Server-Sent Events (`text/event-stream`). Стрим открыт, пока живо соединение: ping-комментарий каждые 15 секунд, принудительное закрытие через 30 минут простоя. Лимиты: 1 одновременное соединение на ключ, 100 глобально. Используется CLI-командами `agentpay listen` и `agentpay events tail`. Эндпоинт не проходит через общий rate-limit и не отдаёт `X-RateLimit-*`.\n\nФормат кадров:\n\n```\n: connected 2026-09-07T18:35:18.000Z      ← комментарий при открытии\n: ping                                      ← каждые 15 с\nevent: invoice.paid                         ← тип события\nid: 6f1e8a2c-9d3b-4c7e-8a1f-2b5d9c0e4a7b     ← id события\ndata: {\"id\":\"6f1e8a2c-…\",\"object\":\"event\",…} ← ресурс Event одной строкой\n\nevent: timeout                              ← перед закрытием по простою\ndata: {\"reason\":\"idle_30m\"}\n```\n\nСобытия приходят в порядке создания (старые первыми внутри одного опроса, опрос каждые 2 с). Видны только события режима ключа (live/test).",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "ISO-8601: отдавать события, созданные строго после этого момента. По умолчанию — момент подключения.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Поток событий. Соединение остаётся открытым.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Кадры SSE: `event: <EventType>`, `id: <uuid>`, `data: <Event как JSON>`; комментарии `: connected …` и `: ping`; финальный кадр `event: timeout`."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`forbidden_scope` — нужен секретный ключ (sk_*).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`too_many_connections_per_key` — уже есть открытое соединение для этого ключа (лимит 1); `too_many_connections` — достигнут глобальный лимит 100 соединений.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/events/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "get": {
        "operationId": "getEvent",
        "tags": [
          "Events"
        ],
        "summary": "Событие + история доставок",
        "description": "Одно событие плюс история доставок вебхука по нему — ответ на вопрос «дошёл ли `invoice.paid` по брони X до моего сервера». Лимит: 120 запросов в минуту.",
        "responses": {
          "200": {
            "description": "Событие с доставками.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventWithDeliveries"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — событие не существует, принадлежит другому тенанту или другому режиму.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/events/{id}/resend": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "operationId": "resendEvent",
        "tags": [
          "Events",
          "Webhooks"
        ],
        "summary": "Переотправить вебхук",
        "description": "Создаёт новую доставку (свежий график повторов) на все активные endpoint’ы, подписанные на тип события, или на один — если передать `endpoint_id`. Исходная история доставок сохраняется. Доставка подписывается текущим `whsec_*` endpoint’а и уходит в течение минуты. `Idempotency-Key` обязателен. Лимит: 60 запросов в минуту.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "description": "Необязательное тело. Пустое тело или `{}` — переотправить на все подписанные endpoint’ы.",
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResendRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Доставки поставлены в очередь.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryList"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — `endpoint_id` не UUID; `invalid_json` — тело не JSON; `idempotency_key_required` — нет заголовка Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`not_found` — событие не существует или другого режима; `endpoint_not_found` — указанный webhook endpoint не существует или отключён.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`no_webhook_endpoints` — ни один активный endpoint не подписан на этот тип события; `idempotency_conflict` — тот же ключ с другим телом.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/test/invoices/{id}/pay": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "operationId": "testPayInvoice",
        "tags": [
          "Sandbox"
        ],
        "summary": "Симулировать оплату (песочница)",
        "description": "Перевести тестовый счёт или платёжную ссылку в `paid`. Уходят те же события (`invoice.paid`) и подписанные вебхуки, что и в бою. Только для ключей `sk_test_*` (боевой ключ получает `403 test_mode_required`). `{id}` — id счёта **или** платёжной ссылки; ответ — тот ресурс, которому принадлежит id. Идемпотентен: повторный вызов на уже переведённом ресурсе вернёт 200 с ресурсом; на закрытом (отменён / просрочен / оплачен для expire) — `409 invalid_state`. Тело пустое, `Idempotency-Key` не требуется. Лимит: 60 запросов в минуту.",
        "responses": {
          "200": {
            "description": "Переход выполнен (или уже был выполнен ранее).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "description": "Счёт или платёжная ссылка — в зависимости от того, чему принадлежит `id`.",
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Invoice"
                    },
                    {
                      "$ref": "#/components/schemas/PaymentLink"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "object",
                    "mapping": {
                      "invoice": "#/components/schemas/Invoice",
                      "payment_link": "#/components/schemas/PaymentLink"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "description": "`test_mode_required` — sandbox-эндпоинт вызван боевым ключом; `forbidden_scope` — pk_-ключ; `api_requires_pro` — нужен тариф Pro.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — ресурс не существует, принадлежит другому тенанту или не тестовый.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`invalid_state` — переход невозможен из текущего статуса.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/test/invoices/{id}/expire": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "operationId": "testExpireInvoice",
        "tags": [
          "Sandbox"
        ],
        "summary": "Симулировать просрочку (песочница)",
        "description": "Перевести тестовый счёт или платёжную ссылку в `expired`. Уходят те же события (`invoice.expired`) и подписанные вебхуки, что и в бою. Только для ключей `sk_test_*` (боевой ключ получает `403 test_mode_required`). `{id}` — id счёта **или** платёжной ссылки; ответ — тот ресурс, которому принадлежит id. Идемпотентен: повторный вызов на уже переведённом ресурсе вернёт 200 с ресурсом; на закрытом (отменён / просрочен / оплачен для expire) — `409 invalid_state`. Тело пустое, `Idempotency-Key` не требуется. Лимит: 60 запросов в минуту.",
        "responses": {
          "200": {
            "description": "Переход выполнен (или уже был выполнен ранее).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "description": "Счёт или платёжная ссылка — в зависимости от того, чему принадлежит `id`.",
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Invoice"
                    },
                    {
                      "$ref": "#/components/schemas/PaymentLink"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "object",
                    "mapping": {
                      "invoice": "#/components/schemas/Invoice",
                      "payment_link": "#/components/schemas/PaymentLink"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "description": "`test_mode_required` — sandbox-эндпоинт вызван боевым ключом; `forbidden_scope` — pk_-ключ; `api_requires_pro` — нужен тариф Pro.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — ресурс не существует, принадлежит другому тенанту или не тестовый.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`invalid_state` — переход невозможен из текущего статуса.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "webhooks": {
    "invoice.created": {
      "post": {
        "operationId": "onInvoiceCreated",
        "tags": [
          "Webhooks"
        ],
        "summary": "invoice.created",
        "description": "Счёт или платёжная ссылка созданы в нашей БД. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event, где `data` дополнена актуальными полями счёта (`InvoiceEventData`).",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "invoice.created"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "invoice"
                      },
                      "data": {
                        "$ref": "#/components/schemas/InvoiceEventData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "invoice.sent": {
      "post": {
        "operationId": "onInvoiceSent",
        "tags": [
          "Webhooks"
        ],
        "summary": "invoice.sent",
        "description": "Отправлен в Kaspi: push ушёл клиенту (счёт) или токен выпущен (ссылка). При перевыпуске токена приходит с `data.refreshed: true`. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event, где `data` дополнена актуальными полями счёта (`InvoiceEventData`).",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "invoice.sent"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "invoice"
                      },
                      "data": {
                        "$ref": "#/components/schemas/InvoiceEventData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "invoice.paid": {
      "post": {
        "operationId": "onInvoicePaid",
        "tags": [
          "Webhooks"
        ],
        "summary": "invoice.paid",
        "description": "Клиент оплатил. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event, где `data` дополнена актуальными полями счёта (`InvoiceEventData`).",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "invoice.paid"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "invoice"
                      },
                      "data": {
                        "$ref": "#/components/schemas/InvoiceEventData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "invoice.expired": {
      "post": {
        "operationId": "onInvoiceExpired",
        "tags": [
          "Webhooks"
        ],
        "summary": "invoice.expired",
        "description": "Просрочен (24 часа без оплаты). Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event, где `data` дополнена актуальными полями счёта (`InvoiceEventData`).",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "invoice.expired"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "invoice"
                      },
                      "data": {
                        "$ref": "#/components/schemas/InvoiceEventData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "invoice.cancelled": {
      "post": {
        "operationId": "onInvoiceCancelled",
        "tags": [
          "Webhooks"
        ],
        "summary": "invoice.cancelled",
        "description": "Отменён продавцом, через API или отклонён клиентом в Kaspi. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event, где `data` дополнена актуальными полями счёта (`InvoiceEventData`).",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "invoice.cancelled"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "invoice"
                      },
                      "data": {
                        "$ref": "#/components/schemas/InvoiceEventData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "invoice.refunded": {
      "post": {
        "operationId": "onInvoiceRefunded",
        "tags": [
          "Webhooks"
        ],
        "summary": "invoice.refunded",
        "description": "Возврат успешно прошёл. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event, где `data` дополнена актуальными полями счёта (`InvoiceEventData`).",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "invoice.refunded"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "invoice"
                      },
                      "data": {
                        "$ref": "#/components/schemas/InvoiceEventData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "invoice.refund_failed": {
      "post": {
        "operationId": "onInvoiceRefundFailed",
        "tags": [
          "Webhooks"
        ],
        "summary": "invoice.refund_failed",
        "description": "Возврат не прошёл. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event, где `data` дополнена актуальными полями счёта (`InvoiceEventData`).",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "invoice.refund_failed"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "invoice"
                      },
                      "data": {
                        "$ref": "#/components/schemas/InvoiceEventData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "invoice.send_failed": {
      "post": {
        "operationId": "onInvoiceSendFailed",
        "tags": [
          "Webhooks"
        ],
        "summary": "invoice.send_failed",
        "description": "Не удалось отправить в Kaspi (детали в `data.code`). Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event, где `data` дополнена актуальными полями счёта (`InvoiceEventData`).",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "invoice.send_failed"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "invoice"
                      },
                      "data": {
                        "$ref": "#/components/schemas/InvoiceEventData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "invoice.duplicate_refunded": {
      "post": {
        "operationId": "onInvoiceDuplicateRefunded",
        "tags": [
          "Webhooks"
        ],
        "summary": "invoice.duplicate_refunded",
        "description": "Повторная оплата по вытесненному токену автоматически возвращена (основной платёж засчитан). Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event, где `data` дополнена актуальными полями счёта (`InvoiceEventData`).",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "invoice.duplicate_refunded"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "invoice"
                      },
                      "data": {
                        "$ref": "#/components/schemas/InvoiceEventData"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "subscription.created": {
      "post": {
        "operationId": "onSubscriptionCreated",
        "tags": [
          "Webhooks"
        ],
        "summary": "subscription.created",
        "description": "Подписка создана. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "subscription.created"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "subscription"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "subscription.advanced": {
      "post": {
        "operationId": "onSubscriptionAdvanced",
        "tags": [
          "Webhooks"
        ],
        "summary": "subscription.advanced",
        "description": "next_run_at сдвинулся (новый инвойс). Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "subscription.advanced"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "subscription"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "subscription.paused": {
      "post": {
        "operationId": "onSubscriptionPaused",
        "tags": [
          "Webhooks"
        ],
        "summary": "subscription.paused",
        "description": "Подписка на паузе. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "subscription.paused"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "subscription"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "subscription.resumed": {
      "post": {
        "operationId": "onSubscriptionResumed",
        "tags": [
          "Webhooks"
        ],
        "summary": "subscription.resumed",
        "description": "Подписка возобновлена. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "subscription.resumed"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "subscription"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "subscription.failed": {
      "post": {
        "operationId": "onSubscriptionFailed",
        "tags": [
          "Webhooks"
        ],
        "summary": "subscription.failed",
        "description": "Очередной счёт по подписке не выставился (ошибка Kaspi). Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "subscription.failed"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "subscription"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "subscription.cancelled": {
      "post": {
        "operationId": "onSubscriptionCancelled",
        "tags": [
          "Webhooks"
        ],
        "summary": "subscription.cancelled",
        "description": "Подписка отменена. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "subscription.cancelled"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "subscription"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "cashier.activated": {
      "post": {
        "operationId": "onCashierActivated",
        "tags": [
          "Webhooks"
        ],
        "summary": "cashier.activated",
        "description": "Кассир прошёл SMS-верификацию. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "cashier.activated"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "cashier"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "cashier.replaced": {
      "post": {
        "operationId": "onCashierReplaced",
        "tags": [
          "Webhooks"
        ],
        "summary": "cashier.replaced",
        "description": "Кассир заменён на нового. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "cashier.replaced"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "cashier"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "cashier.removed": {
      "post": {
        "operationId": "onCashierRemoved",
        "tags": [
          "Webhooks"
        ],
        "summary": "cashier.removed",
        "description": "Кассир удалён (rotation). Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "cashier.removed"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "cashier"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "cashier.banned": {
      "post": {
        "operationId": "onCashierBanned",
        "tags": [
          "Webhooks"
        ],
        "summary": "cashier.banned",
        "description": "Kaspi заблокировал кассира. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "cashier.banned"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "cashier"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    },
    "cashier.auth_expired": {
      "post": {
        "operationId": "onCashierAuthExpired",
        "tags": [
          "Webhooks"
        ],
        "summary": "cashier.auth_expired",
        "description": "Сессия кассира истекла, нужен re-auth. Отправляется на каждый активный webhook endpoint, подписанный на этот тип (`/app/settings/webhooks`). Один endpoint получает события обоих режимов — проверяй поле `mode`. Проверь подпись `X-AgentPay-Signature` по сырому телу, ответь 2xx, дедуплицируй по `id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookSignature"
          },
          {
            "$ref": "#/components/parameters/WebhookEventType"
          },
          {
            "$ref": "#/components/parameters/WebhookEventId"
          },
          {
            "$ref": "#/components/parameters/WebhookUserAgent"
          }
        ],
        "requestBody": {
          "description": "Ресурс Event.",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEvent"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "cashier.auth_expired"
                      },
                      "resource_type": {
                        "type": "string",
                        "const": "cashier"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Доставка подтверждена. Успех = любой HTTP 2xx, полученный в течение 15 секунд; тело ответа игнорируется. Отвечай быстро, тяжёлую обработку делай асинхронно. Приёмник обязан быть идемпотентным (доставка at-least-once) — дедуплицируй по `id` события."
          },
          "default": {
            "description": "Любой не-2xx ответ или таймаут (15 с) считается неудачей: доставка повторяется по графику 1 мин → 5 мин → 30 мин → 2 ч → 12 ч → 1 день → 2 дня, затем dead-letter (всего 8 попыток, ~3 дня). Потерянное событие можно переотправить через `POST /v1/events/{id}/resend` или догнать через `GET /v1/events?created_after=…`."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Заголовок `Authorization: Bearer <key>`. Секретные ключи: `sk_live_*` — боевой режим (реальные платежи через кассира Kaspi), `sk_test_*` — песочница (Kaspi не вызывается, деньги не списываются, кассир не нужен). Ресурсы разделены по режимам: тестовый ключ видит только тестовые счета, ссылки и события, боевой — только боевые. Публикуемые `pk_*` ключи API-эндпоинтами не принимаются (`403 forbidden_scope`). Ключ создаётся в `/app/settings/api-keys`, показывается один раз; API доступен на тарифе Pro с активной подпиской."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Обязателен на POST-эндпоинтах, создающих или мутирующих ресурсы. Сгенерируй UUID на стороне клиента на каждую логическую операцию (не на каждый retry). Повтор с тем же ключом и тем же телом вернёт закешированный ответ (24 часа); тот же ключ с другим телом — `409 idempotency_conflict`. Практичный приём: UUID v5 от строки `booking:<id>`.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Курсор из `next_cursor` предыдущего ответа.",
        "schema": {
          "type": "string"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Размер страницы. По умолчанию 20, максимум 100.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      },
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "UUID ресурса.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "WebhookSignature": {
        "name": "X-AgentPay-Signature",
        "in": "header",
        "required": true,
        "description": "Подпись `t=<unix_seconds>,v1=<hex>`. `v1` = HMAC-SHA256 от строки `<t>.<raw_body>` с твоим `whsec_*` в качестве ключа. Сравнивай constant-time, отвергай запросы, у которых `t` отличается от текущего времени больше чем на 5 минут (replay). Подписывается **сырое** тело — не пересериализуй JSON перед проверкой.",
        "schema": {
          "type": "string",
          "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
        },
        "example": "t=1735689600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
      },
      "WebhookEventType": {
        "name": "X-AgentPay-Event-Type",
        "in": "header",
        "required": true,
        "description": "Тип события, дублирует `type` в теле.",
        "schema": {
          "$ref": "#/components/schemas/EventType"
        }
      },
      "WebhookEventId": {
        "name": "X-AgentPay-Event-Id",
        "in": "header",
        "required": true,
        "description": "UUID события, дублирует `id` в теле. Ключ дедупликации на приёмнике.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "WebhookUserAgent": {
        "name": "User-Agent",
        "in": "header",
        "required": true,
        "description": "Всегда `AgentPay-Webhooks/1.0`.",
        "schema": {
          "type": "string",
          "const": "AgentPay-Webhooks/1.0"
        }
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Лимит запросов в минуту для этого ключа на данном эндпоинте (60 — запись, 120 — чтение).",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Сколько запросов осталось в текущем скользящем минутном окне.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "Unix-время (секунды), когда окно сбросится.",
        "schema": {
          "type": "integer"
        }
      },
      "Retry-After": {
        "description": "Через сколько секунд можно повторить запрос.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "`authentication_required` — нет заголовка Authorization; `invalid_api_key` — ключ не найден или отозван.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "`subscription_required` — подписка тенанта не активна (trial или past_due).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "`forbidden_scope` — pk_-ключ на secret-эндпоинте; `api_requires_pro` — API и CLI доступны только на тарифе Pro.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited` — превышен лимит запросов в минуту (60 на запись, 120 на чтение, скользящее окно на ключ). Подожди `Retry-After` секунд.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "`internal_error` — неожиданная ошибка сервера.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Mode": {
        "type": "string",
        "enum": [
          "live",
          "test"
        ],
        "description": "Режим ключа и ресурса. `live` — боевой (реальные платежи через кассира Kaspi), `test` — песочница (Kaspi не вызывается, деньги не списываются). Тестовый ключ видит только тестовые ресурсы, боевой — только боевые."
      },
      "InvoiceStatus": {
        "type": "string",
        "enum": [
          "pending",
          "success",
          "paid",
          "expired",
          "cancelled",
          "refunded",
          "auth_expired",
          "banned",
          "rate_limited",
          "network_error",
          "invalid_response",
          "error",
          "skipped_stale"
        ],
        "description": "Статус счёта или платёжной ссылки. Happy path: `pending` → `success` (отправлен в Kaspi) → `paid|expired|cancelled|refunded` (терминальные). Статусы ошибок отправки — счёт сохранён, но до Kaspi не дошёл; детали в поле `error`.\n\n- `pending` — Создан в БД, ещё не отправлен в Kaspi\n- `success` — Отправлен в Kaspi, ждёт оплаты\n- `paid` — Оплачен (подтверждено poll’ом или вебхуком)\n- `expired` — Просрочен — Kaspi пометил или вышел TTL (24 часа)\n- `cancelled` — Отменён продавцом или Kaspi (в т.ч. отказ плательщика в приложении Kaspi)\n- `refunded` — Возврат после оплаты\n- `auth_expired` — Ошибка отправки: сессия кассира истекла, нужен re-auth по SMS\n- `banned` — Ошибка отправки: Kaspi заблокировал кассира (анти-фрод)\n- `rate_limited` — Ошибка отправки: Kaspi ограничил частоту запросов\n- `network_error` — Ошибка отправки: сетевая ошибка при вызове Kaspi\n- `invalid_response` — Ошибка отправки: Kaspi ответил в неожиданном формате\n- `error` — Ошибка отправки: неклассифицированная ошибка\n- `skipped_stale` — Пропущено окно отправки (> 12 часов), счёт не отправлен"
      },
      "EventType": {
        "type": "string",
        "enum": [
          "invoice.created",
          "invoice.sent",
          "invoice.paid",
          "invoice.expired",
          "invoice.cancelled",
          "invoice.refunded",
          "invoice.refund_failed",
          "invoice.send_failed",
          "invoice.duplicate_refunded",
          "subscription.created",
          "subscription.advanced",
          "subscription.paused",
          "subscription.resumed",
          "subscription.failed",
          "subscription.cancelled",
          "cashier.activated",
          "cashier.replaced",
          "cashier.removed",
          "cashier.banned",
          "cashier.auth_expired"
        ],
        "description": "Тип события. «Failed» в терминах интегратора — это `invoice.send_failed` (не удалось выставить), `invoice.expired` (клиент не оплатил) и `invoice.refund_failed` (возврат не прошёл).\n\n- `invoice.created` — Счёт или платёжная ссылка созданы в нашей БД\n- `invoice.sent` — Отправлен в Kaspi: push ушёл клиенту (счёт) или токен выпущен (ссылка). При перевыпуске токена приходит с `data.refreshed: true`\n- `invoice.paid` — Клиент оплатил\n- `invoice.expired` — Просрочен (24 часа без оплаты)\n- `invoice.cancelled` — Отменён продавцом, через API или отклонён клиентом в Kaspi\n- `invoice.refunded` — Возврат успешно прошёл\n- `invoice.refund_failed` — Возврат не прошёл\n- `invoice.send_failed` — Не удалось отправить в Kaspi (детали в `data.code`)\n- `invoice.duplicate_refunded` — Повторная оплата по вытесненному токену автоматически возвращена (основной платёж засчитан)\n- `subscription.created` — Подписка создана\n- `subscription.advanced` — next_run_at сдвинулся (новый инвойс)\n- `subscription.paused` — Подписка на паузе\n- `subscription.resumed` — Подписка возобновлена\n- `subscription.failed` — Очередной счёт по подписке не выставился (ошибка Kaspi)\n- `subscription.cancelled` — Подписка отменена\n- `cashier.activated` — Кассир прошёл SMS-верификацию\n- `cashier.replaced` — Кассир заменён на нового\n- `cashier.removed` — Кассир удалён (rotation)\n- `cashier.banned` — Kaspi заблокировал кассира\n- `cashier.auth_expired` — Сессия кассира истекла, нужен re-auth"
      },
      "Metadata": {
        "type": "object",
        "description": "Произвольный набор key→value, задаётся при создании. До 20 ключей (≤40 символов), значения string ≤500 / number / boolean / null (вложенные объекты не допускаются). Не виден плательщику, возвращается в ресурсе и в каждом вебхуке по этому счёту.",
        "maxProperties": 20,
        "propertyNames": {
          "type": "string",
          "minLength": 1,
          "maxLength": 40
        },
        "additionalProperties": {
          "oneOf": [
            {
              "type": "string",
              "maxLength": 500
            },
            {
              "type": "number"
            },
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ]
        },
        "examples": [
          {
            "room": "12A",
            "guests": 2,
            "source": "site"
          }
        ]
      },
      "Error": {
        "type": "object",
        "description": "Единый формат всех ошибок. `type` — стабильный машинный код, `message` — текст для человека.\n\n| HTTP | type | Когда |\n|---|---|---|\n| 400 | `invalid_request` | zod-валидация не прошла (в `message` — какое поле) |\n| 400 | `invalid_json` | тело не JSON |\n| 400 | `idempotency_key_required` | нет Idempotency-Key на мутирующем POST |\n| 401 | `authentication_required` | нет Authorization header |\n| 401 | `invalid_api_key` | ключ не найден или отозван |\n| 402 | `subscription_required` | подписка тенанта не активна (trial или past_due) |\n| 403 | `forbidden_scope` | pk_ ключ на secret endpoint |\n| 403 | `api_requires_pro` | API и CLI доступны только на тарифе Pro |\n| 403 | `test_mode_required` | sandbox-эндпоинт вызван боевым ключом |\n| 404 | `not_found` | ресурс не существует, принадлежит другому тенанту или другому режиму (live/test) |\n| 404 | `endpoint_not_found` | resend на несуществующий/отключённый webhook endpoint |\n| 409 | `no_active_cashier` | у тенанта нет активного кассира (боевой режим) |\n| 409 | `cashier_not_ready` | кассир есть, но status != active |\n| 409 | `wrong_kind` | cancel применим только к разовым счетам |\n| 409 | `not_sent` | счёт не был принят Kaspi — отменять нечего |\n| 409 | `already_paid` | отмена счёта, который уже оплачен — делай возврат |\n| 409 | `already_cancelled` | счёт уже отменён |\n| 409 | `not_paid` | возврат на неоплаченном счёте — refund только для `paid` |\n| 409 | `no_cashier` | у счёта/ссылки нет привязанного кассира |\n| 409 | `invalid_state` | операция невозможна в текущем статусе (напр. refresh закрытой ссылки, test pay на отменённом) |\n| 409 | `idempotency_conflict` | тот же ключ + другое тело |\n| 409 | `no_webhook_endpoints` | resend, но ни один активный endpoint не подписан на этот тип события |\n| 422 | `auth_expired / banned / rate_limited / network_error / invalid_response / error` | Kaspi отклонил выпуск при создании — счёт/ссылка сохранены со статусом ошибки, перечитай GET-ом |\n| 429 | `rate_limited` | превышен лимит запросов в минуту |\n| 429 | `too_many_connections / too_many_connections_per_key` | SSE: превышен лимит соединений (100 глобально / 1 на ключ) |\n| 500 | `internal_error` | неожиданная ошибка сервера |\n| 502 | `kaspi_error` | Kaspi отклонил вызов |",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "description": "Стабильный машинный код ошибки (см. таблицу в описании схемы).",
                "examples": [
                  "invalid_request",
                  "not_found",
                  "no_active_cashier"
                ]
              },
              "message": {
                "type": "string",
                "description": "Человекочитаемое описание. Присутствует во всех ответах, кроме ошибок `GET /v1/events/stream`, где возвращается только `type`."
              }
            }
          }
        },
        "examples": [
          {
            "error": {
              "type": "no_active_cashier",
              "message": "No active cashier on this tenant. Add one in the web app first."
            }
          }
        ]
      },
      "Invoice": {
        "type": "object",
        "description": "Разовый счёт (или счёт по подписке). Три идентификатора у каждого платежа: `id` (наш UUID — ключ дедупликации на твоей стороне), `kaspi_invoice_id` (номер операции в Kaspi) и `receipt_url` (чек Kaspi, появляется после оплаты).",
        "required": [
          "id",
          "object",
          "kind",
          "status",
          "mode",
          "client_phone",
          "amount_kzt",
          "comment",
          "external_id",
          "metadata",
          "kaspi_invoice_id",
          "receipt_url",
          "pay_link_url",
          "created_at",
          "ran_at",
          "paid_at",
          "expired_at",
          "cancelled_at",
          "refunded_at",
          "subscription_id",
          "error"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID счёта. Наш идентификатор — ключ дедупликации на твоей стороне."
          },
          "object": {
            "type": "string",
            "const": "invoice",
            "description": "Тип ресурса, всегда `invoice`."
          },
          "kind": {
            "type": "string",
            "enum": [
              "one_time",
              "subscription"
            ],
            "description": "Вид счёта: `one_time` — разовый, `subscription` — выставлен по подписке."
          },
          "status": {
            "$ref": "#/components/schemas/InvoiceStatus"
          },
          "mode": {
            "$ref": "#/components/schemas/Mode"
          },
          "client_phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Телефон плательщика: 11 цифр, начинается с 7, без `+`.",
            "pattern": "^7\\d{10}$"
          },
          "amount_kzt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Сумма в тенге, целое число."
          },
          "comment": {
            "type": [
              "string",
              "null"
            ],
            "description": "Назначение платежа. Виден плательщику в Kaspi.",
            "maxLength": 200
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Твой id (booking_id / order_id), переданный при создании. Не виден плательщику, возвращается в ресурсе и вебхуках, доступен как фильтр списка.",
            "maxLength": 128
          },
          "metadata": {
            "description": "Метаданные, переданные при создании, или `null`.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/Metadata"
              },
              {
                "type": "null"
              }
            ]
          },
          "kaspi_invoice_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Номер операции в Kaspi (в песочнице — `test_…`). Для сверки с выпиской Kaspi."
          },
          "receipt_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ссылка на чек Kaspi, появляется после оплаты.",
            "format": "uri"
          },
          "pay_link_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ссылка для клиента: открывает Kaspi в один тап. Отправляй её в WhatsApp, SMS, чат. `null` после отмены.",
            "format": "uri"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Момент создания (ISO-8601, UTC)."
          },
          "ran_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент отправки в Kaspi."
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент оплаты. Основа инкрементальной сверки через `paid_after`."
          },
          "expired_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент просрочки."
          },
          "cancelled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент отмены."
          },
          "refunded_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент возврата."
          },
          "subscription_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUID подписки, если счёт выставлен по подписке."
          },
          "error": {
            "type": [
              "object",
              "null"
            ],
            "description": "Детали ошибки отправки в Kaspi (статусы `auth_expired`, `banned`, `rate_limited`, …). `null`, если ошибки не было.",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Машинный код ошибки Kaspi-слоя, совпадает со статусом ошибки."
              },
              "message": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Текст ошибки для человека."
              }
            }
          }
        }
      },
      "PaymentLink": {
        "type": "object",
        "description": "Платёжная ссылка — оплата без телефона клиента. Push от Kaspi не отправляется; ты сам отправляешь ссылку (WhatsApp, чат, QR на экране). Статусы — те же, что у счетов.",
        "required": [
          "id",
          "object",
          "status",
          "mode",
          "amount_kzt",
          "comment",
          "external_id",
          "metadata",
          "pay_url",
          "pay_page_url",
          "kaspi_operation_id",
          "receipt_url",
          "created_at",
          "ran_at",
          "paid_at",
          "expired_at",
          "cancelled_at",
          "refunded_at",
          "error"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID платёжной ссылки. Не меняется при перевыпуске Kaspi-токена."
          },
          "object": {
            "type": "string",
            "const": "payment_link",
            "description": "Тип ресурса, всегда `payment_link`."
          },
          "status": {
            "$ref": "#/components/schemas/InvoiceStatus"
          },
          "mode": {
            "$ref": "#/components/schemas/Mode"
          },
          "amount_kzt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Сумма в тенге, целое число."
          },
          "comment": {
            "type": [
              "string",
              "null"
            ],
            "description": "Назначение платежа. Виден плательщику в Kaspi.",
            "maxLength": 200
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Твой id (booking_id / order_id), переданный при создании. Не виден плательщику, возвращается в ресурсе и вебхуках, доступен как фильтр списка.",
            "maxLength": 128
          },
          "metadata": {
            "description": "Метаданные, переданные при создании, или `null`.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/Metadata"
              },
              {
                "type": "null"
              }
            ]
          },
          "pay_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Прямая Kaspi-ссылка `pay.kaspi.kz/pay/<token>`. **Одноразовая**: живёт до первого открытия экрана оплаты. В песочнице указывает на hosted-страницу.",
            "format": "uri"
          },
          "pay_page_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "**Стабильная** hosted-страница оплаты (`/pay/<token>` на нашем домене). Никогда не меняется и сама перевыпускает Kaspi-токен, когда предыдущий сгорел. Отправляй клиенту её.",
            "format": "uri"
          },
          "kaspi_operation_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Номер операции в Kaspi (в песочнице — `test_…`)."
          },
          "receipt_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ссылка на чек Kaspi, появляется после оплаты.",
            "format": "uri"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Момент создания (ISO-8601, UTC)."
          },
          "ran_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент выпуска токена в Kaspi."
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент оплаты."
          },
          "expired_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент просрочки."
          },
          "cancelled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент отмены."
          },
          "refunded_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент возврата."
          },
          "error": {
            "type": [
              "object",
              "null"
            ],
            "description": "Детали ошибки отправки в Kaspi (статусы `auth_expired`, `banned`, `rate_limited`, …). `null`, если ошибки не было.",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Машинный код ошибки Kaspi-слоя, совпадает со статусом ошибки."
              },
              "message": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Текст ошибки для человека."
              }
            }
          }
        }
      },
      "Event": {
        "type": "object",
        "description": "Запись неизменяемого журнала событий. Каждое изменение состояния пишется сюда и разлетается по подписанным webhook endpoint’ам.",
        "required": [
          "id",
          "object",
          "type",
          "resource_type",
          "resource_id",
          "mode",
          "data",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Уникальный UUID события. Храни его и игнорируй повторы (доставка at-least-once)."
          },
          "object": {
            "type": "string",
            "const": "event",
            "description": "Тип ресурса, всегда `event`."
          },
          "type": {
            "$ref": "#/components/schemas/EventType"
          },
          "resource_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "invoice",
              "subscription",
              "cashier",
              null
            ],
            "description": "Тип ресурса, к которому относится событие."
          },
          "resource_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "UUID ресурса. Для `invoice.*` — id счёта или платёжной ссылки (пригоден для `GET /v1/invoices/{id}` / `GET /v1/payment_links/{id}`)."
          },
          "mode": {
            "$ref": "#/components/schemas/Mode"
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "Полезная нагрузка события (например `kaspiStatus`, `kaspiInvoiceId`). В вебхуках для `invoice.*` дополняется актуальными полями счёта — см. `InvoiceEventData`."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Момент события (ISO-8601, UTC). Основа догона через `created_after`."
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "description": "Цепочка попыток доставки одного события на один webhook endpoint. Видна в `GET /v1/events/{id}` и возвращается из `POST /v1/events/{id}/resend`.",
        "required": [
          "id",
          "object",
          "endpoint_id",
          "endpoint_url",
          "event_id",
          "status",
          "attempt_count",
          "next_attempt_at",
          "last_response_status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID доставки."
          },
          "object": {
            "type": "string",
            "const": "webhook_delivery",
            "description": "Тип ресурса, всегда `webhook_delivery`."
          },
          "endpoint_id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID webhook endpoint’а."
          },
          "endpoint_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL endpoint’а, на который шла доставка.",
            "format": "uri"
          },
          "event_id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID события."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "success",
              "dead_letter"
            ],
            "description": "Статус доставки: `pending` — в очереди / ждёт повтора, `success` — получен 2xx, `dead_letter` — 8 попыток исчерпаны или endpoint отключён."
          },
          "attempt_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Сколько попыток уже сделано."
          },
          "next_attempt_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Когда запланирована следующая попытка (`null`, если доставка завершена)."
          },
          "last_response_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP-статус последнего ответа endpoint’а (`null`, если ответа не было — таймаут / сетевая ошибка)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Момент постановки в очередь."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Момент последнего изменения."
          }
        }
      },
      "EventWithDeliveries": {
        "description": "Событие плюс история доставок вебхука по нему.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Event"
          },
          {
            "type": "object",
            "required": [
              "deliveries"
            ],
            "properties": {
              "deliveries": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                },
                "description": "История доставок, новые первыми (до 100 записей)."
              }
            }
          }
        ]
      },
      "InvoiceEventData": {
        "type": "object",
        "description": "Поле `data` вебхука для событий `invoice.*`: к исходной нагрузке события добавлены актуальные поля счёта, чтобы не делать GET за каждой оплатой. Всё нужное для документа в 1С уже здесь.",
        "additionalProperties": true,
        "properties": {
          "kaspiStatus": {
            "type": "string",
            "description": "Сырой статус операции в Kaspi (например `Confirmed`)."
          },
          "kaspiInvoiceId": {
            "type": "string",
            "description": "Номер операции в Kaspi (дублирует `kaspi_invoice_id`)."
          },
          "code": {
            "type": "string",
            "description": "Только для `invoice.send_failed`: код ошибки отправки (`auth_expired`, `banned`, …)."
          },
          "refreshed": {
            "type": "boolean",
            "description": "Только для `invoice.sent`: `true`, если это перевыпуск токена платёжной ссылки, а не новый счёт."
          },
          "amount_kzt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Сумма в тенге, целое число."
          },
          "client_phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Телефон плательщика (11 цифр). У платёжных ссылок — `null`."
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент оплаты (ISO-8601, UTC)."
          },
          "status": {
            "description": "Актуальный статус счёта на момент доставки.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/InvoiceStatus"
              },
              {
                "type": "null"
              }
            ]
          },
          "kind": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "one_time",
              "subscription",
              "qr_link",
              null
            ],
            "description": "Вид ресурса: `one_time` / `subscription` — счёт, `qr_link` — платёжная ссылка."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Твой booking_id / order_id, переданный при создании.",
            "maxLength": 128
          },
          "metadata": {
            "description": "Метаданные, переданные при создании, или `null`.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/Metadata"
              },
              {
                "type": "null"
              }
            ]
          },
          "kaspi_invoice_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Номер операции в Kaspi (в песочнице — `test_…`)."
          },
          "receipt_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ссылка на чек Kaspi.",
            "format": "uri"
          }
        }
      },
      "WebhookEvent": {
        "description": "Тело webhook-запроса: ресурс `event` плюс `account_id`. Подписывается сырое тело — не пересериализуй JSON перед проверкой подписи.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Event"
          },
          {
            "type": "object",
            "required": [
              "account_id"
            ],
            "properties": {
              "account_id": {
                "type": "string",
                "format": "uuid",
                "description": "Id аккаунта (тенанта), которому принадлежит событие. Для white-label партнёров, чей один endpoint получает события многих аккаунтов, — ключ маршрутизации."
              }
            }
          }
        ]
      },
      "InvoiceCreateRequest": {
        "type": "object",
        "required": [
          "client_phone",
          "amount_kzt"
        ],
        "properties": {
          "client_phone": {
            "type": "string",
            "description": "Телефон плательщика: 11 цифр, начинается с 7, без `+`.",
            "pattern": "^7\\d{10}$"
          },
          "amount_kzt": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500000,
            "description": "Сумма в тенге, 1…500 000."
          },
          "comment": {
            "type": "string",
            "description": "Назначение платежа. **Виден плательщику** в Kaspi.",
            "maxLength": 200
          },
          "external_id": {
            "type": "string",
            "description": "Твой id (booking_id / order_id). Не виден плательщику, возвращается в ресурсе и вебхуках, доступен как фильтр.",
            "minLength": 1,
            "maxLength": 128
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "PaymentLinkCreateRequest": {
        "type": "object",
        "required": [
          "amount_kzt"
        ],
        "properties": {
          "amount_kzt": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500000,
            "description": "Сумма в тенге, 1…500 000."
          },
          "comment": {
            "type": "string",
            "description": "Назначение платежа. **Виден плательщику** в Kaspi.",
            "maxLength": 200
          },
          "external_id": {
            "type": "string",
            "description": "Твой id (booking_id / order_id). Не виден плательщику, возвращается в ресурсе и вебхуках, доступен как фильтр.",
            "minLength": 1,
            "maxLength": 128
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "ResendRequest": {
        "type": "object",
        "description": "Тело необязательно. Без `endpoint_id` событие переотправляется на все активные endpoint’ы, подписанные на его тип.",
        "properties": {
          "endpoint_id": {
            "type": "string",
            "format": "uuid",
            "description": "Переотправить только на этот webhook endpoint."
          }
        }
      },
      "InvoiceList": {
        "type": "object",
        "description": "Страница списка счетов.",
        "required": [
          "object",
          "data",
          "has_more",
          "next_cursor"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list",
            "description": "Тип ресурса, всегда `list`."
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Invoice"
            },
            "description": "Элементы страницы, новые первыми."
          },
          "has_more": {
            "type": "boolean",
            "description": "Есть ли ещё страницы."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Курсор следующей страницы — передай в параметре `cursor`. `null`, если страниц больше нет."
          }
        }
      },
      "PaymentLinkList": {
        "type": "object",
        "description": "Страница списка платёжных ссылок.",
        "required": [
          "object",
          "data",
          "has_more",
          "next_cursor"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list",
            "description": "Тип ресурса, всегда `list`."
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentLink"
            },
            "description": "Элементы страницы, новые первыми."
          },
          "has_more": {
            "type": "boolean",
            "description": "Есть ли ещё страницы."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Курсор следующей страницы — передай в параметре `cursor`. `null`, если страниц больше нет."
          }
        }
      },
      "EventList": {
        "type": "object",
        "description": "Страница списка событий.",
        "required": [
          "object",
          "data",
          "has_more",
          "next_cursor"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list",
            "description": "Тип ресурса, всегда `list`."
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            },
            "description": "Элементы страницы, новые первыми."
          },
          "has_more": {
            "type": "boolean",
            "description": "Есть ли ещё страницы."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Курсор следующей страницы — передай в параметре `cursor`. `null`, если страниц больше нет."
          }
        }
      },
      "WebhookDeliveryList": {
        "type": "object",
        "description": "Список доставок, поставленных в очередь. `has_more` всегда `false`, `next_cursor` — `null`.",
        "required": [
          "object",
          "data",
          "has_more",
          "next_cursor"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "list",
            "description": "Тип ресурса, всегда `list`."
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            },
            "description": "Элементы страницы, новые первыми."
          },
          "has_more": {
            "type": "boolean",
            "description": "Есть ли ещё страницы."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Курсор следующей страницы — передай в параметре `cursor`. `null`, если страниц больше нет."
          }
        }
      }
    }
  }
}