Серверное API

Эта страница — для того, кто подключает Globask со стороны своего сервера. Она же контракт: всё, что здесь описано, менять без предупреждения нельзя.

Способов ходить друг к другу три, и они независимы:

Часть Кто кого зовёт Зачем
Чтение вы — нас выгрузка переписки, архив в карточке CRM
Отправка вы — нас сообщение посетителю из вашей системы
Вебхуки мы — вас события ваших диалогов в реальном времени

Чтение и отправка требуют ключа, вебхуки — нет: они настраиваются в кабинете и подписываются секретом.

Ключи

Ключ выпускается в кабинете: проект → вкладка «Ключи API». Ключ принадлежит проекту, а не аккаунту: если ключ утечёт, посторонний может получить доступ к переписке только одного сайта, а не всех ваших проектов. У проекта их может быть несколько — по одному на интеграцию.

У ключа три способности, и выдавать их лучше по отдельности:

Способность Что открывает
«Чтение» все GET-запросы ниже
«Отправка» POST сообщения в диалог
«Управление диалогом» взять диалог, передать человеку, заметка, закрыть

Третья отделена от «Отправки» не для строгости: рассылка, умеющая писать посетителям, не должна заодно уметь закрывать их диалоги.

Ключ показывается один раз — сразу после выпуска. Повторно мы его не покажем: в базе хранится не он, а его sha256, и показывать нам просто нечего. Потеряли — отзовите и выпустите новый.

Передаётся он заголовком, и только им:

Authorization: Bearer gsk_ваш_ключ

В адресе ключ не принимается намеренно: адреса оседают в логах прокси, в истории браузера и в чужих системах мониторинга — вместе с ключом.

Все адреса начинаются с https://globask.com/api/v1/. Версия в пути стоит с первого дня: на вашей стороне живёт код, который мы не сможем переписать по своей воле.

Проверить ключ:

curl -H "Authorization: Bearer gsk_…" https://globask.com/api/v1/project
{
    "project": {
        "public_key": "pk_…",
        "name": "Мой сайт",
        "timezone": "Europe/Moscow"
    },
    "token": {
        "name": "Выгрузка в BI",
        "abilities": ["read"],
        "expires_at": null
    }
}

Ошибки

Коды обычные, тело — {"message": "…"}, у ошибок проверки полей ещё и errors.

Код Когда
400 тело не разобралось: оборванный JSON или не UTF-8 — и в JSON, и в форме
401 ключа нет, он неизвестен, отозван или просрочен
403 ключу не хватает способности; аккаунт приостановлен
404 записи нет или она из другого проекта — мы не различаем эти случаи вслух
409 диалог занят другой интеграцией или уже отдан людям — см. «Диалог ведёт бот»
422 параметр не подходит: неизвестный статус, чужой курсор, пустой текст, не та картинка
429 превышен лимит частоты — или запросов, или неудачных попыток предъявить ключ

Про 400 стоит знать заранее. Тело мы разбираем сами и на битом отвечаем прямо: «не разобрано как JSON». Раньше такой запрос получал 422 «Поле body обязательно» — формально верно (полей и правда нет) и практически вредно: ответ отправляет искать ошибку в коде отправки, а не в кодировке. Самая частая причина — не UTF-8: тело собрано в CP1251 или уехало через слой, который его перекодировал.

То же самое проверяется и в форме (multipart/form-data, x-www-form-urlencoded), которой отправляются картинки. Здесь разбор не спотыкается вовсе — значения приходят байтами как есть, — поэтому мы проверяем сами: значения полей, имена полей и имена загружаемых файлов. Не-UTF-8 в любом из них даёт 400 «Поля запроса не в кодировке UTF-8». Отдельно про имя файла: оно сохраняется вместе с вложением, и битое имя сломало бы не эту отправку, а чтение переписки потом.

Про 429 стоит знать две вещи. Лимиты частоты: 120 запросов в минуту на чтение, 60 на отправку и 30 на управление диалогом (взять, сдать, закрыть, заметка). Считаются они на ключ, а не на проект и не на адрес: два ключа дают два бюджета. И второй источник того же кода — 30 неудачных предъявлений ключа в минуту с одного адреса: опечатка в конфиге даёт 429 «Слишком много неудачных попыток», а не 401.

Чтение

Как листать

Списки отдаются от старых записей к новым и листаются курсором:

GET /api/v1/conversations?limit=50&after=<id последней прочитанной записи>
  • limit — по умолчанию 50, потолок 100;
  • next_after в ответе — то, что нужно передать в after за следующей страницей; null означает, что страница последняя;
  • after с неизвестным идентификатором — ошибка 422, а не «начни сначала»: иначе опечатка превратила бы ваш цикл в бесконечную выгрузку одного и того же.

Смещений (page, offset) у нас нет намеренно: пока вы листаете, приходят новые сообщения, страницы съезжают, и записи начинают дублироваться или теряться молча.

Порядок «от старых к новым» не меняется никогда. Если вам нужны свежие события первыми — вам нужны не списки, а вебхуки.

Диалоги

GET /api/v1/conversations
GET /api/v1/conversations/{id}

Фильтры: status (open, pending, closed), contact (идентификатор контакта).

{
    "conversations": [
        {
            "id": "0198f3bf-…",
            "status": "open",
            "led_by": "humans",
            "created_at": "2026-08-25T10:15:30+03:00",
            "last_message_at": "2026-08-25T10:19:02+03:00",
            "contact": {
                "id": "0198f3be-…",
                "number": 5,
                "name": null,
                "email": null
            },
            "identity": {
                "channel": "widget",
                "external_id": "v_…",
                "page_url": "https://shop.example/cart",
                "attributes": { "uid": "7f3a…", "Тариф": "Pro" },
                "created_at": "2026-08-25T10:10:00+03:00"
            }
        }
    ],
    "next_after": null
}

identity — как посетитель пишет в этом диалоге. В attributes лежит то, что ваша страница положила в window.globask.user, — такое, каким оно пришло с последним сообщением посетителя. Атрибутов нет — пустой объект.

Эти значения задаёт браузер посетителя, и подделать их можно из консоли. Показать менеджеру тариф или номер заказа они годятся, а решать по ним, чей аккаунт перед вами, — нет. Как подписать значение на своём сервере, чтобы ему можно было верить, — в документации виджета.

Сообщения диалога

GET /api/v1/conversations/{id}/messages
{
    "messages": [
        {
            "id": "0198f3c0-…",
            "direction": "in",
            "body": "Сколько стоит доставка?",
            "author": { "type": "contact", "via": null, "name": null },
            "content_type": "text",
            "attachments": [],
            "created_at": "2026-08-25T10:15:30+03:00"
        }
    ],
    "next_after": null
}

direction — in от посетителя, out к посетителю. author.type — contact, user (менеджер), unlinked_agent, bot (автоответ или ваша собственная отправка) и system.

content_type бывает трёх значений: text, image (у сообщения есть вложение) и email_prompt — это наша просьба к посетителю оставить почту. Последняя приходит обычным сообщением бота; если строите архив переписки, решите заранее, показывать её или отбрасывать.

author.via отвечает на вопрос «это не я ли сам?». У bot два разных источника: via: "api" — сообщение, которое отправили вы через это же API, via: null — наш автоответ. Различать их нужно всерьёз: если вы подписаны на message.created.out, ваша собственная отправка вернётся к вам событием, и без via вы запишете своё же сообщение вторым. У остальных типов автора via всегда null.

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

Контакты

GET /api/v1/contacts
GET /api/v1/contacts/{id}

Один контакт приходит вместе с идентичностями — способами, которыми человек с вами связывался:

{
    "contact": {
        "id": "0198f3be-…",
        "number": 5,
        "name": null,
        "email": null,
        "blocked": false,
        "created_at": "2026-08-25T10:10:00+03:00",
        "identities": [
            {
                "channel": "widget",
                "external_id": "v_…",
                "page_url": "https://shop.example/cart",
                "attributes": { "uid": "7f3a…", "Тариф": "Pro" },
                "created_at": "2026-08-25T10:10:00+03:00"
            }
        ]
    }
}

Атрибуты со страницы стоят в идентичности, а не в самом контакте, намеренно: name и email мы знаем сами, а attributes — то, что сказала страница.

Вложения

Вложение приходит в сообщении объектом:

"attachments": [
    {
        "id": "0198f3c1-…",
        "url": "https://globask.com/api/v1/attachments/{id}",
        "mime": "image/png",
        "size": 84213,
        "width": 1280,
        "height": 853
    }
]

Ссылка не работает сама по себе — требует того же заголовка Authorization и способности «Чтение», что и всё остальное. Подписанных ссылок, живущих без ключа, у нас нет: они утекают из чужих логов и работают у всякого, кто их подобрал.

Отправка

POST /api/v1/conversations/{id}/messages
Content-Type: application/json

{ "body": "Заказ №1024 отправлен, трек 12345.", "client_message_id": "…uuid…" }

Ответ — то же сообщение, что отдаёт чтение, плюс текущее состояние диалога. Код 201 на новое сообщение и 200 на повтор.

Что нужно знать до первой отправки:

  1. Сообщение помечено автоматическим. В ленте виджета оно выглядит как ответ робота, а не оператора. Выдавать вашу систему за живого человека мы не станем: тот, кто ждёт продолжения разговора, не дождётся.
  2. client_message_id — ваш ключ идемпотентности. Он необязателен, но без него сетевой таймаут на вашей стороне обернётся вторым «заказ отправлен» у посетителя. Повтор с тем же ключом возвращает то же самое сообщение.
  3. Закрытый диалог оживает. Уведомление приходит как раз тогда, когда разговор давно закончен, — отказ был бы отказом в главном сценарии. Вместе с диалогом откроется и тема, в которой отвечают менеджеры.
  4. Диалог не перестаёт считаться ждущим. Ваше сообщение отвечает на свой повод, а не на вопрос посетителя, поэтому last_message_at и статус открытого диалога не меняются. Иначе отправка «заказ собран» прятала бы от менеджера человека, который третий час ждёт ответа.
  5. Менеджер видит, что вы ответили. В тему Telegram уходит строка «через API» с текстом сообщения — иначе он не поймёт, почему посетителю уже ответили, и ответит второй раз.

Завести новый диалог через API нельзя: сообщение уходит в существующий, а диалог заводит посетитель.

Картинка

Картинка отправляется тем же запросом, полем file в multipart/form-data:

curl https://globask.com/api/v1/conversations/{id}/messages \
  -H "Authorization: Bearer gsk_…" \
  -F "body=Фон убран." \
  -F "client_message_id=…uuid…" \
  -F "file=@result.webp"
  • Одна картинка на сообщение. Нужно несколько — отправьте несколько сообщений.
  • JPEG, PNG, WebP или GIF, до 5 МБ. Тип определяется по содержимому файла, а не по имени и заголовку. Не картинка или файл больше — код 422 с объяснением.
  • Текст необязателен: без body уйдёт одна картинка. Без текста и без файла — 422.
  • Метаданные съёмки вырезаются из JPEG до сохранения, в том числе координаты.
  • Посетитель видит картинку в ленте чата, менеджер — в кабинете и в теме Telegram: фото с пометкой «через API», текст — следом отдельным сообщением.
  • Повтор с тем же client_message_id не сохраняет файл второй раз.

В ответе картинка приходит в message.attachments — в той же форме, что и при чтении.

Разметка в тексте

Текст понимается как markdown — в панели чата и в теме Telegram он приходит размеченным, а ссылки становятся кликабельными.

{ "body": "Готово: [посмотреть заказ](https://shop.example/orders/1024)" }

Поддерживается то, что осмысленно в пузыре чата: ссылки (в том числе просто адрес в тексте), жирный, курсив, зачёркнутый, код и блок кода. Заголовки и списки приводятся к обычным строкам — верстать документ в переписке не стоит.

Если разметки в тексте нет, ничего и не происходит: сообщение уходит как есть. HTML в теле не поддерживается и вырезается — размечайте markdown'ом.

Исходный текст остаётся тем, что вы прислали: чтение возвращает его в body без изменений.

Диалог ведёт бот

Всё, что выше, — про содержание переписки. Этот раздел про то, кто её ведёт: ваш бот или менеджеры. Нужна способность «Управление диалогом».

Смысл простой. Бот заявляет, что берёт разговор на себя, отвечает посетителю обычной отправкой, а когда упирается — сдаёт диалог людям и объясняет, почему. Менеджеры при этом видят в теме Telegram, что происходит, и не отвечают вторыми.

Взять диалог

POST /api/v1/conversations/{id}/claim

Ответ — { "claimed": true, "conversation": { … } }. Поле led_by у диалога принимает значения bot и humans.

  • Повтор тем же ключом отвечает 200 и claimed: false. Это не ошибка: ретраить безопасно.
  • 409 — диалог занят: его ведёт другая интеграция либо последним в нём ответил живой менеджер. Влезать в такой разговор мы не даём.

Диалог, где менеджер отвечал давно, а посетитель написал снова, свободен: занятым считается тот, где человек ответил последним.

Передать человеку

POST /api/v1/conversations/{id}/handover
Content-Type: application/json

{ "reason": "Клиент просит человека" }

Причина необязательна, но её стоит писать: она уходит в тему Telegram и попадает в историю диалога. Менеджер по ней понимает, чего от него хотят.

Ответ — { "released": true, "conversation": { … } }; повтор отвечает 200 и released: false, ретраить безопасно. 409 — диалог ведёт другая интеграция: сдать его может только тот ключ, который его взял.

Открытый диалог после передачи считается ждущим ответа — иначе передача человеку была бы передачей в никуда: диалог не попал бы ни в фильтр «Ждут ответа», ни в сводку. Закрытый при этом остаётся закрытым: передавать завершённый разговор некому.

Заметка менеджерам

POST /api/v1/conversations/{id}/notes
Content-Type: application/json

{ "body": "Клиент оплатил счёт №1024" }

Заметку видят менеджеры — в кабинете и в теме. Посетитель её не видит никогда. Это то же самое, что команда /note в теме, только вашими руками.

Ответ — 201 и созданное сообщение: заметка это запись в ленте, и у неё есть свой идентификатор.

Закрыть диалог

POST /api/v1/conversations/{id}/close

Закрывает и тему в Telegram — ровно как команда /close. Новое сообщение посетителя откроет диалог снова.

Как бот узнаёт, что пора замолчать

Ответ менеджера — из темы или из кабинета — снимает владение автоматически. Никакого запроса для этого не нужно: человек не обязан помнить про ваш API, а его вмешательство — самый сильный сигнал из возможных.

Вам об этом приходит вебхук conversation.handover — тот же, что и при вашей собственной передаче. Получив его, бот должен замолчать: дальше отвечает человек. Отличить один случай от другого можно по тому, вы ли только что вызывали handover.

Отдельно об этом стоит помнить, если бот отвечает медленно: пока он думает, менеджер вправе ответить первым, и разговор станет человеческим.

Пока диалог ведёт бот

  • Автоответ молчит. Иначе посетитель получил бы подряд ответ вашего бота и наше «сейчас нерабочее время» — вторая фраза была бы неправдой.
  • Доставка менеджерам продолжается. Сообщения посетителя по-прежнему уходят в тему Telegram: замолчавший бот не должен делать посетителя невидимым для людей.
  • В теме видно, кто отвечает. Взятие диалога и передача его людям показываются отдельными строками.