{
  "info": {
    "name": "CalibriPay Client API 1.2.0",
    "description": "Сгенерировано из openapi/client-api.yaml — не редактируйте вручную.\nОбновление: make postman-generate\n\nПеред запуском заполните переменные base_url, access_key и secret_key. Подпись HMAC вычисляется автоматически в pre-request script коллекции.\n\nsecret_key — серверный секрет: не сохраняйте его в общий workspace и не выгружайте коллекцию с заполненным значением.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// ===== Автоподпись каждого запроса (HMAC-SHA256) =====",
          "// Контракт: docs/client-api-full-ru.md, раздел «HMAC-подпись».",
          "// Canonical string — ровно 6 строк, соединённых \\n, без завершающего перевода строки:",
          "//   METHOD \\n PATH_WITH_SORTED_QUERY \\n TIMESTAMP \\n NONCE \\n IDEMPOTENCY_OR_EMPTY \\n SHA256_HEX_OF_RAW_BODY",
          "",
          "// CryptoJS в песочнице Postman/newman уже объявлен глобально;",
          "// require нужен только как fallback для старых версий.",
          "const cryptoJs = (typeof CryptoJS !== 'undefined') ? CryptoJS : require('crypto-js');",
          "",
          "const accessKey = pm.variables.get('access_key');",
          "const secretKey = pm.variables.get('secret_key');",
          "if (!accessKey || !secretKey) {",
          "    throw new Error('Заполните переменные коллекции access_key и secret_key');",
          "}",
          "",
          "// 1. Путь запроса: берём URL после подстановки переменных и отрезаем протокол+хост.",
          "//    В подписи участвует ПОЛНЫЙ путь, включая префикс /api/v1 (он входит в base_url).",
          "const resolvedUrl = pm.variables.replaceIn(pm.request.url.toString());",
          "let pathAndQuery = resolvedUrl.replace(/^[a-z]+:\\/\\/[^\\/]+/i, '');",
          "let [path, query] = pathAndQuery.split('?');",
          "if (query) {",
          "    // Query сортируется по ключу (стандартная кодировка уже сделана Postman'ом).",
          "    path = path + '?' + query.split('&').filter(Boolean).sort().join('&');",
          "}",
          "",
          "// 2. Timestamp (Unix seconds, допуск сервера ±300 c) и одноразовый nonce (UUID).",
          "const timestamp = String(Math.floor(Date.now() / 1000));",
          "const nonce = pm.variables.replaceIn('{{$guid}}');",
          "",
          "// 3. Idempotency-Key обязателен для всего, кроме GET/HEAD.",
          "//    Если заголовок уже задан в запросе вручную — уважаем его (нужно для повторов",
          "//    после timeout: повтор ДОЛЖЕН идти с прежним ключом и тем же body).",
          "const method = pm.request.method.toUpperCase();",
          "let idempotency = '';",
          "if (method !== 'GET' && method !== 'HEAD') {",
          "    const existing = pm.request.headers.get('Idempotency-Key');",
          "    idempotency = (existing && existing.indexOf('{{') === -1) ? existing : pm.variables.replaceIn('{{$guid}}');",
          "    pm.request.headers.upsert({ key: 'Idempotency-Key', value: idempotency });",
          "}",
          "",
          "// 4. SHA-256 сырого body. Для GET/запросов без body — хэш пустой строки.",
          "//    Важно: хэш считается от тех же байт, что реально уйдут в сеть,",
          "//    поэтому переменные в body разворачиваем так же, как это сделает Postman.",
          "let rawBody = '';",
          "if (pm.request.body && pm.request.body.mode === 'raw' && pm.request.body.raw) {",
          "    rawBody = pm.variables.replaceIn(pm.request.body.raw);",
          "    // Фиксируем body: в сеть уйдут ровно те байты, что подписаны, — иначе",
          "    // динамические переменные ({{$timestamp}} и т.п.) разрешились бы второй раз при отправке.",
          "    pm.request.body.update(rawBody);",
          "}",
          "const bodyHash = cryptoJs.SHA256(rawBody).toString(cryptoJs.enc.Hex);",
          "",
          "// 5. Canonical string и подпись (lowercase hex).",
          "const canonical = [method, path, timestamp, nonce, idempotency, bodyHash].join('\\n');",
          "const signature = cryptoJs.HmacSHA256(canonical, secretKey).toString(cryptoJs.enc.Hex);",
          "",
          "pm.request.headers.upsert({ key: 'X-Access-Key', value: accessKey });",
          "pm.request.headers.upsert({ key: 'X-Timestamp', value: timestamp });",
          "pm.request.headers.upsert({ key: 'X-Nonce', value: nonce });",
          "pm.request.headers.upsert({ key: 'X-Signature', value: signature });"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "https://api.example.com/api/v1",
      "type": "string"
    },
    {
      "key": "access_key",
      "value": "",
      "type": "string"
    },
    {
      "key": "secret_key",
      "value": "",
      "type": "string"
    },
    {
      "key": "payment_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "subscription_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "payout_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "verification_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "mandate_id",
      "value": "",
      "type": "string"
    },
    {
      "key": "payout_account",
      "value": "",
      "type": "string"
    },
    {
      "key": "card_number",
      "value": "",
      "type": "string"
    },
    {
      "key": "cardholder_name",
      "value": "",
      "type": "string"
    },
    {
      "key": "card_cvv",
      "value": "",
      "type": "string"
    },
    {
      "key": "confirmation_code",
      "value": "",
      "type": "string"
    },
    {
      "key": "bin",
      "value": "411111",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "Платежи",
      "item": [
        {
          "name": "Создать платёжную сессию",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/payment-sessions",
            "description": "Создаёт сессию hosted-checkout и возвращает checkout_url для перенаправления плательщика.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"order_id\": \"order-{{$timestamp}}\",\n  \"amount_minor\": 1001,\n  \"currency\": \"EUR\",\n  \"description\": \"Order {{$timestamp}}\",\n  \"customer\": {\n    \"first_name\": \"Anna\",\n    \"last_name\": \"Schmidt\",\n    \"email\": \"payer@example.com\",\n    \"phone\": \"+49301234567\",\n    \"billing_address\": {\n      \"country\": \"DE\",\n      \"postal_code\": \"10117\",\n      \"city\": \"Berlin\",\n      \"line1\": \"Friedrichstrasse 100\"\n    }\n  },\n  \"return_url\": \"https://merchant.example/return\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Сохраняем идентификаторы, чтобы коллекция проходилась подряд.",
                  "if (pm.response.code >= 200 && pm.response.code < 300) {",
                  "    const json = pm.response.json();",
                  "    if (json.payment_id) pm.collectionVariables.set('payment_id', json.payment_id);",
                  "    if (json.checkout_url) console.log('checkout_url →', json.checkout_url);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Статус платёжной сессии",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/payment-sessions/{{payment_id}}",
            "description": "Состояние сессии до и после оплаты."
          }
        },
        {
          "name": "Прямой платёж (H2H)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/payments/direct",
            "description": "Приём карточных данных на стороне мерчанта. Доступен только при собственной сертификации PCI DSS.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"order_id\": \"order-{{$timestamp}}\",\n  \"amount_minor\": 1001,\n  \"currency\": \"EUR\",\n  \"description\": \"Order {{$timestamp}}\",\n  \"customer\": {\n    \"first_name\": \"Anna\",\n    \"last_name\": \"Schmidt\",\n    \"email\": \"payer@example.com\",\n    \"phone\": \"+49301234567\",\n    \"billing_address\": {\n      \"country\": \"DE\",\n      \"postal_code\": \"10117\",\n      \"city\": \"Berlin\",\n      \"line1\": \"Friedrichstrasse 100\"\n    }\n  },\n  \"return_url\": \"https://merchant.example/return\",\n  \"payment_method\": {\n    \"type\": \"card\",\n    \"card\": {\n      \"number\": \"{{card_number}}\",\n      \"expire_month\": 12,\n      \"expire_year\": 2030,\n      \"cvv\": \"{{card_cvv}}\",\n      \"cardholder\": \"{{cardholder_name}}\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Сохраняем идентификаторы, чтобы коллекция проходилась подряд.",
                  "if (pm.response.code >= 200 && pm.response.code < 300) {",
                  "    const json = pm.response.json();",
                  "    if (json.payment_id) pm.collectionVariables.set('payment_id', json.payment_id);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Статус платежа",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/payments/{{payment_id}}",
            "description": "Единственный авторитетный способ узнать исход операции помимо вебхука."
          }
        },
        {
          "name": "Подтвердить платёж (OTP)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/payments/{{payment_id}}/confirm",
            "description": "Передаёт код подтверждения, когда платёж перешёл в requires_action.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"value\": \"{{confirmation_code}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Списать захолдированное",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/payments/{{payment_id}}/capture",
            "description": "Списывает ранее авторизованную сумму. Доступно, если канал поддерживает раздельный capture.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount_minor\": 1001\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Отменить платёж",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{base_url}}/payments/{{payment_id}}/cancel",
            "description": "Отменяет операцию до списания средств."
          }
        },
        {
          "name": "Список возвратов",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/payments/{{payment_id}}/refunds",
            "description": "Возвраты по конкретному платежу."
          }
        },
        {
          "name": "Создать возврат",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/payments/{{payment_id}}/refunds",
            "description": "Полный или частичный возврат. Меньшая сумма переводит платёж в partially_refunded.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount_minor\": 1001\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Подписки",
      "item": [
        {
          "name": "Создать подписку",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/subscriptions",
            "description": "Рекуррентные списания по расписанию.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"mandate_id\": \"{{mandate_id}}\",\n  \"amount_minor\": 1001,\n  \"currency\": \"EUR\",\n  \"interval_unit\": \"month\",\n  \"interval_count\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Сохраняем идентификаторы, чтобы коллекция проходилась подряд.",
                  "if (pm.response.code >= 200 && pm.response.code < 300) {",
                  "    const json = pm.response.json();",
                  "    if (json.subscription_id) pm.collectionVariables.set('subscription_id', json.subscription_id);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Статус подписки",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/subscriptions/{{subscription_id}}",
            "description": "Текущее состояние подписки и дата следующего списания."
          }
        },
        {
          "name": "Приостановить подписку",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{base_url}}/subscriptions/{{subscription_id}}/pause",
            "description": "Останавливает списания, сохраняя привязку карты."
          }
        },
        {
          "name": "Возобновить подписку",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{base_url}}/subscriptions/{{subscription_id}}/resume",
            "description": "Возвращает подписку в активное состояние."
          }
        },
        {
          "name": "Отменить подписку",
          "request": {
            "method": "POST",
            "header": [],
            "url": "{{base_url}}/subscriptions/{{subscription_id}}/cancel",
            "description": "Необратимо прекращает списания."
          }
        }
      ]
    },
    {
      "name": "Выплаты",
      "item": [
        {
          "name": "Создать выплату",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/payouts",
            "description": "Выплата на карту получателя.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"order_id\": \"order-{{$timestamp}}\",\n  \"amount_minor\": 1001,\n  \"currency\": \"EUR\",\n  \"account\": \"{{payout_account}}\",\n  \"payout_method\": {\n    \"type\": \"card\",\n    \"card\": {\n      \"number\": \"{{card_number}}\",\n      \"expire_month\": 12,\n      \"expire_year\": 2030\n    }\n  },\n  \"description\": \"Order {{$timestamp}}\",\n  \"return_url\": \"https://merchant.example/return\",\n  \"customer\": {\n    \"first_name\": \"Anna\",\n    \"last_name\": \"Schmidt\",\n    \"email\": \"payer@example.com\",\n    \"phone\": \"+49301234567\",\n    \"billing_address\": {\n      \"country\": \"DE\",\n      \"postal_code\": \"10117\",\n      \"city\": \"Berlin\",\n      \"line1\": \"Friedrichstrasse 100\"\n    }\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Сохраняем идентификаторы, чтобы коллекция проходилась подряд.",
                  "if (pm.response.code >= 200 && pm.response.code < 300) {",
                  "    const json = pm.response.json();",
                  "    if (json.payout_id) pm.collectionVariables.set('payout_id', json.payout_id);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Статус выплаты",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/payouts/{{payout_id}}",
            "description": "Текущее состояние выплаты."
          }
        },
        {
          "name": "Подтвердить выплату",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/payouts/{{payout_id}}/confirm",
            "description": "Второй шаг для каналов, требующих подтверждения.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"value\": \"{{confirmation_code}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Карты",
      "item": [
        {
          "name": "Создать верификацию карты",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/card-verifications",
            "description": "Привязка карты без списания — для последующих подписок и выплат.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"order_id\": \"order-{{$timestamp}}\",\n  \"currency\": \"EUR\",\n  \"card\": {\n    \"number\": \"{{card_number}}\",\n    \"expire_month\": 12,\n    \"expire_year\": 2030,\n    \"cvv\": \"{{card_cvv}}\"\n  },\n  \"description\": \"Order {{$timestamp}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Сохраняем идентификаторы, чтобы коллекция проходилась подряд.",
                  "if (pm.response.code >= 200 && pm.response.code < 300) {",
                  "    const json = pm.response.json();",
                  "    if (json.verification_id) pm.collectionVariables.set('verification_id', json.verification_id);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Статус верификации карты",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/card-verifications/{{verification_id}}",
            "description": "Состояние процесса привязки."
          }
        },
        {
          "name": "Подтвердить верификацию карты",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": "{{base_url}}/card-verifications/{{verification_id}}/verify",
            "description": "Передаёт сумму или код, подтверждающие владение картой.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"code\": \"code\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Служебное",
      "item": [
        {
          "name": "Проверка доступности",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/health",
            "description": "Техническая проверка живости API. Не требует подписи."
          }
        }
      ]
    },
    {
      "name": "Справочники",
      "item": [
        {
          "name": "Список каналов оплаты",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/payment-channels",
            "description": "Каналы, доступные вашему ключу, с их валютами и возможностями."
          }
        },
        {
          "name": "Информация о карте по BIN",
          "request": {
            "method": "GET",
            "header": [],
            "url": "{{base_url}}/cards/information?bin={{bin}}",
            "description": "Платёжная система, тип и банк-эмитент по первым цифрам карты."
          }
        }
      ]
    }
  ]
}
