# Вебхуки

MoreLogin може надсилати JSON-подію методом POST на ваш HTTPS-ендпоінт відразу після того, як операція з хмарним телефоном або хмарним браузером досягла підсумкового результату, тому опитувати результат більше не потрібно. На команду зберігається один URL зворотного виклику, і Local API та Open API пишуть в один і той самий запис.

## Налаштування URL зворотного виклику

Обидва контури надають ту саму операцію. Open API:

```bash
curl -X POST "https://api.morelogin.com/webhook/config" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'
```

Local API:

```bash
curl -X POST "http://127.0.0.1:40000/api/webhook/config" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'
```

Маршрут Local API обслуговує настільний клієнт, тому потрібен клієнт MoreLogin v2.66.0 або новіший. Ендпоінт Open API від клієнта не залежить.

`teamId` ніколи не читається із запиту. Кожен виклик налаштовує команду із серверного контексту, тому спрямувати події іншої команди на свій ендпоінт не вийде.

URL має бути HTTPS, довжиною від 1 до 1024 символів, з іменем хоста, без даних користувача, без фрагмента та з коректним портом. Усі адреси, у які він розв'язується, повинні маршрутизуватися в інтернеті: діапазони localhost, приватні, link-local, CGNAT, метаданих хмари, зарезервовані протоколом, документаційні та 6to4 відхиляються з кодом [`41001`](/uk/api-reference/getting-started/error-codes). Та сама перевірка повторюється в момент, коли доставка відкриває з'єднання, тому імʼя хоста, яке згодом розв'яжеться у приватний діапазон, призведе до невдачі цієї доставки, а не до звернення у вашу мережу.

Збереження — це upsert за командою, тому надіслати те саме тіло двічі безпечно.

### Вимкнення доставки

`enabled: false` зберігає URL і припиняє створення нових подій. Уже поставлені в чергу події скасовуються по одній, у той момент, коли кожну з них має бути авторизовано, і лише поки конфігурація все ще вимкнена — тому при швидкому повторному ввімкненні події з черги продовжать доставлятися, а не будуть відкинуті.

## Як виглядає доставка

Кожна доставка — це `POST` із такими заголовками:

| Заголовок | Значення |
|  --- | --- |
| `Content-Type` | `application/json` |
| `User-Agent` | `MoreLogin-Webhook/1.0` |
| `X-Webhook-Event-Id` | Те саме, що `eventId` у тілі |
| `X-Webhook-Event-Type` | Те саме, що `eventType` у тілі |


Два заголовки `X-Webhook-*` навмисно дублюють поля тіла, щоб маршрутизатор або черга могли розподіляти запити за ними без розбору JSON.

```json
{
  "eventId": "c7f3a2b10d8e4f6a9c1b2d3e4f5a6b7c",
  "eventType": "cloud_phone.power_off",
  "eventTime": "1787241600000",
  "data": {
    "teamId": "10001",
    "cloudPhoneId": "20001",
    "reason": "MONEY_SAVING"
  },
  "result": {
    "success": true,
    "code": "0",
    "message": null
  }
}
```

| Поле | Опис |
|  --- | --- |
| `eventId` | Унікальний ID події. Повтори та внутрішні відтворення тієї самої події використовують його повторно, тому це ключ ідемпотентності |
| `eventType` | Стабільна назва події з таблиці нижче |
| `eventTime` | Коли настав підсумковий результат, у мілісекундах епохи Unix. **Передається рядком** |
| `data.teamId` | Команда, якій належить ресурс. **Передається рядком** |
| `data.cloudPhoneId` | Хмарний телефон, до якого стосується подія. **Передається рядком** |
| `data.cloudBrowserId` | Профіль хмарного браузера, до якого стосується подія. **Передається рядком** |
| `data.runId` | Екземпляр запуску хмарного браузера, до якого стосується подія |
| `data.reason` | Лише у `cloud_phone.power_off` та `cloud_browser.stop` |
| `result.success` | Чи завершилася операція успішно |
| `result.code` | `0` у разі успіху; інакше стабільний бізнес-код помилки, що не залежить від мови |
| `result.message` | `null` у разі успіху; інакше пояснення мовою команди з переходом на `en-US` |


Усі числові ідентифікатори та `eventTime` надходять рядками JSON, як і в решті API — див. [Загальний формат відповіді](/uk/api-reference/getting-started/response-format). Розбирайте їх як рядки, а не як числа, інакше втратите точність на 64-бітних ID.

## Події

| `eventType` | Підсумковий результат, про який повідомляється |
|  --- | --- |
| `cloud_phone.power_on` | Увімкнення вдалося або не вдалося |
| `cloud_phone.power_off` | Вимкнення вдалося. Невдале вимкнення події не створює |
| `cloud_phone.restart` | Перезапуск удався або не вдався |
| `cloud_phone.reset` | Скидання вдалося або не вдалося |
| `cloud_phone.new_machine` | Новий пристрій одним клацанням удався або не вдався |
| `cloud_browser.start` | Запуск хмарного браузера вдався або не вдався |
| `cloud_browser.stop` | Кожна спроба зупинки хмарного браузера, успішна чи ні |


Події хмарного телефона містять `teamId` та `cloudPhoneId`. Події хмарного браузера містять `teamId`, `cloudBrowserId` і `runId`.

Увімкнення, перезапуск, скидання та створення нового пристрою одним клацанням повідомляють `success: true` лише після того, як зворотний виклик постачальника був успішним і локальна синхронізація зафіксована. Помилка списання коштів або асинхронна перевірка проксі, яка завершилася невдало та довела до кінця своє очищення, повідомляють про невдале увімкнення і **не** створюють додатково подію вимкнення. Відкладені задачі, наприклад встановлення попередньо визначених застосунків або видалення застарілих файлів, результат не змінюють.

Якщо вимкнення перервало завантаження, ця спроба не створює події увімкнення; успішне вимкнення згодом усе одно створить власне `cloud_phone.power_off`.

### Причини зупинки

`cloud_phone.power_off` додає `data.reason`:

| Значення | Сенс |
|  --- | --- |
| `NORMAL` | Звичайне вимкнення |
| `MONEY_SAVING` | Режим економії коштів |
| `OTHER` | Заборгованість, завершення терміну, обслуговування або інша причина |


`cloud_browser.stop` додає `data.reason`:

| Значення | Сенс |
|  --- | --- |
| `NORMAL` | Звичайне закриття |
| `MONEY_SAVING` | Режим економії коштів |
| `ENERGY_SAVING` | Режим енергозбереження |
| `BROWSER_EXITED` | Браузер завершився неочікувано |
| `OTHER` | Будь-яка інша причина |


Внутрішня помилка архівування змінює лише `result` і ніколи не перезаписує `reason`, зафіксований на початку зупинки.

## Доставка, повтори та порядок

Будь-який `2xx` вважається доставкою. Будь-який інший статус, а також тайм-аути з'єднання та читання і мережеві помилки вважаються невдачею. Тайм-аут з'єднання — 5 секунд, тайм-аут читання — 30 секунд.

Невдала доставка повторюється не більше 6 разів: через 10 секунд, 30 секунд, 60 секунд, 3 хвилини, 10 хвилин і 30 хвилин. Це не більше 7 запитів на одну подію. Після невдачі шостого повтору подія перестає повторюватися автоматично.

Події одного хмарного телефона або одного хмарного браузера доставляються в порядку створення, по одній. Різні ресурси доставляються паралельно, тому не покладайтеся на якийсь порядок між ними.

Повтори однієї події завжди використовують ті самі `eventId`, `eventTime` і тіло. У межових мережевих або відновлювальних ситуаціях та сама подія може бути доставлена кілька разів із незмінними значеннями, тому **дедуплікуйте за `eventId`**.

Для зупинок хмарного браузера одна спроба зупинки дає рівно одну подію. Подальший повтор вручну, примусове звільнення вручну або примусове звільнення, ініційоване системою, — це нова операція, яка отримує новий `eventId`. Дві спроби ніколи не об'єднуються в одну подію.

Операції хмарного телефона визначаються за ID задачі постачальника, `traceId` або стабільною бізнес-позначкою часу. Коли жодного з них немає, сервіс генерує UUID і вважає поточний підсумковий зворотний виклик новою операцією, тому ви отримуєте новий `eventId`, а дві різні операції не зливаються в одну.

## Реалізація отримувача

Повертайте `2xx` відразу після того, як зберегли подію, а реальну роботу виконуйте асинхронно. Тіло відповіді ігнорується, а повільний обробник витратить ваші 30 секунд на читання і відправить подію до розкладу повторів, хоча ви її вже отримали.

Дедуплікуйте за `eventId`. Вважайте повтор уже обробленим, а не відтворюйте побічні ефекти знову.

Не вважайте корисне навантаження довіреним вводом лише тому, що воно надійшло на ваш ендпоінт. У цьому випуску зворотні виклики не підписуються, тому надіслати запит може будь-хто, хто дізнався ваш URL. Використовуйте URL, який важко вгадати, і перш ніж виконувати щось дороге, підтвердьте стан через API — наприклад [`POST /cloudphone/info`](/uk/api-reference/cloud-phone/open-api) для хмарного телефона або `POST /cloudbrowser/page` для запуску хмарного браузера.

`result.code` використовує ті самі бізнес-коди, що й синхронний API, тому невдачу можна знайти в [Коди помилок](/uk/api-reference/getting-started/error-codes). `result.message` — читабельний для людини рядок, формулювання та мова якого можуть змінюватися; розгалужуйтеся за `result.success` і `result.code`, але ніколи за `result.message`.

Машинно-читний контракт кожної події, включно зі схемами та прикладами по подіях, публікується разом зі специфікацією спільних ресурсів: [Shared Resources Open API](/uk/api-reference/shared-resources/open-api).