{
  "openapi": "3.1.0",
  "info": {
    "title": "TabPay API",
    "version": "1.0.0",
    "description": "Приём платежей в РФ (СБП и банковские карты). Мерчант создаёт платёж, отправляет покупателя на платёжную страницу TabPay (payUrl), результат получает подписанным вебхуком. Все суммы - целые числа в копейках. Документация: https://tabpay.org/docs",
    "contact": {
      "email": "contact@tabpay.org",
      "url": "https://tabpay.org/docs"
    }
  },
  "servers": [
    {
      "url": "https://tabpay.org/api"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/v1/payments": {
      "post": {
        "operationId": "createPayment",
        "summary": "Создание платежа",
        "description": "Создаёт платёж и возвращает ссылку на платёжную страницу (payUrl). Деньги не списываются: оплата происходит, когда покупатель открывает payUrl. Повторный запрос с тем же orderId вернёт 409 - платежи не задваиваются.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Платёж создан",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Платёж с таким orderId уже существует, либо способ оплаты недоступен магазину",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "get": {
        "operationId": "getPayments",
        "summary": "Платёж по номеру заказа или список платежей за период",
        "description": "Два режима. С параметром orderId - поиск ОДНОГО платежа по номеру заказа: в ответе объект платежа (не массив) или 404. Без orderId - страница списка платежей магазина с фильтрами по периоду (createdAt, UTC) и статусам; внутри страницы платежи от старых к новым, страницы стабильны. Список предназначен для сверки и отчётности.",
        "parameters": [
          {
            "name": "orderId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            },
            "description": "Номер заказа в системе мерчанта; задан - режим поиска одного платежа, остальные параметры игнорируются"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Начало периода по createdAt (ISO 8601, UTC), включительно"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Конец периода по createdAt (ISO 8601, UTC), включительно; from позже to - 400"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Фильтр по статусам, через запятую: например SUCCESS,REFUNDED"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000,
              "default": 1
            },
            "description": "Номер страницы"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "description": "Размер страницы"
          }
        ],
        "responses": {
          "200": {
            "description": "С orderId - объект платежа; без orderId - страница списка",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Payment"
                    },
                    {
                      "$ref": "#/components/schemas/PaymentListPage"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/payments/{id}": {
      "get": {
        "operationId": "getPayment",
        "summary": "Получение платежа по идентификатору TabPay",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Идентификатор платежа в TabPay (поле id)"
          }
        ],
        "responses": {
          "200": {
            "description": "Платёж",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/payments/{id}/cancel": {
      "post": {
        "operationId": "cancelPayment",
        "summary": "Отмена платежа до начала оплаты",
        "description": "Отменяет платёж в статусе CREATED: ссылка payUrl перестаёт принимать оплату и показывает покупателю экран «Счёт отменён», магазину уходит вебхук со статусом CANCELED. Повторная отмена идемпотентна (снова 200, без второго вебхука). Если покупатель уже начал оплату (PENDING) или платёж финален - 409: дождитесь финального статуса. orderId отменённого платежа остаётся занятым - для новой попытки создайте платёж с новым orderId.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Идентификатор платежа в TabPay (поле id)"
          }
        ],
        "responses": {
          "200": {
            "description": "Платёж отменён (или уже был отменён)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Отмена невозможна: покупатель уже начал оплату либо платёж в финальном статусе",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/balance": {
      "get": {
        "operationId": "getBalance",
        "summary": "Баланс мерчанта",
        "description": "Те же цифры, что в разделе «Баланс» кабинета. Баланс считается по всему аккаунту владельца магазина (все его магазины и выплаты вместе) - ключ любого магазина аккаунта вернёт одну и ту же сумму. Поступления дня D замораживаются до holdHour (МСК) дня D+1. Лимит - 60 запросов в минуту по ключу.",
        "responses": {
          "200": {
            "description": "Баланс",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "webhooks": {
    "paymentResult": {
      "post": {
        "operationId": "paymentResultWebhook",
        "summary": "Вебхук о финальном статусе платежа",
        "description": "Отправляется на Webhook URL магазина при каждом переходе платежа в финальный статус (SUCCESS, FAILED, EXPIRED, REFUNDED, CANCELED). Рекомендуемая проверка - схема v2: X-Signature-V2 = HMAC-SHA256 (hex) от строки «{X-Timestamp}.{сырое тело}», ключ - секрет подписи магазина; X-Timestamp свежее 5 минут, иначе это replay. Прежний X-Signature (HMAC от тела) уходит параллельно для совместимости. Подпись проверять по сырым байтам до парсинга JSON. Требуется ответ 2xx в течение 5 секунд, редиректы не принимаются. Повторы при неудаче: через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 24 ч (до 7 попыток); тело неизменно, X-Timestamp и подпись v2 пересчитываются на каждую попытку. Обработка должна быть идемпотентной по паре (id, status); неизвестный статус подтверждайте 200 и логируйте.",
        "parameters": [
          {
            "name": "X-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "description": "HMAC-SHA256 (hex) от сырого тела, ключ - секрет подписи магазина (прежняя схема, для совместимости)"
          },
          {
            "name": "X-Timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            },
            "description": "Unix-время отправки в секундах; проверяйте свежесть (окно 5 минут) для защиты от replay"
          },
          {
            "name": "X-Signature-V2",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "description": "HMAC-SHA256 (hex) от строки «{X-Timestamp}.{сырое тело}», ключ - секрет подписи магазина (рекомендуемая схема)"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Уведомление принято мерчантом (любой 2xx)"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "API-ключ магазина (tp_...). Выпускается в личном кабинете в карточке активного магазина, показывается один раз. Любой сбой авторизации - единый ответ 401."
      }
    },
    "schemas": {
      "CreatePaymentRequest": {
        "type": "object",
        "required": [
          "orderId",
          "amountKopecks"
        ],
        "properties": {
          "orderId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "Номер заказа в системе мерчанта; уникален в рамках магазина",
            "examples": [
              "order-1001"
            ]
          },
          "amountKopecks": {
            "type": "integer",
            "minimum": 100,
            "maximum": 10000000000,
            "description": "Сумма в копейках (19900 = 199,00 RUB)",
            "examples": [
              19900
            ]
          },
          "description": {
            "type": "string",
            "maxLength": 255,
            "description": "Назначение платежа; показывается покупателю"
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 255,
            "description": "Email покупателя"
          },
          "method": {
            "type": "string",
            "enum": [
              "SBP",
              "CARD"
            ],
            "description": "Зафиксировать способ оплаты; не задан - покупатель выберет сам"
          },
          "telegramId": {
            "oneOf": [
              {
                "type": "string",
                "pattern": "^\\d{1,20}$"
              },
              {
                "type": "integer"
              }
            ],
            "description": "Телеграм-ID покупателя (для ботов): вернётся строкой в объекте платежа и в вебхуке. Провайдеру не передаётся.",
            "examples": [
              987654321
            ]
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Произвольные данные мерчанта (объект JSON, до 4 КБ). Провайдеру не передаются, возвращаются эхом в объекте платежа и в вебхуке.",
            "examples": [
              { "productId": 42, "tariff": "month" }
            ]
          },
          "successUrl": {
            "type": "string",
            "maxLength": 300,
            "description": "Ссылка возврата после успешной оплаты (https, t.me или tg://). Переопределяет магазинную; не задана - берётся магазинная."
          },
          "failUrl": {
            "type": "string",
            "maxLength": 300,
            "description": "Ссылка возврата после неудачной оплаты (https, t.me или tg://). Переопределяет магазинную; не задана - берётся магазинная."
          }
        }
      },
      "Payment": {
        "type": "object",
        "required": [
          "id",
          "orderId",
          "status",
          "amountKopecks",
          "commissionKopecks",
          "description",
          "method",
          "telegramId",
          "metadata",
          "successUrl",
          "failUrl",
          "payUrl",
          "isTest",
          "paidAt",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Идентификатор платежа в TabPay"
          },
          "orderId": {
            "type": "string",
            "description": "Номер заказа мерчанта"
          },
          "status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "amountKopecks": {
            "type": "integer",
            "description": "Сумма платежа в копейках"
          },
          "commissionKopecks": {
            "type": "integer",
            "description": "Комиссия TabPay в копейках"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Назначение платежа"
          },
          "method": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "SBP",
              "CARD",
              null
            ],
            "description": "Способ оплаты; null до выбора покупателем"
          },
          "telegramId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Телеграм-ID покупателя из запроса на создание; null, если не передавался"
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Произвольные данные мерчанта из запроса на создание (эхо); null, если не передавались"
          },
          "successUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ссылка возврата после успешной оплаты уровня платежа; null - используется магазинная"
          },
          "failUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ссылка возврата после неудачной оплаты уровня платежа; null - используется магазинная"
          },
          "payUrl": {
            "type": "string",
            "format": "uri",
            "description": "Ссылка на платёжную страницу - отправьте покупателя сюда"
          },
          "isTest": {
            "type": "boolean",
            "description": "Тестовый платёж (магазин-песочница): исход задаётся на платёжной странице, деньги не двигаются, вебхук приходит с test: true"
          },
          "paidAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Момент успешной оплаты; null до оплаты"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Момент создания платежа"
          }
        }
      },
      "PaymentStatus": {
        "type": "string",
        "enum": [
          "CREATED",
          "PENDING",
          "SUCCESS",
          "FAILED",
          "EXPIRED",
          "REFUNDED",
          "CANCELED"
        ],
        "description": "CREATED - создан, оплата не начата; PENDING - покупатель начал оплату (20 минут); SUCCESS - оплачен (финальный); FAILED - отказ (финальный); EXPIRED - время истекло (финальный); REFUNDED - возвращён после SUCCESS (финальный); CANCELED - отменён мерчантом по API до начала оплаты (финальный)"
      },
      "PaymentListPage": {
        "type": "object",
        "required": [
          "items",
          "total",
          "page",
          "pageSize"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Payment"
            },
            "description": "Платежи страницы, от старых к новым"
          },
          "total": {
            "type": "integer",
            "description": "Всего платежей под фильтром"
          },
          "page": {
            "type": "integer",
            "description": "Номер текущей страницы"
          },
          "pageSize": {
            "type": "integer",
            "description": "Размер страницы"
          }
        }
      },
      "Balance": {
        "type": "object",
        "required": [
          "availableKopecks",
          "frozenKopecks",
          "holdHour"
        ],
        "properties": {
          "availableKopecks": {
            "type": "integer",
            "description": "Доступно к выводу, в копейках"
          },
          "frozenKopecks": {
            "type": "integer",
            "description": "Заморожено до отсечки разморозки, в копейках"
          },
          "holdHour": {
            "type": "integer",
            "description": "Час разморозки по Москве: платёж дня D доступен после этого часа дня D+1"
          }
        }
      },
      "WebhookBody": {
        "type": "object",
        "required": [
          "id",
          "orderId",
          "status",
          "amountKopecks",
          "telegramId"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Идентификатор платежа в TabPay"
          },
          "orderId": {
            "type": "string",
            "description": "Номер заказа мерчанта"
          },
          "status": {
            "type": "string",
            "enum": [
              "SUCCESS",
              "FAILED",
              "EXPIRED",
              "REFUNDED",
              "CANCELED"
            ],
            "description": "Финальный статус платежа; неизвестный вашему коду статус подтверждайте 200 и логируйте - набор может расширяться"
          },
          "amountKopecks": {
            "type": "integer",
            "description": "Сумма платежа в копейках"
          },
          "telegramId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Телеграм-ID покупателя, если был передан при создании платежа"
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Произвольные данные мерчанта из запроса на создание (эхо); null, если не передавались"
          },
          "test": {
            "type": "boolean",
            "description": "true - уведомление тестовое: либо от кнопки «Отправить тестовый вебхук» в настройках магазина, либо от платежа магазина-песочницы. В обоих случаях подтвердите ответом 200, но товар не выдавайте. У боевых платежей - false."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "statusCode",
          "message"
        ],
        "properties": {
          "statusCode": {
            "type": "integer"
          },
          "message": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ],
            "description": "Строка или массив строк (при ошибках валидации - все проблемы сразу)"
          },
          "error": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "ValidationError": {
        "description": "Тело запроса не прошло валидацию",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Нет или неверный X-Api-Key, либо магазин не активен",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Платёж не найден или принадлежит другому магазину",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Превышен лимит запросов (600 в минуту на метод по API-ключу)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
