API и MCP
Opero для разработчиков
Подключите Opero к Claude, ChatGPT, Cursor и своим программам. MCP-сервер и REST API работают с данными одной компании: лидами, контрагентами, сделками, задачами, заметками, счетами, ИИ-штатом и сводкой «Сегодня».
Обновлено 20 октября 2026 года
На этой странице
Быстрый старт
Ключи, подключения и вебхуки настраивает владелец или администратор компании в Opero: «Настройки», «API и интеграции для ИИ».
- MCP-сервер
- https://my.operobusiness.ru/api/mcp
- REST API
- https://my.operobusiness.ru/api/v1
- Вход
- OAuth 2.1 или ключ API в заголовке Authorization
- Формат
- JSON в UTF-8, даты в ISO 8601 (UTC)
Создайте ключ с правом чтения и проверьте его:
curl https://my.operobusiness.ru/api/v1/me \
-H "Authorization: Bearer $OPERO_API_KEY"Ответ покажет компанию, права ключа и человека, который его создал.
MCP-сервер
Адрес https://my.operobusiness.ru/api/mcp. Транспорт Streamable HTTP, ответ одним JSON. Поддержаны версии протокола 2026-07-28 (без рукопожатия, версия в каждом запросе, есть server/discover) и 2025-11-25, 2025-06-18, 2025-03-26 (через initialize). Сессии не используются.
Вход: OAuth 2.1 (Claude, ChatGPT и другие клиенты проходят его сами) или ключ API в заголовке Authorization: Bearer opk_... для Cursor, Claude Code и своих агентов. Без входа сервер отвечает 401 с заголовком WWW-Authenticate, в котором указан адрес метаданных ресурса.
Подключение
- Claude: claude.ai, меню Customize, раздел Connectors, кнопка Add custom connector. Вставьте адрес сервера, нажмите Add, затем Connect и разрешите доступ в Opero.
- ChatGPT: chatgpt.com/plugins, кнопка «+», Add custom MCP server. Вставьте адрес сервера, оставьте вход через OAuth и нажмите Create as a plugin.
- Cursor: Cursor Settings, раздел Tools & MCP, или файл
~/.cursor/mcp.jsonс ключом API из переменной окружения:
{
"mcpServers": {
"opero": {
"url": "https://my.operobusiness.ru/api/mcp",
"headers": { "Authorization": "Bearer ${env:OPERO_API_KEY}" }
}
}
}claude mcp add --transport http opero https://my.operobusiness.ru/api/mcpИнструменты
| Инструмент | Что делает | Права |
|---|---|---|
| search | поиск по лидам, контрагентам, задачам и счетам; id для fetch | чтение |
| fetch | запись целиком по id из search | чтение |
| get_today | сводка «Сегодня»: цифры, решения на одобрение, что сделал ИИ-штат | чтение |
| list_leads | лиды с фильтрами и страницами | чтение |
| get_lead | карточка лида: стадия сделки, заметки, открытые задачи | чтение |
| create_lead | новый лид, можно без дублей | запись |
| update_lead | изменить поля и статус лида | запись |
| list_companies | контрагенты (лиды с ИНН) | чтение |
| get_company | контрагент по id | чтение |
| create_company | добавить контрагента по ИНН | запись |
| list_deals | конвейер сделок: стадии, суммы, следующий шаг | чтение |
| get_deal | сделка с историей стадий | чтение |
| move_deal | перенести сделку на другую стадию | запись |
| list_tasks | задачи команды | чтение |
| create_task | задача человеку, можно к лиду | запись |
| update_task | статус, срок, исполнитель задачи | запись |
| list_notes | заметки к лиду | чтение |
| add_note | добавить заметку | запись |
| list_invoices | счета (только чтение) | чтение |
| get_invoice | счёт с позициями и ссылкой на PDF | чтение |
| list_ai_employees | ИИ-штат: роли, самостоятельность, дела за сегодня | чтение |
| list_members | участники компании (id для задач) | чтение |
| ask_opero | вопрос или поручение «Чату с Opero» | чтение |
| get_ask_result | ответ на поручение, если он не успел сразу | чтение |
ask_opero передаёт вопрос в «Чат с Opero» от имени человека, который выдал доступ: вопрос и ответ видны в его чате. Из внешних приложений Opero только готовит черновики (письма и счета ждут одобрения в Opero), не нанимает, не тратит деньги и не пишет клиентам. Если ответ не успел за время ожидания, вызовите get_ask_result с request_id.
Авторизация
Ключи API
- Ключ создаёт владелец или администратор: «Настройки», «API и интеграции для ИИ», «Создать ключ». Ключ показывается один раз, в Opero хранится только его хэш.
- Ключ относится к одной компании. Права:
read(чтение),write(чтение и запись) илиchannels(только внешний чат: разговоры поддержки, без данных компании, см. раздел «Внешний чат»). - Передавайте ключ только в заголовке:
Authorization: Bearer opk_...илиX-Api-Key: opk_.... Ключ в адресе запроса не принимается. - Отзыв действует сразу. На каждом вызове Opero проверяет, что человек, создавший ключ, всё ещё в компании: если его удалили, ключ перестаёт работать.
OAuth 2.1
Для приложений, которые подключают пользователей сами: Claude, ChatGPT, ваши сервисы. Издатель https://my.operobusiness.ru/api/oauth.
| Адрес | Назначение |
|---|---|
| /api/oauth/protected-resource | метаданные ресурса MCP (RFC 9728) |
| /api/oauth/.well-known/openid-configuration | метаданные сервера авторизации |
| /api/oauth/register | регистрация клиента (RFC 7591) |
| /api/oauth/authorize | вход и согласие пользователя |
| /api/oauth/token | обмен кода и обновление токенов |
| /api/oauth/revoke | отзыв токена (RFC 7009) |
- Код авторизации с PKCE, только метод
S256. - Клиент регистрируется сам (
none,client_secret_postилиclient_secret_basic) или представляется документом метаданных:client_id= https-адрес документа. - Параметр
resource(RFC 8707):https://my.operobusiness.ru/api/mcp(по умолчанию) илиhttps://my.operobusiness.ru/api/v1. Токен работает только для своего ресурса. - Права (
scope):readиwrite. На странице согласия человек выбирает компанию, где он владелец или администратор, и может оставить только чтение. - Ответ авторизации содержит
iss(RFC 9207). - Токен доступа живёт 1 час, токен обновления 60 дней и меняется при каждом обновлении. Повтор старого токена обновления отключает доступ целиком.
- Адреса возврата: https,
http://localhostиhttp://127.0.0.1с любым портом, схемы приложений.
Подключённые приложения видны в «API и интеграции для ИИ», там же их отключают одной кнопкой.
REST API
Базовый адрес https://my.operobusiness.ru/api/v1. Компания берётся из ключа, поэтому в адресах её нет.
Успешный ответ: {"data": ...}. Списки возвращают next_cursor: передайте его в параметре cursor, чтобы получить следующую страницу. Размер страницы limit от 1 до 100, по умолчанию 25. Ошибка: HTTP-статус и {"error": {"code": "...", "message": "..."}}, язык сообщения по заголовку Accept-Language.
| Метод | Путь | Что делает | Права |
|---|---|---|---|
| GET | /me | компания, ключ и его права | чтение |
| GET | /leads | лиды: status, q, updated_since, created_since | чтение |
| POST | /leads | новый лид | запись |
| GET | /leads/{id} | лид, стадия сделки, заметки, задачи | чтение |
| PATCH | /leads/{id} | изменить лида | запись |
| GET | /leads/{id}/notes | заметки | чтение |
| POST | /leads/{id}/notes | добавить заметку {text} | запись |
| GET | /companies | контрагенты: q | чтение |
| POST | /companies | новый контрагент {name, inn} | запись |
| GET | /companies/{id} | контрагент | чтение |
| PATCH | /companies/{id} | изменить контрагента | запись |
| GET | /deals | сделки по стадиям: stage | чтение |
| GET | /deals/stages | список стадий | чтение |
| GET | /deals/{lead_id} | сделка с историей | чтение |
| POST | /deals/{lead_id}/stage | перенести {stage, reason} | запись |
| GET | /tasks | задачи: status, assignee_id, lead_id, overdue, q | чтение |
| POST | /tasks | новая задача | запись |
| GET | /tasks/{id} | задача | чтение |
| PATCH | /tasks/{id} | изменить задачу | запись |
| GET | /invoices | счета: status, lead_id, q | чтение |
| GET | /invoices/{id} | счёт | чтение |
| GET | /ai-employees | ИИ-штат | чтение |
| GET | /members | участники компании | чтение |
| GET | /today | сводка «Сегодня» | чтение |
| GET | /search | поиск: q | чтение |
| POST | /ask | вопрос Opero {question, wait} | чтение |
| GET | /ask/{id} | ответ на вопрос | чтение |
dedupe: true вернёт существующего лида с той же почтой, телефоном или ИНН вместо дубля.Примеры
curl https://my.operobusiness.ru/api/v1/leads \
-H "Authorization: Bearer $OPERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Кодлаб", "email": "hello@codlab.example", "source": "website", "dedupe": true}'curl -X PATCH https://my.operobusiness.ru/api/v1/leads/$LEAD_ID \
-H "Authorization: Bearer $OPERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "won", "amount": 48000}'curl https://my.operobusiness.ru/api/v1/deals/$LEAD_ID/stage \
-H "Authorization: Bearer $OPERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"stage": "proposal", "reason": "КП отправлено с сайта"}'curl https://my.operobusiness.ru/api/v1/tasks \
-H "Authorization: Bearer $OPERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Позвонить Кодлабу", "lead_id": "'$LEAD_ID'", "deadline": "2026-10-25T10:00:00Z"}'curl https://my.operobusiness.ru/api/v1/ask \
-H "Authorization: Bearer $OPERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "Какие сделки ближе всего к оплате?", "wait": 30}'const base = "https://my.operobusiness.ru/api/v1"
const headers = { Authorization: `Bearer ${process.env.OPERO_API_KEY}` }
let cursor = ""
do {
const url = `${base}/leads?limit=100${cursor ? `&cursor=${cursor}` : ""}`
const res = await fetch(url, { headers })
if (res.status === 429) {
await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") || 5) * 1000))
continue
}
const page = await res.json()
for (const lead of page.data) console.log(lead.name, lead.status)
cursor = page.next_cursor || ""
} while (cursor)Внешний чат
Разговоры поддержки из вашего приложения и сайта приходят в «Диалоги» Opero через ваш сервер. Ваш ИИ-ассистент отвечает первым и передаёт разговор людям: сервер создаёт разговор в Opero, сотрудники отвечают в «Диалогах», ответ сразу уходит на ваш сервер вебхуком.
- Адрес
- https://my.operobusiness.ru/api/v1/conversations
- Ключ
- ключ API с правом «Только внешний чат» (channels): без него 403, данные компании этим ключом не видны
- Канал
- «Настройки», «Внешний чат»: ключ канала, адрес вебхука, секрет подписи, как отвечает ИИ, срок хранения
- Лимит
- 1 200 запросов в минуту на компанию, без ограничений по адресу; 429 с Retry-After. Сигналы «печатает» считаются отдельно: 6 000 в минуту
Личные данные
- Можно передавать только псевдоним:
contact.label(«Пассажир 7K2Q»), постоянный внешний номер клиентаcontact.external_id, языкru,tgилиen, источникapp_ios,app_androidилиsite, а в текстах пометки вместо данных:[код брони],[телефон],[почта],[номер билета],[номер документа],[фамилия]. - А можно показать сотрудникам данные клиента прямо в карточке разговора: блоки
customerиcard(раздел «Карточка клиента» ниже). Карточку видят только сотрудники вашей компании, в Opero она хранится зашифрованной и не передаётся ни в модель ИИ, ни в аналитику. ИИ Opero не просит и не повторяет личные данные. - Файлы клиента Opero скачивает по вашей ссылке сразу при получении и хранит у себя зашифрованными: фото сотрудник видит картинкой и может увеличить, PDF и другие файлы скачивает. Ссылка должна открываться без входа хотя бы несколько минут, файл до 15 МБ.
Адреса
| Метод | Путь | Что делает |
|---|---|---|
| POST | /conversations | создать или снова открыть разговор: 201 новый, 200 уже был |
| GET | /conversations/{id} | разговор: статус, кто ведёт, поля, карточка клиента |
| PUT | /conversations/{id}/card | обновить карточку клиента: 200, новых сообщений нет |
| POST | /conversations/{id}/messages | сообщение клиента: 201, повтор client_id 200 |
| GET | /conversations/{id}/messages?after=&limit= | сообщения, видимые клиенту, по порядку seq, поле next |
| POST | /conversations/{id}/status | status: resolved или open, reason |
| POST | /conversations/{id}/rating | score от 1 до 5, comment |
| POST | /conversations/{id}/typing | клиент печатает: {"state": "typing"}, 204 |
| POST | /conversations/{id}/read | клиент прочитал ответы до seq: {"seq": N}, 204 |
| DELETE | /conversations/{id} | удалить полностью: 204 |
| GET | /channels | каналы компании: slug, название, режим ИИ |
{id}: номер Opero или ваш номер в виде ext:<external_id>. JSON в UTF-8, время в ISO 8601 (UTC), ошибки {"error": {"code", "message"}}, язык сообщения по Accept-Language.
Разговор
Разговор уникален по external_id в пределах компании. Повтор с тем же external_id возвращает существующий разговор (200) и добавляет историю без дублей по client_id; решённый или закрытый разговор открывается снова. topic: refund, booking, baggage, payment, complaint, other; priority: normal или high; links: до 10 https-ссылок, в Opero это кнопка «Данные клиента»; history: до 50 сообщений {client_id, role, text, at}, role: customer, assistant или system, текст до 4 000 знаков.
curl https://my.operobusiness.ru/api/v1/conversations \
-H "Authorization: Bearer $OPERO_CHANNELS_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "conv-1029",
"channel": "support",
"source": "app_ios",
"contact": {"external_id": "c-58f2", "label": "Пассажир 7K2Q", "lang": "ru"},
"topic": "booking",
"priority": "normal",
"summary": "Клиент спрашивает, где посадочный талон по брони [код брони].",
"links": [{"title": "Карточка клиента", "url": "https://support.example.com/clients/c-58f2"}],
"history": [
{"client_id": "h-1", "role": "customer", "text": "Где мой посадочный талон? Бронь [код брони]", "at": "2026-10-20T09:40:00Z"},
{"client_id": "h-2", "role": "assistant", "text": "Передаю ваш вопрос сотрудникам.", "at": "2026-10-20T09:40:05Z"}
]
}'{
"id": "5b8e2c1a-7d4f-4a3e-9c2b-1e6f8a0d3c5b",
"external_id": "conv-1029",
"channel": "support",
"status": "open",
"url": "https://my.operobusiness.ru/inbox?thread=5b8e2c1a-7d4f-4a3e-9c2b-1e6f8a0d3c5b",
"created": true,
"reopened": false,
"history_added": 2,
"last_seq": 0,
"contact_id": null,
"card_updated_at": null
}Карточка клиента
Данные клиента приходят блоками customer и card в POST /conversations или отдельным запросом PUT /conversations/{id}/card с теми же блоками: ответ 200, новых сообщений в разговоре не появляется. Пришедший блок заменяет прежний целиком; в PUT значение null убирает блок, в POST блок, который не менялся, можно не присылать. В ответах есть contact_id (контакт в CRM) и card_updated_at (когда карточка обновилась). Неверный блок в POST не мешает передать разговор: разговор создаётся, блок не сохраняется, а в ответе приходит warnings [{field, code, message}]; в PUT неверный блок даёт 422 invalid_field.
customer:nameдо 120 знаков,phoneв виде+992177590600(плюс, код страны и номер),emailдо 254 знаков,lang:ru,tgилиen,platform:app_ios,app_androidилиsite,club:{status, miles}, программа лояльности.card:{"sections": [{"title", "rows": [{"label", "value", "link"}]}]}, до 10 разделов, до 30 строк в разделе,valueдо 500 знаков,linkтолько https: у сотрудника открывается в новой вкладке. Разделы показываются как есть, в том порядке, в каком пришли.- Сотрудник видит карточку справа от переписки, на телефоне по кнопке над перепиской: имя крупно, телефон с кнопками «Скопировать» и «Позвонить», почта, язык, откуда пишет, клуб, разделы и время обновления. После
PUTоткрытая карточка обновляется сама, за секунду-две. - По телефону разговор привязывается к контакту в CRM вашей компании (сравниваются последние 10 цифр номера). Нет такого контакта: Opero его заводит с именем, телефоном и почтой. Все разговоры клиента видны на странице контакта.
curl -X PUT https://my.operobusiness.ru/api/v1/conversations/ext:conv-1029/card \
-H "Authorization: Bearer $OPERO_CHANNELS_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"name": "Каримова Дилноза",
"phone": "+992900000001",
"email": "dilnoza@example.com",
"lang": "ru",
"platform": "app_ios",
"club": {"status": "Silver", "miles": 2450}
},
"card": {"sections": [
{"title": "Брони", "rows": [
{"label": "DW4K7M", "value": "DW 040 Душанбе - Хорог, 21.10 07:40, оплачено, места 3A и 3B", "link": "https://support.example.com/bookings/DW4K7M"}
]},
{"title": "Билеты", "rows": [
{"label": "KARIMOVA DILNOZA", "value": "240 0000000002, место 3A"}
]}
]}
}'{
"id": "5b8e2c1a-7d4f-4a3e-9c2b-1e6f8a0d3c5b",
"external_id": "conv-1029",
"changed": true,
"customer": {"name": "Каримова Дилноза", "phone": "+992900000001", "email": "dilnoza@example.com", "lang": "ru", "platform": "app_ios", "club": {"status": "Silver", "miles": 2450}},
"card": {"sections": [{"title": "Брони", "rows": [{"label": "DW4K7M", "value": "DW 040 Душанбе - Хорог, 21.10 07:40, оплачено, места 3A и 3B", "link": "https://support.example.com/bookings/DW4K7M"}]}, {"title": "Билеты", "rows": [{"label": "KARIMOVA DILNOZA", "value": "240 0000000002, место 3A"}]}]},
"card_updated_at": "2026-10-21T08:12:40.512Z",
"contact_id": "8f1d2c3b-4a5e-4f60-9b7c-2d3e4f5a6b7c"
}Сообщения клиента
Текст до 4 000 знаков, до 5 файлов ссылками {name, mime, size, url}. Файлы Opero скачивает сразу (только https и публичные адреса, до 3 перенаправлений, до 15 МБ, тип по содержимому), повторяет попытку через 30 секунд, 2 и 10 минут и хранит копию у себя: сотрудник открывает её, даже когда ваша ссылка уже не действует. В GET /messages файлы клиента приходят вашими же ссылками. Повтор с тем же client_id возвращает сохранённое сообщение (200), новое тело не применяется. В закрытый разговор сообщения не принимаются: 409 conversation_closed, откройте его снова запросом POST /conversations с тем же external_id.
curl https://my.operobusiness.ru/api/v1/conversations/ext:conv-1029/messages \
-H "Authorization: Bearer $OPERO_CHANNELS_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_id": "m-3",
"text": "Вот снимок экрана",
"attachments": [{"name": "screen.png", "mime": "image/png", "size": 48211, "url": "https://files.example.com/s/9f2c"}],
"at": "2026-10-20T09:41:10Z"
}'Сообщения разговора
GET /conversations/{id}/messages?after=&limit= отдаёт сообщения, которые видит клиент (его сообщения и отправленные ответы сотрудников и ИИ; черновики и заметки сотрудников сюда не попадают), по возрастанию seq. Ответ: {"messages": [...], "next": <seq или null>, "last_seq": <номер>}.
after: вернуть сообщения сseqбольше этого номера (по умолчанию 0),limit: от 1 до 100, по умолчанию 50.next:seqпоследнего сообщения на странице, если сообщений большеlimit(передайте его вafterследующего запроса), иначеnull.last_seq: последний номер в разговоре.- Сообщение:
id,seq,direction(inот клиента,outот сотрудника или ИИ),text,author,attachments[{name, mime, size, url}],created_at; у сообщений клиента ещё вашclient_id. author: у клиента{"type": "customer"}, у ответов{"type": "agent" | "ai", "id", "name"}: ответ сотрудника, одобренный по черновику ИИ, приходит как обычный ответ сотрудника (agent). Показывайте клиенту все сообщения сtypeagentиai. Файлы сотрудника приходят ссылкой, которая действует 24 часа.
curl "https://my.operobusiness.ru/api/v1/conversations/ext:conv-1029/messages?after=1&limit=50" \
-H "Authorization: Bearer $OPERO_CHANNELS_KEY"
{
"messages": [
{
"id": "m_5d2c7a9e1b3f4c6a8e0d2b4f",
"seq": 2,
"direction": "in",
"text": "Вот снимок экрана",
"author": {"type": "customer"},
"attachments": [{"name": "screen.png", "mime": "image/png", "size": 48211, "url": "https://files.example.com/s/9f2c"}],
"created_at": "2026-10-20T09:41:10.380Z",
"client_id": "m-3"
},
{
"id": "m_9a1c3e5f7b2d4a6c8e0f1a3b",
"seq": 3,
"direction": "out",
"text": "Здравствуйте. Посадочный талон в приложении, раздел «Мои поездки».",
"author": {"type": "agent", "id": "0f3e5d7c-9b1a-4c2e-8d6f-a1b3c5d7e9f0", "name": "Анна П."},
"attachments": [],
"created_at": "2026-10-20T09:45:00.120Z"
}
],
"next": null,
"last_seq": 3
}Клиент печатает и прочитал
POST /conversations/{id}/typingс телом{"state": "typing"}: ответ 204 без тела. Сотрудник видит в окне разговора строку с псевдонимом клиента, например «Пассажир 7K2Q печатает…», 6 секунд: новый сигнал продлевает её, сообщение клиента гасит. Присылайте сигнал раз в 1-3 секунды, пока клиент набирает текст.- Сигнал не хранится и не повторяется. Сотруднику уходит не больше одного сигнала в секунду на разговор, лишние тоже получают 204. Лимит отдельный, 6 000 в минуту на компанию, и общий лимит 1 200 не тратит. В закрытый разговор сигнал не уходит (ответ 204), другое значение
state: 422invalid_field. POST /conversations/{id}/readс телом{"seq": N}: ответ 204. Клиент прочитал сообщения сотрудников до номераNвключительно (номера из вебхуковconversation.messageи изGET /messages), у этих ответов в окне сотрудника появляется отметка «прочитано».- Номер только растёт: меньший или тот же ничего не меняет, больше последнего засчитывается как последний. Отметка хранится в разговоре и удаляется вместе с ним.
curl https://my.operobusiness.ru/api/v1/conversations/ext:conv-1029/typing \
-H "Authorization: Bearer $OPERO_CHANNELS_KEY" \
-H "Content-Type: application/json" \
-d '{"state": "typing"}'
HTTP/1.1 204 No Contentcurl https://my.operobusiness.ru/api/v1/conversations/ext:conv-1029/read \
-H "Authorization: Bearer $OPERO_CHANNELS_KEY" \
-H "Content-Type: application/json" \
-d '{"seq": 3}'
HTTP/1.1 204 No ContentВебхуки
Событие уходит сразу, без пачек: POST с JSON на адрес канала. Заголовки: Content-Type: application/json, Opero-Event-Id, Opero-Event, Opero-Signature, Opero-Attempt.
| Событие | Когда приходит | Поля |
|---|---|---|
| conversation.message | сотрудник или ИИ отправил сообщение клиенту | message: id, seq, text, author {type agent|ai, id, name}, attachments, created_at |
| conversation.status | статус изменился: open, pending (ждёт ответа клиента), resolved, closed | status, previous, by |
| conversation.assigned | сотрудник взял разговор или передал коллеге | assignee, previous, by |
| conversation.typing | сотрудник печатает, не чаще раза в 3 секунды, без повторов | author |
Неодобренные черновики ИИ и внутренние пометки не уходят никогда. Имя сотрудника приходит как имя и первая буква фамилии, у ответов ИИ имя из настроек канала. Ответ сотрудника, одобренный по черновику ИИ, приходит как обычный ответ сотрудника. У каждого сообщения seq растёт внутри разговора.
Тело события одинаковое у всех типов: id (тот же, что в Opero-Event-Id), type (тот же, что в Opero-Event), created_at (ISO 8601 UTC), channel (ключ канала), conversation: {"id", "external_id"}, а поля самого события лежат рядом на верхнем уровне: message, status, assignee, author. Отдельного поля event нет, тип события в type.
conversation.status:statusиpreviousизopen,pending,resolved,closed;by.type:agent(сотрудник, сidиname),ai,client(ваш сервер через API),customer(новое сообщение клиента снова открыло разговор),system.conversation.assigned:assigneeиprevious:{"id", "name"}илиnull(разговор свободен);by:{"type": "agent", "id", "name"}илиnull, если разговор назначен по очереди автоматически.conversation.typing:author:{"type": "agent", "id", "name"}.ping(кнопка «Проверить адрес» в настройках канала): безconversation, с полемmessage, не повторяется.
{
"id": "evt_3f1c9a7e5b2d4c6a8e0f1a3b5c7d9e1f",
"type": "conversation.message",
"created_at": "2026-10-20T09:45:00.120Z",
"channel": "support",
"conversation": {"id": "5b8e2c1a-7d4f-4a3e-9c2b-1e6f8a0d3c5b", "external_id": "conv-1029"},
"message": {
"id": "m_9a1c3e5f7b2d4a6c8e0f1a3b",
"seq": 3,
"text": "Здравствуйте. Посадочный талон в приложении, раздел «Мои поездки».",
"author": {"type": "agent", "id": "0f3e5d7c-9b1a-4c2e-8d6f-a1b3c5d7e9f0", "name": "Анна П."},
"attachments": [],
"created_at": "2026-10-20T09:45:00.120Z"
}
}{
"id": "evt_7a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d",
"type": "conversation.status",
"created_at": "2026-10-20T09:45:00.480Z",
"channel": "support",
"conversation": {"id": "5b8e2c1a-7d4f-4a3e-9c2b-1e6f8a0d3c5b", "external_id": "conv-1029"},
"status": "pending",
"previous": "open",
"by": {"type": "agent", "id": "0f3e5d7c-9b1a-4c2e-8d6f-a1b3c5d7e9f0", "name": "Анна П."}
}{
"id": "evt_1b3d5f7a9c2e4b6d8f0a2c4e6b8d0f1a",
"type": "conversation.assigned",
"created_at": "2026-10-20T09:45:00.510Z",
"channel": "support",
"conversation": {"id": "5b8e2c1a-7d4f-4a3e-9c2b-1e6f8a0d3c5b", "external_id": "conv-1029"},
"assignee": {"id": "0f3e5d7c-9b1a-4c2e-8d6f-a1b3c5d7e9f0", "name": "Анна П."},
"previous": null,
"by": {"type": "agent", "id": "0f3e5d7c-9b1a-4c2e-8d6f-a1b3c5d7e9f0", "name": "Анна П."}
}{
"id": "evt_8c0e2a4b6d8f1a3c5e7b9d1f3a5c7e9b",
"type": "conversation.typing",
"created_at": "2026-10-20T09:44:52.031Z",
"channel": "support",
"conversation": {"id": "5b8e2c1a-7d4f-4a3e-9c2b-1e6f8a0d3c5b", "external_id": "conv-1029"},
"author": {"type": "agent", "id": "0f3e5d7c-9b1a-4c2e-8d6f-a1b3c5d7e9f0", "name": "Анна П."}
}{
"id": "evt_4e6a8c0b2d4f6a8c0e2b4d6f8a0c2e4b",
"type": "ping",
"created_at": "2026-10-20T09:30:00.002Z",
"channel": "support",
"message": "Opero webhook test"
}Подпись
Opero-Signature: t=<время>,v1=<подпись>, где подпись: HMAC-SHA256 секретом канала от строки <время>.<тело> в шестнадцатеричном виде, тело берите ровно в тех байтах, что пришли. Секрет показывается один раз при создании канала и при перевыпуске. Сутки после перевыпуска в заголовке два v1: новым и прежним секретом, принимайте запрос, если совпала любая.
- Секрет
- whsec_example_only_7Kq2Vd9Lm4Xp
- Время (t)
- 1792489500
- Тело
- {"id":"evt_1","type":"ping"}
- Заголовок
- t=1792489500,v1=e3da66a36e54afa70864c58ad343c4d379aa478000a0aa3c46c0ea4e8f608137
import crypto from "node:crypto"
// rawBody: the raw request body (Buffer) exactly as received, before JSON.parse.
// secrets: [current] or, for 24 hours after a reissue, [new, old].
export function verifyOpero(rawBody, header, secrets) {
const items = header.split(",").map((p) => p.trim().split("="))
const t = items.find(([k]) => k === "t")?.[1]
const sigs = items.filter(([k]) => k === "v1").map(([, v]) => Buffer.from(v))
if (!t || !sigs.length || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
return secrets.some((secret) => {
const expected = Buffer.from(crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex"))
return sigs.some((s) => s.length === expected.length && crypto.timingSafeEqual(s, expected))
})
}import hashlib, hmac, time
def verify_opero(raw_body: bytes, header: str, secrets: list[str]) -> bool:
items = [p.strip().split("=", 1) for p in header.split(",")]
t = next((v for k, v in items if k == "t"), None)
sigs = [v for k, v in items if k == "v1"]
if not t or not sigs or abs(time.time() - int(t)) > 300:
return False
for secret in secrets:
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
if any(hmac.compare_digest(expected, s) for s in sigs):
return True
return FalseДоставка и повторы
- Ответ 2xx быстрее 10 секунд: событие доставлено. Иначе повторы через 30 секунд, 2 минуты, 10 минут, 1 час, 6 часов и 24 часа с тем же
Opero-Event-Id: храните его и не обрабатывайте событие дважды. - Ответ 410 прекращает повторы этого события.
- События одного разговора уходят по одному в порядке
seq. Недоставленное не держит следующие: если видите пропуск вseq, запроситеGET /conversations/{id}/messages?after=<последний seq>. - Адрес только https и только публичный: адрес проверяется перед каждым запросом, внутренние и локальные адреса Opero не вызывает, перенаправления не выполняются.
Статусы и ИИ
- Новое сообщение клиента делает разговор открытым (
open), ответ сотрудника или ИИ: «ждёт ответа клиента» (pending). Сотрудник может поставить «решён» и «закрыт», ваш сервер:resolvedиopen. - Режим ИИ в канале: черновики (сотрудник одобряет или правит, клиент видит только одобренное), отвечает сам (сообщение с
author.type: "ai") или выключен. - Деньги, возвраты, жалобы и претензии, ошибки в билете, багаж, здоровье и особые случаи, угрозы и просьба позвать человека: всегда к сотруднику без ответа ИИ, разговор получает пометку «Нужен сотрудник».
- ИИ отвечает на языке клиента по базе знаний компании, не обещает возвратов, сроков и сумм, не называет цены, расписание и статусы броней: отправляет к вашему ассистенту в приложении и на сайте.
Хранение и удаление
- Разговоры хранятся столько дней, сколько указано в канале (по умолчанию 365), потом удаляются полностью, вместе с карточкой клиента и файлами.
DELETE /conversations/{id}удаляет разговор целиком вместе с копиями для ИИ и аналитики: сообщения, историю, карточку клиента, файлы клиента и сотрудников, переводы для сотрудников, записи журнала ИИ-сотрудника, доставки вебхуков. Контакт CRM, который Opero завело для этого разговора, удаляется тоже, если у него не осталось других разговоров и записей сотрудников.- Тексты сообщений, данные карточки и ссылки на файлы не пишутся в журналы сервера: там только номера, размеры и коды.
Ошибки
| HTTP | code | Когда |
|---|---|---|
| 400 | invalid_json | тело не JSON-объект |
| 401 | unauthorized | нет ключа или он недействителен |
| 403 | insufficient_scope | у ключа нет права channels |
| 404 | not_found | разговор не найден или удалён |
| 409 | conversation_closed | сообщение в закрытый разговор |
| 409 | channel_mismatch | external_id уже занят разговором другого канала |
| 422 | invalid_field, channel_not_found | поле не прошло проверку (в том числе state, seq, поля customer и card): имя поля в field, например card.sections[0].rows[2].value |
| 429 | rate_limited | больше 1 200 запросов в минуту на компанию (сигналов «печатает» больше 6 000), ждите Retry-After |
Вебхуки
Opero отправляет POST с JSON на ваш https-адрес, когда в компании происходит событие. Адрес и события задаются в «API и интеграции для ИИ», там же журнал доставок и кнопка проверки.
| Событие | Когда приходит | Что в data |
|---|---|---|
| lead.created | появился новый лид: из формы, канала, вручную или через API | lead |
| deal.won | лид получил статус won, в том числе когда конвейер видит оплату | lead, amount |
| invoice.paid | счёт отмечен оплаченным: банк, 1С или вручную | invoice |
{
"id": "evt_5c0e9b1f2a7d4f6e8b3c1d2e4f6a8b0c",
"type": "lead.created",
"created_at": "2026-10-20T09:15:02Z",
"company_id": "c0a8012e-5b7d-4e1f-9a3b-2d4e6f8a0b1c",
"data": {
"lead": {
"id": "7d9e1f3a-2b4c-4d6e-8f0a-1b3c5d7e9f2a",
"name": "Codlab",
"status": "new",
"amount": null,
"phone": "+7 900 000-00-00",
"email": "hello@codlab.example",
"source": "website",
"url": "https://my.operobusiness.ru/leads/?lead=7d9e1f3a-2b4c-4d6e-8f0a-1b3c5d7e9f2a",
"created_at": "2026-10-20T09:14:51Z"
}
}
}Заголовки
Opero-Event: тип события,Opero-Delivery: id доставки,Opero-Timestamp: время отправки (секунды Unix).Opero-Signature: t=<время>,v1=<подпись>, где подпись: HMAC-SHA256 секретом вебхука от строки<время>.<тело запроса>в шестнадцатеричном виде.User-Agent: Opero-Webhooks/1.0.
Проверяйте подпись по сырому телу запроса, сравнивайте за постоянное время и отклоняйте запросы старше 5 минут. Секрет показывается один раз при создании вебхука.
import crypto from "node:crypto"
// rawBody: the raw request body (Buffer), before JSON.parse
export function verifyOpero(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
const age = Math.abs(Date.now() / 1000 - Number(parts.t))
if (!parts.t || !parts.v1 || age > 300) return false
const expected = Buffer.from(
crypto.createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest("hex"),
)
const got = Buffer.from(parts.v1)
return got.length === expected.length && crypto.timingSafeEqual(got, expected)
}import hashlib, hmac, time
def verify_opero(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1 or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Доставка и повторы
- Ответ 2xx за 10 секунд: событие доставлено.
- Иначе повторы через 1 минуту, 5 минут, 30 минут, 2 часа и 6 часов: всего 6 попыток.
- Перенаправления не выполняются. Адрес только https и только публичный: внутренние и локальные адреса Opero не вызывает.
- Ответ 410 или 20 недоставленных событий подряд выключают вебхук, включить его снова можно в настройках.
- События проверяются раз в 30 секунд, поэтому приходят с задержкой до минуты. Одно событие может прийти дважды: храните
idсобытия и не обрабатывайте его повторно.
ping и показывает ответ вашего сервера.Лимиты
| Что | Лимит |
|---|---|
| Запросы на ключ или подключённое приложение | 120 в минуту и 3 000 в час |
| Запросы компании всего | 600 в минуту |
| Запись | 60 в минуту на ключ или приложение |
Поручения Opero (ask) | 10 в час на ключ, 30 в час на компанию |
| Ключи API | 20 на компанию |
| Подключённые приложения | 50 на компанию |
| Вебхуки | 10 на компанию |
| Размер тела запроса | 256 КБ, у MCP 1 МБ |
При превышении ответ HTTP 429 с заголовком Retry-After (секунды). В MCP превышение при вызове инструмента приходит результатом с isError, чтобы модель подождала.
Коды ошибок
| Код | HTTP | Что значит |
|---|---|---|
| unauthorized | 401 | нет ключа или токена в заголовке |
| invalid_token | 401 | ключ или токен неверный, отозван или истёк |
| wrong_audience | 401 | токен OAuth выдан для другого ресурса |
| owner_inactive | 401 | человек, выдавший доступ, больше не в компании |
| insufficient_scope | 403 | нужно право записи |
| read_only_company | 403 | компания открыта только для чтения |
| bad_request | 400 | неверные поля, в ответе есть field |
| assignee_is_ai | 400 | задачи ИИ-сотрудникам ставятся в Opero |
| not_found | 404 | записи нет или она в другой компании |
| rate_limited | 429 | превышен лимит, смотрите Retry-After |
| unavailable | 503 | временный сбой, повторите позже |
Безопасность
- Каждый запрос видит только компанию своего ключа или доступа: чужие записи отвечают 404.
- Права не шире, чем у человека, который выдал доступ, и проверяются на каждом вызове.
- Ключи и токены хранятся в Opero только хэшами. Секрет вебхука хранится отдельно, он нужен для подписи.
- Каждый вызов попадает в журнал «API и интеграции для ИИ» на 7 дней.
- Соединения шифруются, резервные копии делаются каждую ночь, каждое действие ИИ видно в журнале.
- В данных бывает текст от внешних людей (сообщения клиентов, заметки). Если ваш агент читает его, пусть считает такой текст данными, а не командами.