{
  "info": {
    "name": "SMS-хаб Термолэнд",
    "description": "Публичный API хаба. Перед первым запросом задайте переменные окружения base_url и api_key.\n\nКлюч режима test работает против симулятора и денег не тратит; ключ live уходит оператору. Разделение — по ключу, отдельного флага в запросе нет.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {"key": "base_url", "value": "https://api.sms.termdev.ru", "description": "Корень API"},
    {"key": "api_key", "value": "nbh_test_...", "description": "Ключ; показывается один раз при выпуске"},
    {"key": "message_id", "value": "", "description": "Заполняется после отправки"},
    {"key": "endpoint_id", "value": "", "description": "Заполняется после создания подписки"}
  ],
  "auth": {
    "type": "bearer",
    "bearer": [{"key": "token", "value": "{{api_key}}", "type": "string"}]
  },
  "item": [
    {
      "name": "Аккаунт: режим и лимиты",
      "request": {
        "method": "GET",
        "url": {"raw": "{{base_url}}/v1/account", "host": ["{{base_url}}"], "path": ["v1", "account"]},
        "description": "Первый запрос при подключении: показывает режим ключа и действующие лимиты. Числа здесь — правда для вашего аккаунта, в руководстве стоят умолчания."
      }
    },
    {
      "name": "Отправка: одноразовый код",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// Ключ идемпотентности — на ПОПЫТКУ отправки. В Postman он",
              "// генерируется на каждый запуск: здесь каждый запуск и есть",
              "// новое намерение отправить. В коде так делать нельзя — там",
              "// ключ обязан переживать перезапуск процесса.",
              "pm.variables.set('idempotency_key', require('uuid').v4());"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('принято', () => pm.response.to.have.status(202));",
              "// 202 — это НЕ доставка: хаб принял сообщение и отвечает за него",
              "// дальше. Доставка приедет вебхуком либо видна в GET /messages/{id}.",
              "pm.collectionVariables.set('message_id', pm.response.json().id);"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {"key": "Content-Type", "value": "application/json"},
          {"key": "Idempotency-Key", "value": "{{idempotency_key}}"}
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"to\": \"+79000000001\",\n  \"class\": \"otp\",\n  \"template\": \"otp-login\",\n  \"variables\": {\"code\": \"482913\"}\n}"
        },
        "url": {"raw": "{{base_url}}/v1/messages", "host": ["{{base_url}}"], "path": ["v1", "messages"]},
        "description": "Отправка идёт по шаблону: свободного текста в API нет. Номер +79000000001 в режиме test — магический: симулятор доставляет его сразу."
      }
    },
    {
      "name": "Оценка: сколько частей и сколько стоит",
      "request": {
        "method": "POST",
        "header": [{"key": "Content-Type", "value": "application/json"}],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"to\": \"+79012223344\",\n  \"class\": \"service\",\n  \"template\": \"visit-reminder\",\n  \"variables\": {\"time\": \"18:30\"}\n}"
        },
        "url": {"raw": "{{base_url}}/v1/messages/estimate", "host": ["{{base_url}}"], "path": ["v1", "messages", "estimate"]},
        "description": "Считает число частей и цену до отправки. Кириллица кодируется UCS-2 и режется на части по 67 символов — оценка спасает от неожиданного счёта."
      }
    },
    {
      "name": "Пакет: массовая рассылка",
      "request": {
        "method": "POST",
        "header": [
          {"key": "Content-Type", "value": "application/json"},
          {"key": "Idempotency-Key", "value": "{{$guid}}"}
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"messages\": [\n    {\"to\": \"+79012223344\", \"class\": \"service\", \"template\": \"visit-reminder\", \"variables\": {\"time\": \"18:30\"}, \"client_ref\": \"visit-84213\"},\n    {\"to\": \"+79012223355\", \"class\": \"service\", \"template\": \"visit-reminder\", \"variables\": {\"time\": \"19:00\"}, \"client_ref\": \"visit-84214\"}\n  ]\n}"
        },
        "url": {"raw": "{{base_url}}/v1/messages/batch", "host": ["{{base_url}}"], "path": ["v1", "messages", "batch"]},
        "description": "До 1000 элементов. client_ref обязателен у каждого: по нему повтор пакета возвращает duplicate вместо второго сообщения. Отказ одного элемента не отменяет остальные."
      }
    },
    {
      "name": "Состояние сообщения",
      "request": {
        "method": "GET",
        "url": {
          "raw": "{{base_url}}/v1/messages/{{message_id}}",
          "host": ["{{base_url}}"],
          "path": ["v1", "messages", "{{message_id}}"]
        },
        "description": "Опрос — способ догнать пропущенное, а не основной путь. Подпишитесь на вебхуки, и события приедут сами."
      }
    },
    {
      "name": "Подписка на события",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const body = pm.response.json();",
              "pm.collectionVariables.set('endpoint_id', body.id);",
              "// Секрет подписи показывается ОДИН раз: в базе лежит",
              "// зашифрованное значение, повторить показ невозможно.",
              "console.log('секрет подписи (сохраните сейчас):', body.secret);"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [{"key": "Content-Type", "value": "application/json"}],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"url\": \"https://example.org/hooks/sms\",\n  \"event_types\": [\"message.delivered\", \"message.undelivered\", \"message.expired\"]\n}"
        },
        "url": {"raw": "{{base_url}}/v1/webhook_endpoints", "host": ["{{base_url}}"], "path": ["v1", "webhook_endpoints"]},
        "description": "Только https и только публичный адрес: имена, разрешающиеся в частные сети, отвергаются на записи."
      }
    },
    {
      "name": "Пробная доставка",
      "request": {
        "method": "POST",
        "url": {
          "raw": "{{base_url}}/v1/webhook_endpoints/{{endpoint_id}}/test",
          "host": ["{{base_url}}"],
          "path": ["v1", "webhook_endpoints", "{{endpoint_id}}", "test"]
        },
        "description": "Отправляет на ваш адрес событие webhook.ping — так проверяется, что приёмник поднялся и подпись у вас сходится, до первого настоящего события."
      }
    },
    {
      "name": "Журнал попыток доставки",
      "request": {
        "method": "GET",
        "url": {
          "raw": "{{base_url}}/v1/webhook_endpoints/{{endpoint_id}}/deliveries",
          "host": ["{{base_url}}"],
          "path": ["v1", "webhook_endpoints", "{{endpoint_id}}", "deliveries"]
        },
        "description": "Код ответа, длительность и время следующей попытки по каждой доставке. Первое место, куда смотреть, когда «вебхуки не приходят»."
      }
    },
    {
      "name": "Ротация секрета подписи",
      "request": {
        "method": "POST",
        "url": {
          "raw": "{{base_url}}/v1/webhook_endpoints/{{endpoint_id}}/rotate_secret",
          "host": ["{{base_url}}"],
          "path": ["v1", "webhook_endpoints", "{{endpoint_id}}", "rotate_secret"]
        },
        "description": "Новый секрет (снова один раз) и переходный период — сутки по умолчанию, — в который хаб подписывает обоими. Порядок: получить новый → научить приёмник принимать оба → дождаться конца периода → убрать старый."
      }
    },
    {
      "name": "События: догнать пропущенное",
      "request": {
        "method": "GET",
        "url": {
          "raw": "{{base_url}}/v1/events?limit=100",
          "host": ["{{base_url}}"],
          "path": ["v1", "events"],
          "query": [
            {"key": "limit", "value": "100"},
            {"key": "starting_after", "value": "evt_...", "disabled": true}
          ]
        },
        "description": "Журнал событий — источник правды, он не зависит от доставки. После простоя приёмника пропущенное догоняется отсюда по курсору."
      }
    },
    {
      "name": "Шаблоны",
      "request": {
        "method": "GET",
        "url": {"raw": "{{base_url}}/v1/templates", "host": ["{{base_url}}"], "path": ["v1", "templates"]},
        "description": "Отправка возможна только по шаблону в статусе active. Заведённый шаблон проходит модерацию у оператора — это не мгновенно."
      }
    },
    {
      "name": "Магические номера песочницы",
      "request": {
        "method": "GET",
        "url": {"raw": "{{base_url}}/v1/sandbox/magic_numbers", "host": ["{{base_url}}"], "path": ["v1", "sandbox", "magic_numbers"]},
        "description": "Номера, на которых симулятор разыгрывает конкретный исход: доставку, отказ оператора, молчание. Список отдаёт сам хаб — он же источник правды для тестов."
      }
    }
  ]
}
