Opero

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 из переменной окружения:
~/.cursor/mcp.json
{
  "mcpServers": {
    "opero": {
      "url": "https://my.operobusiness.ru/api/mcp",
      "headers": { "Authorization": "Bearer ${env:OPERO_API_KEY}" }
    }
  }
}
Claude Code
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ответ на поручение, если он не успел сразучтение
Ключ или доступ только для чтения видит только инструменты чтения. Удаление, одобрение решений ИИ-штата, письма клиентам, оплаты, реклама и найм через MCP недоступны: это делают люди в Opero.

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}ответ на вопросчтение
Лиды и контрагенты создаются через те же функции, что и экраны Opero: синхронизация с Битрикс24 и правила автоматизации срабатывают как при ручной работе. Параметр 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"}'
Вопрос Opero
curl https://my.operobusiness.ru/api/v1/ask \
  -H "Authorization: Bearer $OPERO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question": "Какие сделки ближе всего к оплате?", "wait": 30}'
Все лиды по страницам (Node.js)
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}/statusstatus: resolved или open, reason
POST/conversations/{id}/ratingscore от 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). Показывайте клиенту все сообщения с type agent и 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: 422 invalid_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 Content
Клиент прочитал до seq 3
curl 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, closedstatus, 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, не повторяется.
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"
  }
}
conversation.status
{
  "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": "Анна П."}
}
conversation.assigned
{
  "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": "Анна П."}
}
conversation.typing
{
  "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": "Анна П."}
}
ping
{
  "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
Проверка подписи (Node.js)
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))
  })
}
Проверка подписи (Python)
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 завело для этого разговора, удаляется тоже, если у него не осталось других разговоров и записей сотрудников.
  • Тексты сообщений, данные карточки и ссылки на файлы не пишутся в журналы сервера: там только номера, размеры и коды.

Ошибки

HTTPcodeКогда
400invalid_jsonтело не JSON-объект
401unauthorizedнет ключа или он недействителен
403insufficient_scopeу ключа нет права channels
404not_foundразговор не найден или удалён
409conversation_closedсообщение в закрытый разговор
409channel_mismatchexternal_id уже занят разговором другого канала
422invalid_field, channel_not_foundполе не прошло проверку (в том числе state, seq, поля customer и card): имя поля в field, например card.sections[0].rows[2].value
429rate_limitedбольше 1 200 запросов в минуту на компанию (сигналов «печатает» больше 6 000), ждите Retry-After

Вебхуки

Opero отправляет POST с JSON на ваш https-адрес, когда в компании происходит событие. Адрес и события задаются в «API и интеграции для ИИ», там же журнал доставок и кнопка проверки.

СобытиеКогда приходитЧто в data
lead.createdпоявился новый лид: из формы, канала, вручную или через APIlead
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 минут. Секрет показывается один раз при создании вебхука.

Проверка подписи (Node.js)
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)
}
Проверка подписи (Python)
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 в час на компанию
Ключи API20 на компанию
Подключённые приложения50 на компанию
Вебхуки10 на компанию
Размер тела запроса256 КБ, у MCP 1 МБ

При превышении ответ HTTP 429 с заголовком Retry-After (секунды). В MCP превышение при вызове инструмента приходит результатом с isError, чтобы модель подождала.

Коды ошибок

КодHTTPЧто значит
unauthorized401нет ключа или токена в заголовке
invalid_token401ключ или токен неверный, отозван или истёк
wrong_audience401токен OAuth выдан для другого ресурса
owner_inactive401человек, выдавший доступ, больше не в компании
insufficient_scope403нужно право записи
read_only_company403компания открыта только для чтения
bad_request400неверные поля, в ответе есть field
assignee_is_ai400задачи ИИ-сотрудникам ставятся в Opero
not_found404записи нет или она в другой компании
rate_limited429превышен лимит, смотрите Retry-After
unavailable503временный сбой, повторите позже

Безопасность

  • Каждый запрос видит только компанию своего ключа или доступа: чужие записи отвечают 404.
  • Права не шире, чем у человека, который выдал доступ, и проверяются на каждом вызове.
  • Ключи и токены хранятся в Opero только хэшами. Секрет вебхука хранится отдельно, он нужен для подписи.
  • Каждый вызов попадает в журнал «API и интеграции для ИИ» на 7 дней.
  • Соединения шифруются, резервные копии делаются каждую ночь, каждое действие ИИ видно в журнале.
  • В данных бывает текст от внешних людей (сообщения клиентов, заметки). Если ваш агент читает его, пусть считает такой текст данными, а не командами.

Вопросы по API и подключениям присылайте на info@yakdu-group.ru