> ## Documentation Index
> Fetch the complete documentation index at: https://api.zenedu.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Курс в боте

> Создайте пошаговый курс в Telegram с уроками, медиа, квизами и заданиями

В этом гайде собирается полноценный курс, который проходит внутри вашего Telegram-бота: уроки с видео и картинками, материалы для печати, квизы, вопросы и задания. Курс ждёт подписчика на каждом шаге и идёт дальше только после того, как он нажмёт кнопку, ответит на вопрос или сдаст задание.

Порядок действий:

1. Создайте продукт
2. Загрузите файлы в Telegram и зарегистрируйте их
3. Создайте шаги
4. Настройте, когда отправляется каждый шаг
5. Включите продукт

## Перед началом

Понадобится:

* **API-токен** — см. [Авторизация](/ru/authentication).
* **ID бота** — из [Список ботов](/ru/api-reference/bots/list).
* **Токен бота из @BotFather** — только для загрузки файлов из скрипта. API его никогда не возвращает (только `token_hint`).
* **Вы запустили бота** — Telegram-аккаунт, привязанный к вашему пользователю Zenedu (владельцу API-токена), должен хотя бы раз нажать **Start** в боте. Эндпоинты файлов проверяют файл, отправляя его в этот чат, и иначе возвращают `409`.
* **Для файлов больше 50 МБ** — либо `api_id` и `api_hash` с [my.telegram.org](https://my.telegram.org), чтобы загружать их из скрипта, **либо** человек, который один раз отправит каждый такой файл боту. См. [Загрузите файлы](#2-загрузите-файлы).

Примеры — это команды для терминала. Сначала задайте переменные; дальше в шагах сказано, какие из полученных ID сохранить в новые переменные (`PRODUCT_ID`, `OFFER_ID`, …):

```bash theme={null}
API=https://app.zenedu.io/api/v1/bot/BOT_ID
TOKEN=YOUR_API_TOKEN
```

<Warning>
  Отправляйте из скриптов обычный `User-Agent`. Запросы со стандартным user agent Python `urllib` блокируются Cloudflare с `403` (error 1010) ещё до API. `curl` и большинство HTTP-библиотек работают как есть.
</Warning>

## 1. Создайте продукт

```bash theme={null}
curl -X POST $API/products \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"name": "Fun geometry for kids", "platform": "bot"}'
```

```json theme={null}
{ "data": { "id": 123, "platform": "bot", "is_active": false, "steps_count": 0, "link": "https://t.me/YourBot?start=…" } }
```

Сохраните `data.id` как `PRODUCT_ID`. `platform` должен быть `bot` — шаги есть только у продуктов для бота. Новый продукт **выключен**, вы включите его на [шаге 5](#5-включите-продукт), когда контент будет готов.

## 2. Загрузите файлы

Вложения шагов хранятся на серверах Telegram. API не принимает загрузку файлов — он принимает Telegram `file_id` файла, **загруженного этим ботом**. Файлы прикрепляются к шагам в два этапа: загрузите файл в Telegram, затем зарегистрируйте его через [Зарегистрировать или запросить файл](/ru/api-reference/telegram-files/register), чтобы получить ID для `telegram_file_ids`.

Загрузить файл в Telegram можно тремя способами. Выбирайте по размеру файла и по тому, что у вас есть:

| Способ | Размер файла | Что нужно | Действие человека |
| - | - | - | - |
| Bot API | до 50 МБ | токен бота | не нужно |
| MTProto | до 2 ГБ | токен бота, `api_id`, `api_hash` | не нужно |
| Отправить файл боту | до 2 ГБ | ничего | отправить файл в Telegram |

<Warning>
  50 МБ — это лимит только загрузки через Bot API. К шагу можно прикрепить файлы до 2 ГБ. Не сжимайте и не перекодируйте медиа, чтобы уложиться в 50 МБ: для больших файлов используйте MTProto или отправьте файл боту. Если у вас нет ни `api_id` и `api_hash`, ни человека, который отправит файл, запросите что-то одно из этого.
</Warning>

Чтобы видео воспроизводились как видео, они должны быть в MPEG4 (`.mp4`, например видео H.264 со звуком AAC). Другие форматы Telegram может сохранить как `document`, и регистрация такого файла как `video` завершится ошибкой `type_mismatch`.

<Tabs>
  <Tab title="Bot API (до 50 МБ)">
    Отправьте файл через Bot API в свой чат с ботом, возьмите его `file_id` и удалите сообщение:

    ```bash theme={null}
    BOT_TOKEN=123456:ABC…   # from @BotFather
    CHAT_ID=123456789       # your Telegram user ID

    curl -s https://api.telegram.org/bot$BOT_TOKEN/sendVideo \
      -F chat_id=$CHAT_ID -F disable_notification=true \
      -F video=@lesson-1.mp4 -F supports_streaming=true -F width=1280 -F height=720 -F duration=18
    # → result.video.file_id, result.message_id

    curl -s https://api.telegram.org/bot$BOT_TOKEN/deleteMessage -d chat_id=$CHAT_ID -d message_id=MESSAGE_ID
    ```

    Для картинок используйте `sendPhoto`, для PDF и других файлов — `sendDocument`. Для видео передавайте `width`, `height`, `duration` и `supports_streaming`, иначе Telegram может сохранить файл как `document`, и регистрация его как `video` завершится ошибкой `type_mismatch`.

    У документов подписчики видят имя загруженного файла. Используйте понятное имя: `-F "document=@sheet.pdf;filename=Coloring page.pdf"`.

    <Tip>
      Не знаете свой `CHAT_ID`? Один раз вызовите [Зарегистрировать или запросить файл](/ru/api-reference/telegram-files/register) с пустым телом — в ответе будет `chat_id`. Бот заодно попросит прислать файл, это сообщение можно проигнорировать.
    </Tip>
  </Tab>

  <Tab title="MTProto (до 2 ГБ)">
    Для файлов больше 50 МБ из скрипта. Bot API отклоняет загрузки больше 50 МБ, а MTProto, клиентский протокол Telegram, — нет. Авторизуйтесь **как бот** (`auth.importBotAuthorization` с токеном бота и вашими `api_id` и `api_hash`), загрузите файл и отправьте его в `CHAT_ID` через `messages.sendMedia`. Так бот может загружать файлы до 2 ГБ.

    MTProto идентифицирует загруженный файл в своём формате, а не как `file_id` из Bot API. Чтобы получить `file_id`, перешлите только что отправленное сообщение через Bot API — ответ его содержит:

    ```bash theme={null}
    curl -s https://api.telegram.org/bot$BOT_TOKEN/forwardMessage \
      -d chat_id=$CHAT_ID -d from_chat_id=$CHAT_ID -d message_id=MESSAGE_ID -d disable_notification=true
    # → result.video.file_id
    ```

    Затем удалите оба сообщения через `deleteMessage`.
  </Tab>

  <Tab title="Отправить боту (до 2 ГБ)">
    Для файлов больше 50 МБ, когда нет `api_id` и `api_hash` или нет даже токена бота. Человек один раз отправляет файл боту, всё остальное остаётся автоматическим:

    1. Вызовите [Зарегистрировать или запросить файл](/ru/api-reference/telegram-files/register) с **пустым телом**. Бот попросит прислать файл (запрос действует 3 часа — см. `expires_at`).
    2. Отправьте файл боту в Telegram.
    3. Файл появится в [Список Telegram-файлов](/ru/api-reference/telegram-files/list).
  </Tab>
</Tabs>

Зарегистрируйте каждый `file_id`:

```bash theme={null}
curl -X POST $API/telegram-files \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"file_id": "BAACAgIAAxkD…", "type": "video"}'
```

```json theme={null}
{ "data": { "id": 123, "type": "video", "name": "lesson-1.mp4", "size": 64657027 } }
```

Сохраните `data.id` каждого файла, например как `VIDEO_FILE_ID` и `IMAGE_FILE_ID`.

API проверяет файл: отправляет его в ваш чат и сразу удаляет сообщение, поэтому в чате может что-то мелькнуть.

| Ответ | Значение |
| - | - |
| `201` | Зарегистрирован. Используйте `data.id` в `telegram_file_ids`. |
| `409` | Пользователь, которому принадлежит API-токен, не запустил бота. |
| `422` `file_id` | Telegram отклонил `file_id` — обычно его загрузил другой бот. |
| `422` `type` | `type` не совпадает с файлом, в сообщении указано, что определил Telegram. |

Зарегистрированные файлы остаются в [Список Telegram-файлов](/ru/api-reference/telegram-files/list), пока вы их не прикрепите. Файлы привязаны к пользователю: вы видите и прикрепляете только свои. Ненужный файл удаляется через [Удалить Telegram-файл](/ru/api-reference/telegram-files/delete).

## 3. Создайте шаги

Шаги создаются через [Создать шаг продукта](/ru/api-reference/products/steps/create). Каждый новый шаг добавляется в конец продукта. Все типы шагов:

| `type` | Что получает подписчик | Дополнительные поля |
| - | - | - |
| `message` | Сообщение | — |
| `lesson` | Урок | — |
| `task` | Задание с кнопкой сдачи (добавляется автоматически) | `task_autocheck` |
| `question` | Вопрос, подписчик отвечает текстом | `settings.subtype`, `settings.question_error_message`, `settings.save_profile` |
| `poll` | Опрос Telegram | `options` |
| `quiz` | Квиз Telegram с одним правильным ответом | `options`, `settings.quiz_explanation` |

Урок с видео и картинкой:

```bash theme={null}
curl -X POST $API/products/$PRODUCT_ID/steps \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
  --data @- <<EOF
{
  "type": "lesson",
  "name": "Lesson 1. Circle",
  "text": "<p>🟡 <strong>Lesson 1. Circle</strong></p><p>Watch the video together. A circle has no corners, so it can roll.</p>",
  "sending_time": {"option": "immediately"},
  "telegram_file_ids": [$VIDEO_FILE_ID, $IMAGE_FILE_ID]
}
EOF
```

В ответе — созданный шаг. Сохраните его `data.id` (здесь как `LESSON_STEP_ID`): ID шагов понадобятся для кнопок и `sending_time`.

<Warning>
  Всегда передавайте `sending_time`. Если его не передать, шаг получит `never` и **никогда не будет отправлен автоматически** — в курсе он пропускается. Все варианты — на [шаге 4](#4-настройте-когда-отправляется-каждый-шаг).
</Warning>

### Текст

`text` — это HTML, такой же, как выдаёт редактор в личном кабинете. `<p>` и `<br>` превращаются в переносы строк. Теги форматирования передаются в Telegram: `<b>`/`<strong>`, `<i>`/`<em>`, `<u>`, `<s>`, `<a href="…">`, `<code>`, `<pre>`, `<blockquote>`, `<tg-spoiler>`. Остальные теги удаляются, а их текст остаётся — например, списки (`<ul>`, `<li>`) превращаются в обычные строки.

Ограничения длины:

* **4096** символов — обычное сообщение.
* **1024** символа — когда у шага есть вложения и `settings.attach_file` равен `true` (текст отправляется как подпись к медиа). С `attach_file: false` (по умолчанию) текст идёт отдельным сообщением после медиа, и действует лимит 4096.
* **300** символов — `poll` и `quiz` (вопрос).

### Вложения

`telegram_file_ids` принимает ID из [шага 2](#2-загрузите-файлы). Несколько вложений отправляются вместе альбомом. Переданный набор **заменяет** вложения шага; чтобы убрать один файл, используйте [Удалить вложение шага](/ru/api-reference/products/steps/delete-attachment).

### Вопросы

```json theme={null}
{
  "type": "question",
  "name": "How many corners does a square have?",
  "text": "<p>Count the corners together and type the answer as a number 👇</p>",
  "settings": {"subtype": "number", "question_error_message": "Type a number, for example: 2"},
  "sending_time": {"option": "stop"}
}
```

`subtype` — одно из `text`, `number`, `email`, `phone`, `name`. Ответ проверяется по нему; при неверном формате подписчик получает `question_error_message` и может попробовать снова. Если его не задать, используется стандартное сообщение на языке бота. `save_profile: true` сохраняет ответ `name`, `email` или `phone` в профиль подписчика.

### Опросы и квизы

```json theme={null}
{
  "type": "quiz",
  "name": "Quiz: triangle corners",
  "text": "<p>How many corners does a triangle have?</p>",
  "options": [{"text": "2"}, {"text": "3", "is_correct": true}, {"text": "4"}],
  "settings": {"quiz_explanation": "Tri-angle — three corners!"},
  "sending_time": {"option": "stop"}
}
```

* 2–12 вариантов, каждый до 100 символов.
* У `quiz` должен быть **ровно один** `is_correct: true`, иначе `422` `options`.
* `quiz_explanation` (до 200 символов) показывается после ответа.
* У `poll` нет правильного ответа.

### Задания

```json theme={null}
{
  "type": "task",
  "name": "Assignment 1. Shape hunt",
  "text": "<p>Find 3 round and 3 square things at home, put them together, and take a photo. Press the button below and send the photo.</p>",
  "task_autocheck": true,
  "sending_time": {"option": "stop"}
}
```

Кнопка сдачи создаётся автоматически с названием на языке бота — не добавляйте её сами. Подписчик нажимает её и отправляет работу в чат.

* `task_autocheck: true` — задание принимается при сдаче, и курс сразу идёт дальше.
* `task_autocheck: false` — задание ждёт проверки в [Список заданий](/ru/api-reference/tasks/list); курс продолжится после того, как вы [примете его](/ru/api-reference/tasks/approve).

## 4. Настройте, когда отправляется каждый шаг

У каждого шага есть `sending_time`. Шаг планируется, когда отправлен предыдущий; первый шаг — когда подписчик получает доступ к продукту. Когда именно, решает `sending_time.option`:

| `option` | Шаг отправляется | Поля |
| - | - | - |
| `immediately` | сразу после предыдущего шага | — |
| `stop` | после действия подписчика на предыдущем шаге | — |
| `duration` | через заданное время | `delay_number`, `delay_type`, необязательно `delay_limit` |
| `time` | в заданное время суток | `delay_time`, необязательно `delay_number`, `delay_limit` |
| `date` | в точную дату и время | `delay_at`, необязательно `skip_past_date` |
| `never` | не отправляется автоматически | — |

Задайте его при создании шага или позже через [Обновить время отправки](/ru/api-reference/products/steps/update-sending-time):

```bash theme={null}
curl -X PATCH $API/products/$PRODUCT_ID/steps/$NEXT_STEP_ID/sending-time \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"sending_time": {"option": "stop"}}'
```

<Note>
  Подписчика ждёт только `stop`. С любым другим вариантом следующий шаг планируется сразу после отправки предыдущего — даже если подписчик ещё не ответил на вопрос и не сдал задание. Если у всех шагов `immediately`, весь курс придёт сразу.
</Note>

### Ждать подписчика: `stop`

Что считается действием на предыдущем шаге:

| Предыдущий шаг | Следующий шаг со `stop` отправляется, когда подписчик… |
| - | - |
| `question`, `quiz`, `poll` | отвечает |
| `task` | сдаёт задание (с `task_autocheck`) или вы его принимаете |
| любой шаг с кнопкой `send_next_step` | нажимает кнопку |

У `message` и `lesson` своего действия нет — добавьте им кнопку:

```bash theme={null}
curl -X POST $API/products/$PRODUCT_ID/steps/$LESSON_STEP_ID/buttons \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"type": "action", "name": "✅ Watched! Next", "actions": [{"name": "send_next_step"}]}'
```

<Warning>
  `stop` принимается, только если у предыдущего шага уже есть действие. Иначе запрос завершится ошибкой `422` `sending_time` — «Select the action in the previous step». Первый шаг продукта не может быть `stop`.
</Warning>

Поэтому порядок важен. Либо:

* создавайте шаги по одному: после каждого `message` или `lesson` добавьте ему кнопку, затем создайте следующий шаг со `stop`; либо
* создайте все шаги с `immediately`, добавьте кнопки, затем переключите шаги 2…N на `stop` через [Обновить время отправки](/ru/api-reference/products/steps/update-sending-time).

Типичный курс:

| # | Шаг | `sending_time` | Идёт дальше, когда |
| - | - | - | - |
| 1 | `message` Приветствие + кнопка «🚀 Начать» | `immediately` | кнопка |
| 2 | `question` (`name`) Знакомство | `stop` | ответ |
| 3 | `lesson` Урок 1 + кнопка «Дальше» | `stop` | кнопка |
| 4 | `quiz` | `stop` | ответ |
| 5 | `message` Материалы для печати + кнопка «Дальше» | `stop` | кнопка |
| 6 | `task` Задание (`task_autocheck`) | `stop` | сдача |
| 7 | `poll` Любимая фигура | `stop` | ответ |
| 8 | `message` Сертификат | `stop` | — |

### Через время: `duration`

```json theme={null}
{"option": "duration", "delay_number": 1, "delay_type": "days"}
```

Задержка отсчитывается от момента отправки предыдущего шага, а для первого шага — от момента получения доступа. `delay_type` — `seconds`, `minutes`, `hours` или `days`.

После текстового `question` есть одно исключение: если подписчик отвечает до окончания задержки, отсчёт начинается заново с момента ответа. Ответы на опросы и квизы и сданные задания на задержку не влияют.

Чтобы сообщения не приходили ночью или в выходные, добавьте окно отправки (`delay_limit`, см. ниже).

### В заданное время суток: `time`

```json theme={null}
{"option": "time", "delay_time": "2026-01-01T10:00:00+02:00", "delay_number": 1}
```

* Из `delay_time` берётся только **время суток**, дата игнорируется. Передавайте его со смещением — см. [Часовой пояс](#часовой-пояс).
* `delay_number` — через сколько дней после отправки предыдущего шага: `0` — в тот же день в это время или на следующий день, если это время уже прошло; `1` — на следующий день; и так далее.
* `delay_limit.weekdays` ограничивает дни: в неразрешённый день шаг переносится на ближайший разрешённый день в то же время. `time_from` и `time_to` для этого варианта игнорируются.

Например, «каждый урок на следующее утро в 10:00» — это `time` с `delay_number: 1` на каждом уроке.

### В дату: `date`

```json theme={null}
{"option": "date", "delay_at": "2026-11-01T10:00:00+02:00", "skip_past_date": false}
```

Шаг отправляется всем подписчикам в один и тот же момент — используйте это для запусков и живых событий. Передавайте `delay_at` со смещением — см. [Часовой пояс](#часовой-пояс).

* При сохранении активного шага `delay_at` должен быть в будущем, иначе `422` `sending_time` — «Change the sending time to a date in the future».
* Подписчик, который дошёл до шага после этой даты, получает его сразу. С `skip_past_date: true` шаг вместо этого пропускается, и подписчик переходит к следующему.

### Окно отправки: `delay_limit`

Ограничивает `duration` определёнными часами и днями (для `time` применяются только дни):

```json theme={null}
{
  "option": "duration", "delay_number": 2, "delay_type": "hours",
  "delay_limit": {"time_from": 540, "time_to": 1260, "weekdays": ["mon", "tue", "wed", "thu", "fri"]}
}
```

* `time_from` и `time_to` — минуты от полуночи в [часовом поясе проекта](#часовой-пояс): `540` — это 09:00, `1260` — 21:00. Учитываются только целые часы: `570` (09:30) работает как 09:00.
* `time_from` больше `time_to` — ночное окно, например `1320`–`360` (22:00–06:00).
* `time_from`, равный `time_to`, означает любое время суток; тогда действуют только `weekdays`.
* `weekdays`: `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun` — хотя бы один.
* Если шаг выпадает за пределы окна, он переносится на ближайший момент, когда окно открыто в разрешённый день.
* Окно «любое время во все семь дней» равнозначно отсутствию окна и не сохраняется.

### Не отправлять автоматически: `never`

Шаг в курсе пропускается: подписчики не получают его после предыдущего шага, и следующий шаг идёт сразу за предыдущим. Это значение по умолчанию, если `sending_time` не передан.

### Часовой пояс

Расписание считается в часовом поясе вашего проекта (в личном кабинете: **Настройки → Проект → Основное → Часовой пояс**; если не задан — UTC): время суток для `time`, а также часы и дни `delay_limit`.

`delay_at` и `delay_time` — значения даты и времени. Значение без смещения читается как UTC, поэтому передавайте смещение часового пояса проекта — например, `10:00:00+02:00` для 10:00 по Киеву зимой. Для `time` время суток остаётся фиксированным в часовом поясе проекта круглый год.

## 5. Включите продукт

```bash theme={null}
curl -X PUT $API/products/$PRODUCT_ID/active \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json"
```

Когда продукт включён, выдайте подписчикам доступ — см. [Выдать доступ к продукту](/ru/guides/product-access).

## Что нужно помнить

* **Язык стандартных текстов.** Кнопка задания, её сообщения и стандартные ошибки вопросов создаются на **языке бота**. Ответы API и сообщения об ошибках всегда на английском.
* **Файлы привязаны к пользователю.** Файлы, зарегистрированные с одним API-токеном, нельзя прикрепить токеном другого пользователя.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.