> ## 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-токен** — див. [Авторизація](/uk/authentication).
* **ID бота** — зі [Список ботів](/uk/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, потім зареєструйте його через [Зареєструвати або запросити файл](/uk/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`? Один раз викличте [Зареєструвати або запросити файл](/uk/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. Викличте [Зареєструвати або запросити файл](/uk/api-reference/telegram-files/register) з **порожнім тілом**. Бот попросить надіслати файл (запит діє 3 години — див. `expires_at`).
    2. Надішліть файл боту в Telegram.
    3. Файл з’явиться в [Список Telegram-файлів](/uk/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-файлів](/uk/api-reference/telegram-files/list), доки ви їх не прикріпите. Файли прив’язані до користувача: ви бачите й прикріплюєте лише свої. Непотрібний файл видаляється через [Видалити Telegram-файл](/uk/api-reference/telegram-files/delete).

## 3. Створіть кроки

Кроки створюються через [Створити крок продукту](/uk/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-завантажте-файли). Кілька вкладень надсилаються разом альбомом. Переданий набір **замінює** вкладення кроку; щоб прибрати один файл, використовуйте [Видалити вкладення кроку](/uk/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` — завдання чекає на перевірку в [Список завдань](/uk/api-reference/tasks/list); курс продовжиться після того, як ви [приймете його](/uk/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` | не надсилається автоматично | — |

Задайте його під час створення кроку або пізніше через [Оновити час надсилання](/uk/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` через [Оновити час надсилання](/uk/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"
```

Коли продукт увімкнений, видайте підписникам доступ — див. [Видати доступ до продукту](/uk/guides/product-access).

## Що потрібно пам’ятати

* **Мова стандартних текстів.** Кнопка завдання, її повідомлення та стандартні помилки запитань створюються **мовою бота**. Відповіді API й повідомлення про помилки завжди англійською.
* **Файли прив’язані до користувача.** Файли, зареєстровані з одним API-токеном, не можна прикріпити токеном іншого користувача.


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