---
title: "Webhook: бот как HTTP-сервер"
description: "Webhook в telegram-боте на Go: SetWebhook, ListenForWebhook с токеном в пути, HTTPS-сертификат, конфликт 409 с long polling и сравнение двух подходов."
course: "Telegram-боты на Go: от эха до полноценного сервиса"
url: "https://golangmaster.ru/courses/telegram-bot/lessons/18-webhook-bot/"
published: "2026-09-19"
updated: "2026-09-25"
minutes: 20
---

# Webhook: бот как HTTP-сервер

*Курс «Telegram-боты на Go: от эха до полноценного сервиса», урок 18 из 25. ~20 минут. Обновлено 25.09.2026. Источник: https://golangmaster.ru/courses/telegram-bot/lessons/18-webhook-bot/*

Webhook переводит telegram-бота из режима опроса в режим сервиса: Telegram сам приносит обновления POST-запросами на ваш HTTPS-адрес, и бот работает как обычный HTTP-сервер на net/http. Разберём SetWebhook и ListenForWebhook в tgbotapi, токен как секрет пути, серты и порты, конфликт с long polling и честное сравнение двух способов получать апдейты.

Все пятнадцать предыдущих уроков бот жил на ноутбуке и работал в режиме long polling: цикл сам ходил в Telegram за обновлениями и ждал. Это удобно на старте, но у модели есть цена, которая становится заметной на сервере: постоянные исходящие запросы, задержка между событием и обработкой, невозможность масштабировать на два инстанса — апдейты уйдут в одного из поллингов, и никто не знает в какого. Webhook переворачивает стрелку: вы один раз говорите Telegram свой HTTPS-адрес командой setWebhook, и с этого момента каждое событие — сообщение, нажатие кнопки, inline-запрос — приезжает к вам POST-запросом с JSON-апдейтом в теле. Бот становится обычным HTTP-сервисом: порт, хендлер, nginx при желании, горизонтальное масштабирование за балансировщиком. Хорошая новость для тех, кто прошёл первые уроки: цикл обработки апдейтов не меняется вообще — меняется только источник, из которого цикл читает. В этом уроке разберём настройку на Go, подводный камень с включённым polling, серты и порты, отладку без Telegram и путь переезда на сервер.

### Как устроен webhook

Механика двухшаговая. Сначала бот сообщает Telegram адрес: конфигурация NewWebhook с HTTPS-URL передаётся методом Request — это отправка setWebhook на сервер. Дальше Telegram на каждое событие шлёт POST на этот адрес, и телом запроса лежит тот самый JSON-апдейт, который long polling получал в канале. Адрес обязан быть HTTPS: сервер отклонит http-URL ошибкой про то, что для вебхука требуется HTTPS. Сертификат подходит любой из двух: валидный от доверенного центра вроде Let's Encrypt или самоподписанный, который прикладывается к setWebhook через NewWebhookWithCert. Путь в URL — это ваш единственный барьер от подделок, и стандартная практика — использовать сам токен бота: Telegram знает полный адрес, посторонний не угадает, и каждый входящий запрос можно доверять по факту пути. Порт тоже ограничен: документация называет 443, 80, 88 и 8443 — слушать сервером что-то другое Telegram откажется.

### Бот-сервер целиком

```go
func main() {
        bot, err := tgbotapi.NewBotAPI(os.Getenv("BOT_TOKEN"))
        if err != nil {
                log.Fatal(err)
        }

        wh := tgbotapi.NewWebhook("https://example.com/" + bot.Token)
        if _, err := bot.Request(wh); err != nil {
                log.Fatal(err)
        }

        updates := bot.ListenForWebhook("/" + bot.Token)
        go http.ListenAndServe("0.0.0.0:8080", nil)

        for update := range updates {
                if update.Message != nil {
                        log.Println(update.Message.Text)
                }
        }
}
```

Три сущности: конфигурация NewWebhook уходит методом Request — это и есть setWebhook, затем ListenForWebhook регистрирует приёмник по пути с токеном и возвращает канал, а HTTP-сервер поднимается в горутине. Main занят привычным циклом range по апдейтам — тот же код, что работал с long polling, читает новый источник.

### ListenForWebhook под капотом

Метод ListenForWebhook делает ровно две вещи и обе важны для понимания. Первая: он регистрирует HTTP-хендлер на переданном пути в стандартном роутере net/http — каждый POST на этот путь будет разобран. Вторая: в хендлере тело запроса декодируется из JSON в структуру tgbotapi.Update и отправляется в буферизованный канал, который метод возвращает вызывающему. Отсюда два практических правила. Первое — порядок строк: ListenForWebhook вызывается до старта HTTP-сервера, иначе сервер начнёт принимать запросы раньше, чем хендлер зарегистрирован, и первые апдейты упадут в 404. Второе — цикл range из уроков про polling работает без единой правки: источник сменился, интерфейс остался, и весь код обработки сообщений, callback и inline-запросов переезжает как есть. Буфер канала сглаживает всплески: Telegram шлёт апдейты параллельно и не ждёт, пока ваш цикл до них дойдёт, — при длительном заторе сервер начнёт возвращать ошибки, и Telegram будет повторять доставку.

> **409 Conflict: polling и webhook не соседствуют.** Пока на боте установлен вебхук, запросы GetUpdates отклоняются ошибкой Conflict: terminated by other getUpdates request; make sure that only one bot instance is running. Типичный сценарий новичка: на сервере поднят webhook, а на ноутбуке ещё крутится старый polling-процесс — и оба выглядят сломанными: один получает Conflict, второй тише молчит. Telegram отдаёт апдейты только одному получателю. Лечение: оставить один способ получения, а перед возвратом к polling снять вебхук командой DeleteWebhook. Правило на память: один бот — один источник апдейтов, и второй процесс — это не резерв, а конкуренты за одни и те же сообщения.

### HTTPS, порты и локальная отладка

Главное неудобство webhook — проверка живьём требует публичного HTTPS-адреса, которого у локальной разработки нет. Рабочие варианты три. Первый — туннель: ngrok или ssh-туннель до сервера дают временный HTTPS-адрес, который на время отладки скармливается NewWebhook. Второй — curl: Telegram шлёт обычный POST с JSON, и тот же запрос можно сделать руками на localhost — хендлер получит апдейт и канал отработает, а Telegram для этого не нужен. Третий — сразу деплой на тестовый VPS с нормальным сертификатом, и здесь помогает связка с уроком про деплой. Токен в пути стоит держать сквозным: от Telegram до вашего хендлера, через nginx, если он есть, — тогда проверка подлинности остаётся в одном месте. Для продакшена конфигурация уточняется: AllowedUpdates срезает ненужные типы апдейтов ещё на стороне Telegram, а MaxConnections ограничивает параллельные соединения доставки.

### Уточнённая настройка и сервер

```go
wh := tgbotapi.WebhookConfig{
        URL:            "https://example.com/" + bot.Token,
        AllowedUpdates: []string{"message", "callback_query"},
}
if _, err := bot.Request(wh); err != nil {
        log.Fatal(err)
}

server := &http.Server{
        Addr:         "127.0.0.1:8080",
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
}

updates := bot.ListenForWebhook("/" + bot.Token)
go server.ListenAndServe()
```

Вместо конструктора здесь собрана структура WebhookConfig: AllowedUpdates говорит Telegram присылать только сообщения и нажатия, остальная почта отрезается до отправки. Сервер — не голый ListenAndServe, а http.Server с таймаутами на чтение и запись: webhook-доставка это короткие POST, и таймауты защищают от висящих соединений.

### Кейс: переезд бота на VPS с nginx

Типовой переезд выглядит так. На VPS ставится nginx, который завершает TLS на порту 443 с сертификатом Let's Encrypt, и проксирует запросы на внутренний адрес 127.0.0.1:8080, где живёт Go-сервер бота. NewWebhook получает внешний адрес вида https://bot.example.com/ с токеном в пути, nginx передаёт его как есть, и проверка подлинности остаётся в коде бота. Бот запускается юнитом systemd — настройкам автозапуска посвящён отдельный урок про деплой, — и после каждого рестарта setWebhook заново отправлять не нужно: Telegram хранит адрес до смены. Первые сутки после переезда стоит смотреть два лога: access-лог nginx на предмет посторонних запросов к пути с токеном — их не должно быть, а единичные можно молча отдавать 403 — и лог бота на предмет всплесков. Ежедневные апдейты приезжают параллельно, без длинных ожиданий, и латентность ответа бота падает с сотен миллисекунд polling до единиц миллисекунд доставки.

### Возврат к polling

```go
// снять вебхук и вернуться к long polling
if _, err := bot.Request(tgbotapi.DeleteWebhookConfig{}); err != nil {
        log.Fatal(err)
}

u := tgbotapi.NewUpdate(0)
u.Timeout = 30
updates := bot.GetUpdatesChan(u)
```

DeleteWebhookConfig снимает регистрацию адреса, и апдейты снова накапливаются на сервере Telegram до запроса GetUpdates. Дальше включается привычный канал из пятого урока: NewUpdate с офсетом и тайм-аутом, GetUpdatesChan и тот же цикл. Переключение между режимами стоит несколько строк, если код обработки вынесен отдельно от источника.

### Когда polling всё ещё лучше

Переехав на webhook, честно признать и обратную сторону. Пока бот живёт на ноутбуке, за NAT или в песочнице, у вас нет публичного HTTPS-адреса — и polling остаётся единственным способом получать апдейты без обвязки туннелями. Для отладки polling удобнее ещё и потому, что апдейты накапливаются на стороне Telegram: выключили процесс на ночь, включили утром — очередь пришла вся, а с вебхуком часть событий Telegram повторит, а часть отбросит. Небольшому боту на сотню пользователей вебхук ничего не даёт, кроме обязательного серта и лишнего компонента в схеме. Мой рабочий порог — переезд на сервер и аудитория в тысячи чатов: раньше этого polling не мешал никому, и половина продовых ботов спокойно живёт на нём годами. Переключение стоит пять строк, если источник апдейтов вынесен за обработку, так что решение не финальное и легко пересматривается в обе стороны.

### Long polling против webhook

| Параметр | Long polling | Webhook |
| --- | --- | --- |
| Кто инициирует | Бот ходит за апдейтами | Telegram приносит POST |
| Адрес | Не нужен, работает из ноутбука | Публичный HTTPS с сертом |
| Порты | Любые | 443, 80, 88, 8443 |
| Отладка | Проще: ничего кроме токена | Туннель или curl руками |
| Масштабирование | Один поллер на бота | Несколько инстансов за балансировщиком |
| Когда выбирать | Разработка, первые проекты | Прод, аудитория, интеграции |
| Повтор доставки | Очередь копится на стороне Telegram | Повторы несколько часов, потом отброс |

> **Апдейт руками: тест без Telegram.** Сохраните образец JSON-апдейта из debug-лога и подсовывайте его хендлеру через curl: curl -X POST localhost:8080/секретный-путь -d @update.json. Канал отработает как настоящий, и логику обработки можно гонять без Telegram вообще. Тот же приём ложится в юнит-тесты из урока про тестирование бота — HTTP-хендлер проверяется стандартными инструментами. Для ускорения эксперимента держите второй терминал с хвостом лога: каждый POST виден сразу, и опечатка в пути отлавливается за секунды.

### Проверка: что делает ListenForWebhook

**Вопрос:** Что возвращает метод bot.ListenForWebhook("/" + token)?

- Срез последних сообщений
- ✅ Канал апдейтов, в который пишет зарегистрированный HTTP-хендлер
- Настроенный HTTPS-сертификат
- Ответ Telegram на setWebhook

ListenForWebhook регистрирует хендлер на указанном пути и возвращает канал, в который декодированные из POST-запросов апдейты пишет сам хендлер. Цикл range читает этот канал так же, как читал канал от GetUpdates при polling.

### Проверка: зачем токен в пути

**Вопрос:** Зачем в путь вебхука кладут токен бота?

- Для красивого лога
- ✅ Путь работает как общий секрет: подделать POST без токена нельзя
- Telegram требует токен в доменном имени
- Так быстрее работает балансировщик

Telegram знает полный адрес из setWebhook и шлёт запросы только на него; посторонний, не знающий токена, не угадает путь. Проверка подлинности сводится к факту совпадения пути, и дополнительной подписи запросов не требуется.

### Что ответит Telegram на http-адрес

```go
wh := tgbotapi.NewWebhook("http://example.com/hook")
_, err := bot.Request(wh)
fmt.Println(err)
```

**Что выведет программа?**

- nil — примет любой адрес
- ✅ Ошибка: для вебхука требуется HTTPS
- Апдейты поедут по http
- 429 Too Many Requests

setWebhook принимает только HTTPS-URL, и http-адрес отклоняется ошибкой с упоминанием HTTPS url. Лечение — валидный сертификат либо самоподписанный через NewWebhookWithCert, а для локальной проверки — туннель с HTTPS или отладка curl без Telegram.

### Соберите включение вебхука

Впишите конструктор, поле токена и методы регистрации.

```go
wh := tgbotapi.___("https://example.com/" + bot.___)
bot.___(wh)
updates := bot.___("/" + bot.Token)
```

Подсказка: Адрес собирает NewWebhook, токен лежит в bot.Token, конфигурация уходит через bot.Request, а приёмник возвращает ListenForWebhook.

Правильные ответы в пропусках: `NewWebhook`, `Token`, `Request`, `ListenForWebhook`

### Шесть строк перехода на webhook

Расставьте запуск бота-сервера: от создания клиента до цикла апдейтов.

Правильный порядок строк:

1. `bot, err := tgbotapi.NewBotAPI(token)`
2. `wh := tgbotapi.NewWebhook(url)`
3. `_, err = bot.Request(wh)`
4. `updates := bot.ListenForWebhook("/" + bot.Token)`
5. `go http.ListenAndServe("0.0.0.0:8080", nil)`
6. `for update := range updates {`

### Карточки: webhook

- **setWebhook / NewWebhook** — Регистрация HTTPS-адреса доставки: Telegram начинает слать апдейты POST-запросами. Конфигурация уходит через bot.Request, не через Send.
- **ListenForWebhook** — Метод библиотеки: регистрирует хендлер на пути и возвращает канал апдейтов. Вызывается до старта HTTP-сервера.
- **Токен в пути** — Стандартный секрет вебхука: Telegram шлёт на полный адрес, посторонний путь не угадает. Проверка подлинности — факт совпадения пути.
- **Порты вебхука** — Документация называет 443, 80, 88 и 8443. Другие порты Telegram не примет.
- **409 Conflict** — Ошибка GetUpdates при включённом вебхуке: апдейты получает только один источник. Возврат к polling — через DeleteWebhook.
- **AllowedUpdates** — Поле WebhookConfig: срез типов апдейтов, которые Telegram вообще будет присылать. Меньше шума до вашего сервера.

Бот стал HTTP-сервисом, и дальше маршрут раздваивается. Если остаётесь в режиме приёма, следующий шаг — фоновые задачи: [урок про напоминания](/courses/telegram-bot/lessons/19-napominaniya/) добавляет боту горутину с тикером, которая работает независимо от апдейтов. Обслуживание на сервере — сборка и systemd — разобраны в [уроке про деплой](/courses/telegram-bot/lessons/24-deploy-bota/). Откуда вебхук растёт: механика long polling и её ограничения остались в [пятом уроке](/courses/telegram-bot/lessons/05-long-polling/), а обвязка HTTP-серверов на Go с роутерами и middleware — тема [курса по chi](/courses/chi/). Полный маршрут курса — на [его странице](/courses/telegram-bot/).

## Коротко: что запомнить

- Webhook переворачивает направление: не бот ходит за апдейтами, а Telegram приносит их POST-запросами на HTTPS-адрес.
- setWebhook отправляется конфигурацией NewWebhook через bot.Request; ListenForWebhook регистрирует приёмник и возвращает канал.
- Цикл range по апдейтам не меняется: источник сменился, код обработки переезжает как есть.
- Путь с токеном — секрет доставки: посторонний POST не угадает адрес, а порты ограничены 443, 80, 88 и 8443.
- Polling и webhook не работают одновременно: GetUpdates отвечает 409 Conflict, возврат — через DeleteWebhook.
- HTTPS обязателен: валидный сертификат, самоподписанный через NewWebhookWithCert или туннель для отладки.
- AllowedUpdates и http.Server с таймаутами — минимальная прод-настройка приёма.
- Отладка без Telegram: сохранённый JSON-апдейт уходит в хендлер через curl, и канал отрабатывает как настоящий.

## Домашнее задание

Переведите локального бота на webhook с туннелем или проверьте хендлер через curl с сохранённым JSON-апдейтом: NewWebhook с адресом и токеном в пути, Request на setWebhook, ListenForWebhook до старта сервера, http.Server с таймаутами в горутине. Убедитесь, что цикл range обрабатывает апдейты без правок, затем попробуйте запустить рядом polling-процесс и поймайте в логе ошибку 409 Conflict — теперь вы знаете её в лицо. Завершите упражнение возвратом: DeleteWebhookConfig и polling из пятого урока снова работает.

## Вопросы и ответы по уроку

### Нужен ли вебхук, если бот маленький и живёт на ноутбуке?

Нет: для разработки и первых проектов long polling проще — не нужен ни адрес, ни сертификат, ни открытый порт. Вебхук окупается на сервере: прод-аудитория, интеграции, желание масштабироваться и измерять латентность доставки. Переключение туда-обратно стоит несколько строк, если источник апдейтов вынесен за обработку.

### Можно ли слушать произвольный порт, например 3000?

Telegram принимает доставку только на 443, 80, 88 и 8443. Ваше приложение может слушать любой порт внутри — например, 8080 за nginx, — но внешний вход для Telegram должен заканчиваться на одном из разрешённых портов.

### Что происходит с апдейтами, если сервер был недоступен?

Telegram несколько часов повторяет неудавшуюся доставку с отступающими интервалами, потом отбрасывает апдейт. Порядок при этом сохраняется по чату, но не гарантируется глобально. Для критичных к потере задач бот обязан дублировать состояние у себя — очередь в Redis или журнал событий — и не полагаться только на доставку Telegram.
