API OkoCRM — документация для разработчиков | OkoCRM API OkoCRM — документация для разработчиков | OkoCRM
API OkoCRM — документация для разработчиков | OkoCRM API OkoCRM — документация для разработчиков | OkoCRM
Настроим OkoCRM под ваш бизнес
Экономим ваше время и забираем внедрение CRM на себя
Заказать консультацию
Пользователи
Этапы воронок
Причины отказа
Типы задач
Лента событий в сущности
Поля сущностей
Шаблоны
Вспомогательные данные
Командный чат

Открытый API OkoCRM: интеграция и разработка

Введение

Возможности

API OkoCRM поддерживает чтение и запись контактов, компаний, сделок и других элементов системы. Это даёт вам возможность как просматривать, так и использовать данные, которые хранятся в OkoCRM, в других своих сервисах.

Вы можете использовать API для импорта внешних данных в свою учётную запись OkoCRM.

Мы также работаем над интеграцией других сервисов в платформу OkoCRM. Эти сервисы будут доступны для всех пользователей OkoCRM. Если вы планируете создать свою интеграцию, свяжитесь с нами по адресу support@okocrm.com.

Как это работает?

API OkoCRM основано на REST. Базовый URL: https://api.okocrm.com/v2/.

Пользователи

Список пользователей

GET https://api.okocrm.com/v2/users/
Получить список пользователей
Параметр
Тип
Описание
Запрос
page
integer
Номер страницы, по умолчанию 1
Ответ
data
array
Список пользователей
data.*.id
integer
Идентификатор пользователя
data.*.name
string
ФИО
data.*.email
string
E-mail
data.*.phone
string
Телефон
data.*.date_born
string
Дата рождения в формате ДД.ММ.ГГГГ или null
data.*.avatar_url
string
Ссылка на аватар
data.*.act
integer
Признак активности пользователя
data.*.last_visit
string
Дата последнего визита (ISO 8601) или null
data.*.created_at
integer
Дата создания (unix timestamp)
data.*.updated_at
integer
Дата изменения (unix timestamp)
data.*.deleted_at
integer
Дата удаления (unix timestamp) или null, если пользователь действующий
links
object
Ссылки first, prev, next (next = null на последней странице)
meta
object
Сведения о странице
meta.current_page
integer
Текущая страница
meta.per_page
integer
Записей на странице (15)
meta.from
integer
Порядковый номер первой записи на странице
meta.to
integer
Порядковый номер последней записи на странице
meta.path
string
Адрес запроса без параметров

Возвращает пользователей аккаунта по 15 на страницу. В список попадают и удалённые пользователи: у них заполнено deleted_at. Сначала идут действующие, внутри — по имени.

Общее количество записей не возвращается: чтобы узнать, есть ли следующая страница, проверьте links.next.

Ошибки: 403 — нет прав на просмотр пользователей.

curl --location 'https://api.okocrm.com/v2/users?page=1' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'

Контакты

Список контактов

GET https://api.okocrm.com/v2/contacts/
Получить список контактов
Параметр
Тип
Описание
Запрос
page
integer
Номер страницы, по умолчанию 1
search
string
Строка поиска по имени, телефону, e-mail
filter
array
Фильтр по полям контакта
Ответ
data
array
Список контактов
data.*.id
integer
Идентификатор контакта
data.*.name
string
ФИО
data.*.user_id
integer
Идентификатор ответственного
data.*.author_id
integer
Идентификатор автора
data.*.city_id
integer
Идентификатор города
data.*.date_born
string
Дата рождения (ISO 8601) или null
data.*.source_id
integer
Идентификатор источника
data.*.phones
array
Телефоны контакта
data.*.phones.*.id
integer
Идентификатор телефона
data.*.phones.*.phone
string
Номер телефона
data.*.emails
array
E-mail адреса контакта
data.*.emails.*.id
integer
Идентификатор e-mail
data.*.emails.*.email
string
E-mail адрес
data.*.utm_source
string
UTM Source
data.*.utm_medium
string
UTM Medium
data.*.utm_campaign
string
UTM Campaign
data.*.utm_content
string
UTM Content
data.*.utm_term
string
UTM Term
data.*.url
string
Ссылка на карточку контакта в CRM
data.*.avatar_url
string
Ссылка на аватар
data.*.created_at
integer
Дата создания (unix timestamp)
data.*.updated_at
integer
Дата последнего изменения (unix timestamp)
current_page
integer
Текущая страница
per_page
integer
Записей на странице (30)
from
integer
Порядковый номер первой записи на странице
to
integer
Порядковый номер последней записи на странице
last_page
integer
Номер последней страницы
total
integer
Всего записей
first_page_url
string
Ссылка на первую страницу
last_page_url
string
Ссылка на последнюю страницу
next_page_url
string
Ссылка на следующую страницу или null
prev_page_url
string
Ссылка на предыдущую страницу или null
path
string
Адрес запроса без параметров

Возвращает список контактов по 30 на страницу. Пользователь видит только те контакты, которые ему доступны по правам роли (все, своего отдела или только свои).

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

Контакты лежат в data, рядом — сведения о странице: current_page, per_page, last_page, total и ссылки на соседние страницы.

Ошибки: 403 — нет прав на просмотр контактов.

curl --location 'https://api.okocrm.com/v2/contacts?page=1' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'

Получить контакт

GET https://api.okocrm.com/v2/contacts/{contact_id}/
Получить контакт по идентификатору
Параметр
Тип
Описание
Запрос
contact_id *
integer
Идентификатор контакта (в адресе запроса)
Ответ
data
object
Контакт
data.id
integer
Идентификатор контакта
data.name
string
ФИО
data.user_id
integer
Идентификатор ответственного
data.author_id
integer
Идентификатор автора
data.date_born
string
Дата рождения (ISO 8601) или null
data.source_id
integer
Идентификатор источника
data.city_id
integer
Идентификатор города
data.city
object
Город контакта или null
data.phones
array
Телефоны контакта
data.phones.*.id
integer
Идентификатор телефона, нужен для его изменения
data.phones.*.phone
string
Номер телефона
data.emails
array
E-mail адреса контакта
data.emails.*.id
integer
Идентификатор e-mail, нужен для его изменения
data.emails.*.email
string
E-mail адрес
data.tags
array
Теги контакта
data.utm_source
string
UTM Source
data.utm_medium
string
UTM Medium
data.utm_campaign
string
UTM Campaign
data.utm_content
string
UTM Content
data.utm_term
string
UTM Term
data.url
string
Ссылка на карточку контакта в CRM
data.avatar_url
string
Ссылка на аватар
data.tabs
array
Вкладки карточки и их поля
data.created_at
integer
Дата создания (unix timestamp)
data.updated_at
integer
Дата последнего изменения (unix timestamp)

Возвращает контакт с телефонами, e-mail, городом и тегами. Кроме стандартных полей приходят дополнительные поля, настроенные в аккаунте (ключ — var_name), и описание вкладок карточки с их полями в tabs.

Ошибки: 403 — нет прав на просмотр контакта, 404 — контакт не найден.

curl --location 'https://api.okocrm.com/v2/contacts/125' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'

Добавить контакт

POST https://api.okocrm.com/v2/contacts/
Добавить новый контакт
Параметр
Тип
Описание
Запрос
name *
string
ФИО, до 150 символов
user_id
integer
Идентификатор ответственного пользователя аккаунта
date_born
string
Дата рождения
source_id
integer
Источник
utm_source
string
UTM Source
utm_medium
string
UTM Medium
utm_campaign
string
UTM Campaign
utm_content
string
UTM Content
utm_term
string
UTM Term
client_ids
array
Идентификаторы ClientID (например, Яндекс Метрики)
phones[][phone]
array
Телефоны. Номер должен быть уникальным в аккаунте, иначе 422
emails[][email]
array
E-mail адреса. Адрес должен быть уникальным в аккаунте, иначе 422
tags
array
Названия тегов (до 255 символов). Несуществующие теги создаются
Ответ
id
integer
Идентификатор созданного контакта

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

Телефоны проверяются на корректность и приводятся к международному формату.

Ошибки: 403 — нет прав на создание контактов, 422 — ошибка валидации (например, такой телефон или e-mail уже есть у другого контакта).

<?php

$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => 'https://api.okocrm.com/v2/contacts',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => '',
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 0,
  CURLOPT_FOLLOWLOCATION => true,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_POSTFIELDS => array('name' => 'Domingo Huel','user_id' => '1','phones[][phone]' => '599-318-0994','emails[][email]' => 'Evie_Treutel@gmail.com','tags[]' => 'Cambridgeshire'),
  CURLOPT_HTTPHEADER => array(
    'Accept: application/json',
    'Authorization: Bearer {{токен}}'
  ),
));

$response = curl_exec($curl);

curl_close($curl);
echo $response;
curl --location 'https://api.okocrm.com/v2/contacts' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'name="Delbert Cummerata"' \
--form 'user_id="1"' \
--form 'phones[][phone]="464-313-2066"' \
--form 'emails[][email]="Marianna99@yahoo.com"' \
--form 'tags[]="feed"'
package main

import (
  "fmt"
  "bytes"
  "mime/multipart"
  "net/http"
  "io"
)

func main() {

  url := "https://api.okocrm.com/v2/contacts"
  method := "POST"

  payload := &bytes.Buffer{}
  writer := multipart.NewWriter(payload)
  _ = writer.WriteField("name", "Carlton Corwin")
  _ = writer.WriteField("user_id", "1")
  _ = writer.WriteField("phones[][phone]", "204-857-4763")
  _ = writer.WriteField("emails[][email]", "Lenora.Wisozk63@gmail.com")
  _ = writer.WriteField("tags[]", "HDD")
  err := writer.Close()
  if err != nil {
    fmt.Println(err)
    return
  }


  client := &http.Client {
  }
  req, err := http.NewRequest(method, url, payload)

  if err != nil {
    fmt.Println(err)
    return
  }
  req.Header.Add("Accept", "application/json")
  req.Header.Add("Authorization", "Bearer {{токен}}")

  req.Header.Set("Content-Type", writer.FormDataContentType())
  res, err := client.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer res.Body.Close()

  body, err := io.ReadAll(res.Body)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println(string(body))
}
var https = require('follow-redirects').https;
var fs = require('fs');

var options = {
  'method': 'POST',
  'hostname': 'api.okocrm.com',
  'path': '/v2/contacts',
  'headers': {
    'Accept': 'application/json',
    'Authorization': 'Bearer {{токен}}'
  },
  'maxRedirects': 20
};

var req = https.request(options, function (res) {
  var chunks = [];

  res.on("data", function (chunk) {
    chunks.push(chunk);
  });

  res.on("end", function (chunk) {
    var body = Buffer.concat(chunks);
    console.log(body.toString());
  });

  res.on("error", function (error) {
    console.error(error);
  });
});

var postData = "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nShelia Sawayn\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"user_id\"\r\n\r\n1\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"phones[][phone]\"\r\n\r\n201-325-1196\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"emails[][email]\"\r\n\r\nKatelynn62@gmail.com\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"tags[]\"\r\n\r\nrepurpose\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--";

req.setHeader('content-type', 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW');

req.write(postData);

req.end();
import requests

url = "https://api.okocrm.com/v2/contacts"

payload = {'name': 'Lana Raynor',
'user_id': '1',
'phones[][phone]': '568-371-6852',
'emails[][email]': 'Bonita.Wuckert37@gmail.com',
'tags[]': 'partnerships'}
files=[

]
headers = {
  'Accept': 'application/json',
  'Authorization': 'Bearer {{токен}}'
}

response = requests.request("POST", url, headers=headers, data=payload, files=files)

print(response.text)

Изменить контакт

PUT https://api.okocrm.com/v2/contacts/{contact_id}/
Изменить контакт по идентификатору
Параметр
Тип
Описание
Запрос
name
string
ФИО
user_id
integer
Идентификатор ответственного
date_born
string
Дата рождения
source_id
integer
Источник
utm_source
string
UTM Source
utm_medium
string
UTM Medium
utm_campaign
string
UTM Campaign
utm_content
string
UTM Content
utm_term
string
UTM Term
client_ids
array
Идентификаторы ClientID (например, Яндекс Метрики)
contact_id *
integer
Идентификатор контакта (в адресе запроса)
phones[{id}][phone]
array
Телефоны. Ключ {id} — идентификатор существующего телефона из phones.*.id контакта
emails[{id}][email]
array
E-mail адреса. Ключ {id} — идентификатор существующего e-mail из emails.*.id контакта
Ответ
id
integer
Идентификатор контакта

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

Ошибки: 403 — нет прав на изменение контакта, 404 — контакт не найден, 422 — ошибка валидации.

curl --location --request PUT 'https://api.okocrm.com/v2/contacts/125' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}' \
--data-urlencode 'name=Иванов Иван Петрович' \
--data-urlencode 'user_id=1' \
--data-urlencode 'utm_source=yandex'

Удалить контакт

DELETE https://api.okocrm.com/v2/contacts/{contact_id}/
Удалить контакт по идентификатору
Параметр
Тип
Описание
Запрос
contact_id *
integer
Идентификатор контакта (в адресе запроса)

Удаляет контакт. В случае успеха возвращает true.

Ошибки: 403 — нет прав на удаление контакта, 404 — контакт не найден.

curl --location --request DELETE 'https://api.okocrm.com/v2/contacts/125' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'

Привязка сущностей

POST https://api.okocrm.com/v2/contacts/{contact_id}/link/
Привязать к контакту компании или сделки
Параметр
Тип
Описание
Запрос
contact_id *
integer
Идентификатор контакта (в адресе запроса)
companies
array
Массив объектов с идентификаторами компаний
companies.*.id *
integer
Идентификатор компании, обязателен, если передан companies
leads
array
Массив объектов с идентификаторами сделок
leads.*.id *
integer
Идентификатор сделки, обязателен, если передан leads

Добавляет связи контакта с компаниями и сделками. Уже существующие связи пропускаются без ошибки. В случае успеха возвращает true.

Ошибки: 403 — нет прав на изменение контакта, 404 — контакт не найден, 422 — компания или сделка с таким идентификатором не найдена в аккаунте.

curl --location 'https://api.okocrm.com/v2/contacts/125/link' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}' \
--form 'companies[0][id]="48"' \
--form 'leads[0][id]="1032"'

Компании

Список компаний

GET https://api.okocrm.com/v2/companies/
Получить список компаний
Параметр
Тип
Описание
Запрос
page
integer
Номер страницы, по умолчанию 1
search
string
Строка поиска по наименованию, телефону, e-mail
filter
array
Фильтр по полям компании
Ответ
data
array
Список компаний
data.*.id
integer
Идентификатор компании
data.*.name
string
Наименование
data.*.user_id
integer
Идентификатор ответственного
data.*.source_id
integer
Идентификатор источника
data.*.phones
array
Телефоны компании
data.*.phones.*.id
integer
Идентификатор телефона
data.*.phones.*.phone
string
Номер телефона
data.*.emails
array
E-mail адреса компании
data.*.emails.*.id
integer
Идентификатор e-mail
data.*.emails.*.email
string
E-mail адрес
data.*.utm_source
string
UTM Source
data.*.utm_medium
string
UTM Medium
data.*.utm_campaign
string
UTM Campaign
data.*.utm_content
string
UTM Content
data.*.utm_term
string
UTM Term
data.*.created_at
integer
Дата создания (unix timestamp)
data.*.updated_at
integer
Дата последнего изменения (unix timestamp)
current_page
integer
Текущая страница
per_page
integer
Записей на странице (30)
from
integer
Порядковый номер первой записи на странице
to
integer
Порядковый номер последней записи на странице
last_page
integer
Номер последней страницы
total
integer
Всего записей
first_page_url
string
Ссылка на первую страницу
last_page_url
string
Ссылка на последнюю страницу
next_page_url
string
Ссылка на следующую страницу или null
prev_page_url
string
Ссылка на предыдущую страницу или null
path
string
Адрес запроса без параметров

Возвращает список компаний по 30 на страницу. Пользователь видит только те компании, которые ему доступны по правам роли (все, своего отдела или только свои).

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

Компании лежат в data, рядом — сведения о странице: current_page, per_page, last_page, total и ссылки на соседние страницы.

Ошибки: 403 — нет прав на просмотр компаний.

curl --location 'https://api.okocrm.com/v2/companies?page=1' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'

Получить компанию

GET https://api.okocrm.com/v2/companies/{company_id}/
Получить компанию по идентификатору
Параметр
Тип
Описание
Запрос
company_id *
integer
Идентификатор компании (в адресе запроса)
Ответ
data
object
Компания
data.id
integer
Идентификатор компании
data.name
string
Наименование
data.user_id
integer
Идентификатор ответственного
data.source_id
integer
Идентификатор источника
data.phones
array
Телефоны компании
data.phones.*.id
integer
Идентификатор телефона, нужен для его изменения
data.phones.*.phone
string
Номер телефона
data.emails
array
E-mail адреса компании
data.emails.*.id
integer
Идентификатор e-mail, нужен для его изменения
data.emails.*.email
string
E-mail адрес
data.tags
array
Теги компании
data.utm_source
string
UTM Source
data.utm_medium
string
UTM Medium
data.utm_campaign
string
UTM Campaign
data.utm_content
string
UTM Content
data.utm_term
string
UTM Term
data.tabs
array
Вкладки карточки и их поля
data.created_at
integer
Дата создания (unix timestamp)
data.updated_at
integer
Дата последнего изменения (unix timestamp)

Возвращает компанию с телефонами, e-mail и тегами. Кроме стандартных полей приходят дополнительные поля, настроенные в аккаунте (ключ — var_name), и описание вкладок карточки с их полями в tabs.

Ошибки: 403 — нет прав на просмотр компании, 404 — компания не найдена.

curl --location 'https://api.okocrm.com/v2/companies/48' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'

Добавить компанию

POST https://api.okocrm.com/v2/companies/
Добавить новую компанию
Параметр
Тип
Описание
Запрос
name *
string
Наименование, до 150 символов
user_id
integer
Идентификатор ответственного пользователя аккаунта
source_id
integer
Источник
utm_source
string
UTM Source
utm_medium
string
UTM Medium
utm_campaign
string
UTM Campaign
utm_content
string
UTM Content
utm_term
string
UTM Term
phones[][phone]
array
Телефоны. Номер должен быть уникальным среди компаний аккаунта, иначе 422
emails[][email]
array
E-mail адреса. Адрес должен быть уникальным среди компаний аккаунта, иначе 422
Ответ
id
integer
Идентификатор созданной компании

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

Ошибки: 403 — нет прав на создание компаний, 422 — ошибка валидации (например, такой телефон или e-mail уже есть у другой компании).

<?php

$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => 'https://api.okocrm.com/v2/companies',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => '',
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 0,
  CURLOPT_FOLLOWLOCATION => true,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_POSTFIELDS => array('name' => 'Roland Schneider','user_id' => '1','phones[][phone]' => '962-994-0760','emails[][email]' => 'Isac31@hotmail.com','tags[]' => 'deposit'),
  CURLOPT_HTTPHEADER => array(
    'Accept: application/json',
    'Authorization: Bearer {{токен}}'
  ),
));

$response = curl_exec($curl);

curl_close($curl);
echo $response;
curl --location 'https://api.okocrm.com/v2/companies' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'name="Boyd Hodkiewicz"' \
--form 'user_id="1"' \
--form 'phones[][phone]="546-722-7423"' \
--form 'emails[][email]="Estella54@hotmail.com"' \
--form 'tags[]="transmit"'
package main

import (
  "fmt"
  "bytes"
  "mime/multipart"
  "net/http"
  "io"
)

func main() {

  url := "https://api.okocrm.com/v2/companies"
  method := "POST"

  payload := &bytes.Buffer{}
  writer := multipart.NewWriter(payload)
  _ = writer.WriteField("name", "Brian Stoltenberg")
  _ = writer.WriteField("user_id", "1")
  _ = writer.WriteField("phones[][phone]", "765-323-5658")
  _ = writer.WriteField("emails[][email]", "Darrel7@hotmail.com")
  _ = writer.WriteField("tags[]", "deposit")
  err := writer.Close()
  if err != nil {
    fmt.Println(err)
    return
  }


  client := &http.Client {
  }
  req, err := http.NewRequest(method, url, payload)

  if err != nil {
    fmt.Println(err)
    return
  }
  req.Header.Add("Accept", "application/json")
  req.Header.Add("Authorization", "Bearer {{токен}}")

  req.Header.Set("Content-Type", writer.FormDataContentType())
  res, err := client.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer res.Body.Close()

  body, err := io.ReadAll(res.Body)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println(string(body))
}
var https = require('follow-redirects').https;
var fs = require('fs');

var options = {
  'method': 'POST',
  'hostname': 'api.okocrm.com',
  'path': '/v2/companies',
  'headers': {
    'Accept': 'application/json',
    'Authorization': 'Bearer {{токен}}'
  },
  'maxRedirects': 20
};

var req = https.request(options, function (res) {
  var chunks = [];

  res.on("data", function (chunk) {
    chunks.push(chunk);
  });

  res.on("end", function (chunk) {
    var body = Buffer.concat(chunks);
    console.log(body.toString());
  });

  res.on("error", function (error) {
    console.error(error);
  });
});

var postData = "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nSuzanne DuBuque\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"user_id\"\r\n\r\n1\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"phones[][phone]\"\r\n\r\n730-602-9619\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"emails[][email]\"\r\n\r\nSamara79@yahoo.com\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"tags[]\"\r\n\r\nCustomizable\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--";

req.setHeader('content-type', 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW');

req.write(postData);

req.end();
import requests

url = "https://api.okocrm.com/v2/companies"

payload = {'name': 'Winston Anderson Jr.',
'user_id': '1',
'phones[][phone]': '856-699-9699',
'emails[][email]': 'Keshawn37@gmail.com',
'tags[]': 'Clothing'}
files=[

]
headers = {
  'Accept': 'application/json',
  'Authorization': 'Bearer {{токен}}'
}

response = requests.request("POST", url, headers=headers, data=payload, files=files)

print(response.text)

Изменить компанию

PUT https://api.okocrm.com/v2/companies/{company_id}/
Изменить компанию по идентификатору
Параметр
Тип
Описание
Запрос
name
string
Наименование, до 150 символов
user_id
integer
Идентификатор ответственного
source_id
integer
Источник
utm_source
string
UTM Source
utm_medium
string
UTM Medium
utm_campaign
string
UTM Campaign
utm_content
string
UTM Content
utm_term
string
UTM Term
company_id *
integer
Идентификатор компании (в адресе запроса)
phones[{id}][phone]
array
Телефоны. Ключ {id} — идентификатор существующего телефона из phones.*.id компании
emails[{id}][email]
array
E-mail адреса. Ключ {id} — идентификатор существующего e-mail из emails.*.id компании
Ответ
id
integer
Идентификатор

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

Ошибки: 403 — нет прав на изменение компании, 404 — компания не найдена, 422 — ошибка валидации.

curl --location --request PUT 'https://api.okocrm.com/v2/companies/48' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}' \
--data-urlencode 'name=ООО «СтройМаркет»' \
--data-urlencode 'user_id=1'

Удалить компанию

DELETE https://api.okocrm.com/v2/companies/{company_id}/
Удалить компанию по идентификатору
Параметр
Тип
Описание
Запрос
company_id *
integer
Идентификатор компании (в адресе запроса)

Удаляет компанию. В случае успеха возвращает true.

Ошибки: 403 — нет прав на удаление компании, 404 — компания не найдена.

curl --location --request DELETE 'https://api.okocrm.com/v2/companies/48' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'

Привязка сущностей

POST https://api.okocrm.com/v2/companies/{company_id}/link/
Привязать к компании контакты или сделки
Параметр
Тип
Описание
Запрос
company_id *
integer
Идентификатор компании (в адресе запроса)
contacts
array
Массив объектов с идентификаторами контактов
contacts.*.id *
integer
Идентификатор контакта, обязателен, если передан contacts
leads
array
Массив объектов с идентификаторами сделок
leads.*.id *
integer
Идентификатор сделки, обязателен, если передан leads

Добавляет связи компании с контактами и сделками. Уже существующие связи пропускаются без ошибки. В случае успеха возвращает true.

Ошибки: 403 — нет прав на изменение компании, 404 — компания не найдена, 422 — контакт или сделка с таким идентификатором не найдены в аккаунте.

curl --location 'https://api.okocrm.com/v2/companies/48/link' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}' \
--form 'contacts[0][id]="125"' \
--form 'leads[0][id]="1032"'

Сделки

Список сделок

GET https://api.okocrm.com/v2/leads/
Получить список сделок по всем воронкам и этапам
Параметр
Тип
Описание
Запрос
page
integer
Номер страницы, от 1
search
string
Строка поиска, до 50 символов
filter
array
Фильтры списка, каждый элемент — массив
Ответ
current_page
integer
Текущая страница
per_page
integer
Записей на странице (30)
from
integer
Номер первой записи на странице
to
integer
Номер последней записи на странице
path
string
URL запроса
first_page_url
string
Ссылка на первую страницу
next_page_url
string
Ссылка на следующую страницу, null — это последняя
prev_page_url
string
Ссылка на предыдущую страницу или null
total
integer
Всего найденных сделок. Только при search или filter
last_page
integer
Номер последней страницы. Только при search или filter
data
array
Сделки
data.*.id
integer
ID сделки
data.*.name
string
Название сделки
data.*.pipeline_id
integer
ID воронки
data.*.stages_id
integer
ID этапа воронки
data.*.user_id
integer
ID ответственного
data.*.author_id
integer
ID автора сделки
data.*.client_type
integer
Тип клиента
data.*.budget
float
Бюджет сделки, 2 знака после запятой
data.*.margin
float
Маржа, 2 знака после запятой
data.*.currency_id
integer
ID валюты
data.*.source_id
integer
ID источника
data.*.loss_reason_id
integer
ID причины отказа или null
data.*.city_id
integer
ID города
data.*.closed_at
integer
Дата закрытия, unix timestamp; null — сделка открыта
data.*.closest_task_at
integer
Срок ближайшей задачи, unix timestamp или null
data.*.arrived_stage_at
integer
Дата перехода на текущий этап, unix timestamp
data.*.created_at
integer
Дата создания, unix timestamp
data.*.updated_at
integer
Дата последнего изменения, unix timestamp
data.*.utm_source
string
UTM Source
data.*.utm_medium
string
UTM Medium
data.*.utm_campaign
string
UTM Campaign
data.*.utm_content
string
UTM Content
data.*.utm_term
string
UTM Term
data.*.roistat
string
Номер визита Roistat
data.*.tilda_orderid
string
Номер заказа Tilda
data.*.getcourse_number
string
Номер заказа GetCourse
data.*.url
string
Ссылка на карточку сделки в CRM
data.*.contacts
array
Привязанные контакты: объекты с id, name, phones, emails и доп. полями контакта
data.*.companies
array
Привязанные компании: объекты с id, name, phones, emails и доп. полями компании
data.*.cf_*
string
Доп. поля сделки, ключ cf_ + имя поля; набор зависит от настроек аккаунта

Список сделок, новые сначала, по 30 на странице. Возвращаются только сделки, доступные пользователю по правам его роли: если роль видит только свои сделки или сделки своего отдела, в список попадут они и сделки, где пользователь участник.

Без search и filter общее количество не считается: в ответе нет total и last_page, следующую страницу определяйте по next_page_url. С search или filter в ответе есть total и last_page.

Кроме стандартных полей у сделки выводятся доп. поля с ключами cf_*: их набор свой в каждом аккаунте.

Ошибки: 422 — неверные параметры.

curl -X GET 'https://api.okocrm.com/v2/leads?page=2' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Получить сделку

GET https://api.okocrm.com/v2/leads/{lead_id}/
Получить всю информацию о сделке по её идентификатору
Параметр
Тип
Описание
Запрос
lead_id *
integer
ID сделки, в пути запроса
Ответ
data
object
Сделка
data.id
integer
ID сделки
data.name
string
Название сделки
data.pipeline_id
integer
ID воронки
data.stages_id
integer
ID этапа воронки
data.user_id
integer
ID ответственного
data.author_id
integer
ID автора сделки
data.client_type
integer
Тип клиента
data.budget
float
Бюджет сделки, 2 знака после запятой
data.margin
float
Маржа, 2 знака после запятой
data.currency_id
integer
ID валюты
data.source_id
integer
ID источника
data.loss_reason_id
integer
ID причины отказа или null
data.city_id
integer
ID города
data.closed_at
integer
Дата закрытия, unix timestamp; null — сделка открыта
data.closest_task_at
integer
Срок ближайшей задачи, unix timestamp или null
data.arrived_stage_at
integer
Дата перехода на текущий этап, unix timestamp
data.created_at
integer
Дата создания, unix timestamp
data.updated_at
integer
Дата последнего изменения, unix timestamp
data.utm_source
string
UTM Source
data.utm_medium
string
UTM Medium
data.utm_campaign
string
UTM Campaign
data.utm_content
string
UTM Content
data.utm_term
string
UTM Term
data.roistat
string
Номер визита Roistat
data.tilda_orderid
string
Номер заказа Tilda
data.getcourse_number
string
Номер заказа GetCourse
data.url
string
Ссылка на карточку сделки в CRM
data.contacts
array
Привязанные контакты: объекты с id, name, phones, emails и доп. полями контакта
data.companies
array
Привязанные компании: объекты с id, name, phones, emails и доп. полями компании
data.cf_*
string
Доп. поля сделки, ключ cf_ + имя поля; набор зависит от настроек аккаунта
data.participants_user
array
Участники сделки (пользователи)
data.tabs
array
Вкладки карточки и поля на них для воронки сделки

Сделка со связанными контактами, компаниями и участниками. Кроме стандартных полей выводятся доп. поля с ключами cf_*: их набор свой в каждом аккаунте. В tabs приходит описание вкладок карточки и полей на них.

Ошибки: 403 — нет прав на просмотр сделки; 404 — сделка не найдена.

curl -X GET 'https://api.okocrm.com/v2/leads/51877' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Создать сделку

POST https://api.okocrm.com/v2/leads/
Добавление новой сделки
Параметр
Тип
Описание
Запрос
pipeline_id *
integer
ID воронки
stages_id *
integer
ID этапа воронки
name
string
Название сделки, до 150 символов
loss_reason_id
integer
ID причины отказа
budget
float
Бюджет сделки
margin
float
Маржа
user_id
integer
ID ответственного
responsible
string
Ответственный для нового контакта: ID пользователя или queue_{ID очереди} — берётся активный пользователь очереди
city_id
integer
ID города
ip
string
IP клиента, до 255 символов. При переданном contact по нему определяется город сделки и контакта
source_id
integer
ID источника
utm_source
string
UTM Source
utm_medium
string
UTM Medium
utm_campaign
string
UTM Campaign
utm_content
string
UTM Content
utm_term
string
UTM Term
tags
array
Названия тегов, до 255 символов каждое. Несуществующие теги создаются
contact
object
Контакт сделки: ищется по телефону, если не найден — создаётся
contact.phone *
string
Телефон контакта. Обязателен, если передан contact
contact.name
string
Имя контакта
contact.email
string
Email контакта
contacts
array
ID существующих контактов для привязки
companies
array
ID существующих компаний для привязки
note
object
Примечание к сделке
note.text
string
Текст примечания
check_lead_isset
boolean
Если у найденного по телефону контакта есть открытая сделка — вернуть её вместо создания новой
foreign_id
integer
ID сделки во внешней системе
Ответ
lead_id
integer
ID сделки
contact_id
integer
ID контакта из contact (найденного или созданного) или null
lead
object
Сделка: поля в формате GET leads/{lead_id}

Создаёт сделку. Кроме перечисленных параметров можно передавать доп. поля сделки с ключами cf_*: их набор свой в каждом аккаунте.

Контакт можно передать объектом contact: он ищется по номеру телефона, а если не найден — создаётся с ответственным из responsible (или текущим пользователем). Если контакт найден, ответственным сделки становится ответственный контакта. При check_lead_isset=1 и найденном контакте вместо новой сделки вернётся уже открытая сделка этого контакта.

Уже существующие контакты и компании привязываются массивами ID contacts[] и companies[].

Теги в tags[] передаются названиями: несуществующие теги создаются.

Ошибки: 403 — нет прав на создание сделок; 422 — ошибка валидации.

curl -X POST 'https://api.okocrm.com/v2/leads' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}' \
  --form 'name="Заявка с сайта: кухонный гарнитур"' \
  --form 'pipeline_id="2"' \
  --form 'stages_id="4"' \
  --form 'budget="185000"' \
  --form 'contact[name]="Анна Смирнова"' \
  --form 'contact[phone]="+79161234567"' \
  --form 'responsible="queue_24"' \
  --form 'ip="95.165.12.34"' \
  --form 'note[text]="Клиент просит перезвонить после 18:00"' \
  --form 'tags[]="Сайт"'
<?php

$curl = curl_init();

curl_setopt_array($curl, [
  CURLOPT_URL => 'https://api.okocrm.com/v2/leads',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_POSTFIELDS => [
    'name' => 'Заявка с сайта: кухонный гарнитур',
    'pipeline_id' => '2',
    'stages_id' => '4',
    'budget' => '185000',
    'contact[name]' => 'Анна Смирнова',
    'contact[phone]' => '+79161234567',
    'responsible' => 'queue_24',
    'ip' => '95.165.12.34',
    'note[text]' => 'Клиент просит перезвонить после 18:00',
    'tags[]' => 'Сайт',
  ],
  CURLOPT_HTTPHEADER => [
    'Accept: application/json',
    'Authorization: Bearer {token}',
  ],
]);

$response = curl_exec($curl);
curl_close($curl);
echo $response;
import requests

url = "https://api.okocrm.com/v2/leads"

payload = {
    'name': 'Заявка с сайта: кухонный гарнитур',
    'pipeline_id': '2',
    'stages_id': '4',
    'budget': '185000',
    'contact[name]': 'Анна Смирнова',
    'contact[phone]': '+79161234567',
    'responsible': 'queue_24',
    'ip': '95.165.12.34',
    'note[text]': 'Клиент просит перезвонить после 18:00',
    'tags[]': 'Сайт',
}
headers = {
    'Accept': 'application/json',
    'Authorization': 'Bearer {token}',
}

response = requests.post(url, headers=headers, data=payload)
print(response.text)
const form = new FormData();
form.append('name', 'Заявка с сайта: кухонный гарнитур');
form.append('pipeline_id', '2');
form.append('stages_id', '4');
form.append('budget', '185000');
form.append('contact[name]', 'Анна Смирнова');
form.append('contact[phone]', '+79161234567');
form.append('responsible', 'queue_24');
form.append('ip', '95.165.12.34');
form.append('note[text]', 'Клиент просит перезвонить после 18:00');
form.append('tags[]', 'Сайт');

const res = await fetch('https://api.okocrm.com/v2/leads', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer {token}',
  },
  body: form,
});

console.log(await res.json());
package main

import (
  "bytes"
  "fmt"
  "io"
  "mime/multipart"
  "net/http"
)

func main() {
  payload := &bytes.Buffer{}
  writer := multipart.NewWriter(payload)
  _ = writer.WriteField("name", "Заявка с сайта: кухонный гарнитур")
  _ = writer.WriteField("pipeline_id", "2")
  _ = writer.WriteField("stages_id", "4")
  _ = writer.WriteField("budget", "185000")
  _ = writer.WriteField("contact[name]", "Анна Смирнова")
  _ = writer.WriteField("contact[phone]", "+79161234567")
  _ = writer.WriteField("responsible", "queue_24")
  _ = writer.WriteField("ip", "95.165.12.34")
  _ = writer.WriteField("note[text]", "Клиент просит перезвонить после 18:00")
  _ = writer.WriteField("tags[]", "Сайт")
  writer.Close()

  req, _ := http.NewRequest("POST", "https://api.okocrm.com/v2/leads", payload)
  req.Header.Add("Accept", "application/json")
  req.Header.Add("Authorization", "Bearer {token}")
  req.Header.Set("Content-Type", writer.FormDataContentType())

  res, err := http.DefaultClient.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer res.Body.Close()

  body, _ := io.ReadAll(res.Body)
  fmt.Println(string(body))
}

Изменить сделку

PUT https://api.okocrm.com/v2/leads/{lead_id}/
Обновление информации о сделке
Параметр
Тип
Описание
Запрос
pipeline_id
integer
Идентификатор воронки
stages_id
integer
Идентификатор этапа воронки
name
string
Название сделки, до 150 символов
loss_reason_id
integer
ID причины отказа
tags
array
ID тегов. Полностью заменяет текущие теги сделки
budget
float
Бюджет сделки
margin
float
Маржа
user_id
integer
Идентификатор ответственного
city_id
integer
Идентификатор города
source_id
integer
ID источника
utm_source
string
UTM Source
utm_medium
string
UTM Medium
utm_campaign
string
UTM Campaign
utm_content
string
UTM Content
utm_term
string
UTM Term
lead_id *
integer
ID сделки, в пути запроса
tags_attach
array
ID тегов, которые нужно добавить к текущим
contacts
array
ID контактов, которые нужно привязать к сделке
companies
array
ID компаний, которые нужно привязать к сделке
note
object
Примечание, которое добавится в сделку
note.text
string
Текст примечания

Меняет переданные поля сделки. Кроме перечисленных параметров можно передавать доп. поля сделки с ключами cf_*: их набор свой в каждом аккаунте.

tags[] заменяет весь список тегов, tags_attach[] добавляет теги к текущим. В обоих случаях передаются ID тегов. Контакты и компании из contacts[] и companies[] добавляются к уже привязанным.

В ответ приходит true.

Ошибки: 403 — нет прав на изменение сделки; 404 — сделка не найдена; 422 — ошибка валидации.

curl -X PUT 'https://api.okocrm.com/v2/leads/51877' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "stages_id": 5,
    "budget": 210000,
    "tags_attach": [12],
    "note": { "text": "Согласовали замер на 3 октября" }
  }'

Удалить сделку

DELETE https://api.okocrm.com/v2/leads/{lead_id}/
Удаление сделки по её идентификатору
Параметр
Тип
Описание
Запрос
lead_id *
integer
ID сделки, в пути запроса

Удаляет сделку. В ответ приходит true.

Ошибки: 403 — нет прав на удаление сделки; 404 — сделка не найдена.

curl -X DELETE 'https://api.okocrm.com/v2/leads/51877' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Привязка сущностей

POST https://api.okocrm.com/v2/leads/{lead_id}/link/
Добавить к сущности связь с контактами или компаниями
Параметр
Тип
Описание
Запрос
lead_id *
integer
ID сделки, в пути запроса
contacts
array
Контакты: массив объектов с id
contacts.*.id
integer
ID контакта, обязателен для каждого элемента contacts
companies
array
Компании: массив объектов с id
companies.*.id
integer
ID компании, обязателен для каждого элемента companies

Привязывает к сделке существующие контакты и компании. Уже привязанные пропускаются без ошибки. В ответ приходит true.

Ошибки: 403 — нет прав на изменение сделки; 404 — сделка не найдена; 422 — контакт или компания не найдены.

curl -X POST 'https://api.okocrm.com/v2/leads/51877/link' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "contacts": [{ "id": 30412 }],
    "companies": [{ "id": 8150 }]
  }'

Справочники

Позиции из справочников в сделке. Добавлять можно только элементы справочников, подключённых к воронке сделки.
GET https://api.okocrm.com/v2/leads/{lead_id}/guides
Товары и услуги сделки
Параметр
Тип
Описание
Запрос
lead_id *
integer
ID сделки, в пути запроса
Ответ
data
array
Позиции сделки
data.*.id
integer
ID элемента справочника
data.*.guide_id
integer
ID справочника
data.*.name
string
Название
data.*.article
string
Артикул или null
data.*.quantity
integer
Количество
data.*.price
float
Цена за единицу на момент добавления в сделку
data.*.sale
float
Цена за единицу со скидкой, null — скидки нет
data.*.total
float
Итог позиции: (sale, если её нет — price) × quantity; null, если цены нет
data.*.unit
object
Единица измерения: id, name, name_short или null
data.*.nds
object
Ставка НДС: id, name или null

Позиции из справочников, добавленные в сделку.

price — цена за единицу на момент добавления в сделку: если цену в справочнике потом изменят, здесь она не изменится. total = (sale, а если её нет — price) × quantity.

Ошибки: 403 — нет доступа к сделке; 404 — сделка не найдена.

curl -X GET 'https://api.okocrm.com/v2/leads/51877/guides' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'
POST https://api.okocrm.com/v2/leads/{lead_id}/guides
Добавить или изменить товары в сделке
Параметр
Тип
Описание
Запрос
lead_id *
integer
ID сделки, в пути запроса
items *
array
Позиции, от 1 до 100
items.*.id *
integer
ID элемента справочника, без повторов
items.*.quantity *
integer
Количество, от 1 до 999999
items.*.sale
float
Цена за единицу со скидкой, от 0 до 99999999. Не передана — остаётся прежней, null — скидка снимается
Ответ
data
array
Все позиции сделки в формате GET leads/{lead_id}/guides

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

Добавлять можно только элементы справочников, подключённых к воронке сделки. Бюджет сделки не пересчитывается: при необходимости измените его через PUT leads/{lead_id}.

В ответе возвращаются все позиции сделки после изменения, в формате GET leads/{lead_id}/guides.

Ошибки:

  • 403 — нет прав на изменение сделки;
  • 404 — сделка не найдена;
  • 422 с ключом items.N.id — элемент не найден или его справочник не подключён к воронке сделки;
  • 422 с ключом guides.{id} — скидка не входит в допустимые для справочника;
  • 422 — повторяющиеся id, quantity меньше 1 или пустой items.

curl -X POST 'https://api.okocrm.com/v2/leads/51877/guides' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "items": [
      { "id": 5012, "quantity": 2, "sale": 22491 },
      { "id": 5020, "quantity": 1 }
    ]
  }'
DELETE https://api.okocrm.com/v2/leads/{lead_id}/guides/{id}
Убрать товар из сделки
Параметр
Тип
Описание
Запрос
lead_id *
integer
ID сделки, в пути запроса
id *
integer
ID элемента справочника, в пути запроса

Убирает одну позицию из сделки. Сам элемент в справочнике остаётся.

Ответ 204 без тела. Ошибки: 403 — нет прав на изменение сделки; 404 — сделка не найдена или такой позиции в сделке нет.

curl -X DELETE 'https://api.okocrm.com/v2/leads/51877/guides/5012' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Воронки

Список воронок

GET https://api.okocrm.com/v2/pipelines/
Получить список воронок
Параметр
Тип
Описание
Ответ
data
array
Воронки
data.*.id
integer
ID воронки
data.*.name
string
Название воронки

Воронки, доступные пользователю, в порядке сортировки в CRM.

Ошибки: 403 — нет прав на просмотр сделок.

curl -X GET 'https://api.okocrm.com/v2/pipelines' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Этапы воронок

Список этапов

GET https://api.okocrm.com/v2/pipelines/stages/{pipeline_id}
Список этапов воронки
Параметр
Тип
Описание
Запрос
pipeline_id *
integer
ID воронки, в пути запроса
Ответ
data
array
Этапы воронки
data.*.id
integer
ID этапа
data.*.pipeline_id
integer
ID воронки
data.*.name
string
Название этапа
data.*.color
string
Цвет этапа в HEX, например #3B82F6
data.*.system
integer
Признак системного этапа; null — обычный этап
data.*.type
integer
Тип закрывающего этапа: 1 — успех, 2 — провал; null — обычный этап

Этапы воронки: сначала обычные в порядке сортировки в CRM, затем системные этапы закрытия сделки. Закрывающие этапы отличаются полем type: 1 — успешно реализована, 2 — закрыта и не реализована.

Ошибки: 403 — нет прав на просмотр воронок.

curl -X GET 'https://api.okocrm.com/v2/pipelines/stages/2' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Причины отказа

Список

GET https://api.okocrm.com/v2/loss_reasons
Список причин отказа
Параметр
Тип
Описание
Запрос
pipeline_id
integer
ID воронки
Ответ
data
array
Причины отказа
data.*.id
integer
ID причины отказа
data.*.pipeline_id
integer
ID воронки
data.*.name
string
Название

Причины отказа. Без pipeline_id возвращаются причины всех воронок по порядку создания, с pipeline_id — только причины этой воронки в порядке сортировки в CRM.

Ошибки: 422 — воронка не найдена.

curl -X GET 'https://api.okocrm.com/v2/loss_reasons?pipeline_id=2' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Задачи

Список задач

GET https://api.okocrm.com/v2/tasks/
Получить список задач
Параметр
Тип
Описание
Запрос
lead_id
integer
Идентификатор сделки
contact_id
integer
Идентификатор контакта
company_id
integer
Идентификатор компании
binding
string
Привязка задачи: entity — к сделке, контакту или компании; personal — личные задачи без привязки
page
integer
Номер страницы, по умолчанию 1. На странице до 50 задач
Ответ
current_page
integer
Номер текущей страницы
per_page
integer
Размер страницы (50)
from
integer
Порядковый номер первой задачи на странице
to
integer
Порядковый номер последней задачи на странице
path
string
Адрес запроса без параметров
first_page_url
string
Ссылка на первую страницу
next_page_url
string
Ссылка на следующую страницу или null, если страница последняя
prev_page_url
string
Ссылка на предыдущую страницу или null
data
array
Список задач
data.*.id
integer
Идентификатор задачи
data.*.type_id
integer
Идентификатор типа задачи
data.*.lead_id
integer
Идентификатор сделки, к которой привязана задача
data.*.contact_id
integer
Идентификатор контакта, к которому привязана задача
data.*.company_id
integer
Идентификатор компании, к которой привязана задача
data.*.text
string
Текст задачи
data.*.executor_id
integer
Идентификатор исполнителя задачи
data.*.owner_id
integer
Идентификатор пользователя, поставившего задачу
data.*.date_start
integer
Планируемое начало выполнения задачи, unix timestamp
data.*.date_finish
integer
Планируемое окончание выполнения задачи, unix timestamp
data.*.done
integer
Признак закрытой задачи: 1 — выполнена, 0 — открыта
data.*.closed_at
integer
Фактическое закрытие задачи, unix timestamp
data.*.created_at
integer
Создание задачи, unix timestamp
data.*.updated_at
integer
Последнее изменение задачи, unix timestamp
data.*.checklist
array
Чеклист задачи или null, если его нет
data.*.checklist.*.id
integer
Идентификатор пункта чеклиста
data.*.checklist.*.name
string
Название пункта
data.*.checklist.*.done
boolean
Пункт выполнен
data.*.checklist.*.done_by
integer
Идентификатор пользователя, отметившего пункт
data.*.checklist.*.done_at
integer
Время отметки пункта, unix timestamp
data.*.comments
array
Комментарии к задаче, от старых к новым
data.*.comments.*.id
integer
Идентификатор комментария
data.*.comments.*.text
string
Текст комментария
data.*.comments.*.created_at
integer
Создание комментария, unix timestamp
data.*.comments.*.updated_at
integer
Изменение комментария, unix timestamp

Возвращает задачи CRM (привязанные к сделкам, контактам, компаниям, а также личные задачи без привязки). Задачи проектов в список не попадают. Задачи отсортированы от старых к новым.

Постраничный вывод: по 50 задач на страницу, номер страницы передаётся в параметре page. Ответ — объект пагинатора: задачи в data, ссылки на соседние страницы в next_page_url / prev_page_url. Общее количество не возвращается: если next_page_url равен null, страница последняя.

Личные задачи пользователя с правом просмотра задач «только свои» или «свои и группы» выводятся только те, где он (или его коллеги по группе) исполнитель или постановщик.

Все даты в ответе — unix timestamp.

Ошибки:

  • 403 — нет права на просмотр задач;
  • 422 — ошибка валидации (например, сделка с таким id не найдена).

curl --location 'https://api.okocrm.com/v2/tasks/?lead_id=1725&page=1' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}'

Создать задачу

POST https://api.okocrm.com/v2/tasks/
Создать задачу
Параметр
Тип
Описание
Запрос
type_id
integer
Идентификатор типа задачи. Обязателен, если не передан text
text
string
Текст задачи, до 20000 символов. Обязателен, если не передан type_id
executor_id *
integer
Идентификатор исполнителя задачи
date_start *
string
Начало выполнения задачи в формате Y-m-d H:i
date_finish *
string
Срок выполнения задачи в формате Y-m-d H:i, не раньше date_start
lead_id
integer
Идентификатор сделки, к которой привязана задача
contact_id
integer
Идентификатор контакта, к которому привязана задача
company_id
integer
Идентификатор компании, к которой привязана задача
reminder
integer
За сколько секунд напомнить о задаче. Максимум 15552000 (180 дней)
checklist
array
Пункты чеклиста: массив строк или объектов {name}. До 50 пунктов, название до 255 символов
checklist.*.name
string
Название пункта чеклиста
Ответ
id
integer
Идентификатор созданной задачи

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

Обязательно передать type_id или text (хотя бы одно из двух), а также executor_id, date_start и date_finish. Даты передаются в формате Y-m-d H:i в часовом поясе аккаунта, date_finish не может быть раньше date_start.

Если checklist не передан, а у типа задачи есть шаблон чеклиста, он подставится автоматически. Чтобы создать задачу без чеклиста, передайте пустой checklist.

Ошибки:

  • 403 — нет права на создание задачи;
  • 422 — ошибка валидации.

curl --location 'https://api.okocrm.com/v2/tasks/' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'lead_id="1725"' \
--form 'type_id="2"' \
--form 'text="Позвонить клиенту и согласовать дату доставки"' \
--form 'executor_id="76"' \
--form 'date_start="2026-10-05 10:00"' \
--form 'date_finish="2026-10-05 12:00"' \
--form 'checklist[0][name]="Уточнить адрес"' \
--form 'checklist[1][name]="Подтвердить время"'
<?php

$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => 'https://api.okocrm.com/v2/tasks/',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_POSTFIELDS => array(
    'lead_id' => '1725',
    'type_id' => '2',
    'text' => 'Позвонить клиенту и согласовать дату доставки',
    'executor_id' => '76',
    'date_start' => '2026-10-05 10:00',
    'date_finish' => '2026-10-05 12:00'
  ),
  CURLOPT_HTTPHEADER => array(
    'Accept: application/json',
    'Authorization: Bearer {{токен}}'
  ),
));

$response = curl_exec($curl);

curl_close($curl);
echo $response;
import requests

url = "https://api.okocrm.com/v2/tasks/"

payload = {
  'lead_id': '1725',
  'type_id': '2',
  'text': 'Позвонить клиенту и согласовать дату доставки',
  'executor_id': '76',
  'date_start': '2026-10-05 10:00',
  'date_finish': '2026-10-05 12:00'
}
headers = {
  'Accept': 'application/json',
  'Authorization': 'Bearer {{токен}}'
}

response = requests.post(url, headers=headers, data=payload)

print(response.text)
const params = new URLSearchParams({
  lead_id: '1725',
  type_id: '2',
  text: 'Позвонить клиенту и согласовать дату доставки',
  executor_id: '76',
  date_start: '2026-10-05 10:00',
  date_finish: '2026-10-05 12:00'
});

fetch('https://api.okocrm.com/v2/tasks/', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer {{токен}}'
  },
  body: params
})
  .then((res) => res.json())
  .then(console.log);
package main

import (
  "fmt"
  "io"
  "net/http"
  "net/url"
  "strings"
)

func main() {
  form := url.Values{}
  form.Set("lead_id", "1725")
  form.Set("type_id", "2")
  form.Set("text", "Позвонить клиенту и согласовать дату доставки")
  form.Set("executor_id", "76")
  form.Set("date_start", "2026-10-05 10:00")
  form.Set("date_finish", "2026-10-05 12:00")

  req, _ := http.NewRequest("POST", "https://api.okocrm.com/v2/tasks/", strings.NewReader(form.Encode()))
  req.Header.Add("Accept", "application/json")
  req.Header.Add("Authorization", "Bearer {{токен}}")
  req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

  res, err := http.DefaultClient.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer res.Body.Close()

  body, _ := io.ReadAll(res.Body)
  fmt.Println(string(body))
}

Изменить задачу

PUT https://api.okocrm.com/v2/tasks/{task_id}/
Изменить задачу, по её идентификатору
Параметр
Тип
Описание
Запрос
type_id
integer
Идентификатор типа задачи
text
string
Текст задачи, до 20000 символов
executor_id
integer
Идентификатор исполнителя задачи
date_start
string
Начало выполнения задачи в формате Y-m-d H:i
date_finish
string
Срок выполнения задачи в формате Y-m-d H:i, не раньше date_start
lead_id
integer
Идентификатор сделки, к которой привязана задача
contact_id
integer
Идентификатор контакта, к которому привязана задача
company_id
integer
Идентификатор компании, к которой привязана задача
reminder
integer
За сколько секунд напомнить о задаче. Максимум 15552000 (180 дней)
checklist
array
Новый состав чеклиста (заменяет текущий). До 50 пунктов, название до 255 символов
checklist.*.id
integer
Идентификатор существующего пункта — чтобы сохранить его отметку
checklist.*.name
string
Название пункта чеклиста

Изменяет задачу CRM. Все параметры необязательные: меняются только переданные поля. Даты передаются в формате Y-m-d H:i.

Если передан checklist, состав чеклиста заменяется целиком. Чтобы сохранить отметку выполнения у существующего пункта, передайте его id; пункты без id добавятся как новые невыполненные, пункты, которых нет в списке, удалятся. Пустой checklist удаляет чеклист.

При успехе возвращается true.

Ошибки:

  • 403 — нет права на изменение задачи;
  • 404 — задача не найдена;
  • 422 — ошибка валидации.

curl --location --request PUT 'https://api.okocrm.com/v2/tasks/5310' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--data-urlencode 'date_finish=2026-10-06 18:00' \
--data-urlencode 'text=Перенести звонок на вторник'

Удалить задачу

DELETE https://api.okocrm.com/v2/tasks/{task_id}/
Удалить задачу, по её идентификатору
Параметр
Тип
Описание

Удаляет задачу CRM. При успехе возвращается true.

Ошибки:

  • 403 — нет права на удаление задачи;
  • 404 — задача не найдена.

curl --location --request DELETE 'https://api.okocrm.com/v2/tasks/5310' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}'

Задача выполнена + комментарий

POST https://api.okocrm.com/v2/tasks/done/{id}
Отметка выполнения задачи и добавление комментария по ней
Параметр
Тип
Описание
Запрос
text
string
Комментарий с результатом выполнения

Закрывает задачу CRM (отмечает выполненной). Если передан text, он добавляется к задаче комментарием с результатом. При успехе возвращается true.

Ошибки:

  • 403 — нет права на выполнение задачи;
  • 404 — задача не найдена.

curl --location 'https://api.okocrm.com/v2/tasks/done/5310' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'text="Клиент подтвердил доставку на пятницу"'

Отметить пункт чеклиста

POST https://api.okocrm.com/v2/tasks/checklist/{task_id}/{item_id}
Отметить пункт чеклиста задачи
Параметр
Тип
Описание
Запрос
done *
boolean
1 (true) — отметить пункт выполненным, 0 (false) — снять отметку
Ответ
data
object
Задача
data.id
integer
Идентификатор задачи
data.type_id
integer
Идентификатор типа задачи
data.lead_id
integer
Идентификатор сделки
data.contact_id
integer
Идентификатор контакта
data.company_id
integer
Идентификатор компании
data.text
string
Текст задачи
data.executor_id
integer
Идентификатор исполнителя
data.owner_id
integer
Идентификатор постановщика
data.date_start
integer
Начало выполнения, unix timestamp
data.date_finish
integer
Срок выполнения, unix timestamp
data.done
integer
Признак закрытой задачи: 1 или 0
data.closed_at
integer
Фактическое закрытие, unix timestamp
data.created_at
integer
Создание задачи, unix timestamp
data.updated_at
integer
Последнее изменение, unix timestamp
data.checklist
array
Чеклист задачи
data.checklist.*.id
integer
Идентификатор пункта
data.checklist.*.name
string
Название пункта
data.checklist.*.done
boolean
Пункт выполнен
data.checklist.*.done_by
integer
Кто отметил пункт, или null
data.checklist.*.done_at
integer
Когда отмечен пункт, unix timestamp или null
data.comments
array
Комментарии к задаче (id, text, created_at, updated_at)

Отмечает пункт чеклиста задачи CRM выполненным или снимает отметку. В адресе передаются идентификатор задачи и идентификатор пункта (checklist.*.id из списка задач). Работает только для задач CRM, не для задач проектов.

При отметке у пункта сохраняются пользователь (done_by) и время (done_at), при снятии отметки они очищаются. В ответе возвращается задача целиком в data.

Ошибки:

  • 403 — нет права на выполнение задачи;
  • 404 — задача или пункт чеклиста не найдены;
  • 422 — не передан done.

curl --location 'https://api.okocrm.com/v2/tasks/checklist/5310/2' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'done="1"'

Типы задач

Список типов

GET https://api.okocrm.com/v2/tasks/types/
Получить список типов
Параметр
Тип
Описание
Ответ
data
array
Список типов задач
data.*.id
integer
Идентификатор типа
data.*.title
string
Название типа
data.*.icon
string
Иконка
data.*.color
string
Цвет иконки
data.*.checklist
array
Шаблон чеклиста: пункты, которые подставляются в новую задачу этого типа, или null

Возвращает все типы задач аккаунта в порядке сортировки, без пагинации. Список обёрнут в data.

curl --location 'https://api.okocrm.com/v2/tasks/types/' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}'

Лента событий в сущности

Список событий

GET https://api.okocrm.com/v2/notes/
Получить список примечаний
Параметр
Тип
Описание
Запрос
lead_id
integer
Идентификатор сделки. Обязателен, если не переданы contact_id и company_id
contact_id
integer
Идентификатор контакта. Обязателен, если не переданы lead_id и company_id
company_id
integer
Идентификатор компании. Обязателен, если не переданы lead_id и contact_id
page
integer
Номер страницы, по умолчанию 1. На странице до 15 примечаний
Ответ
data
array
Список примечаний
data.*.id
integer
Идентификатор примечания
data.*.type
integer
Тип примечания: 4 — текстовое, 17 — объявление Авито
data.*.text
string
Текст примечания
data.*.meta
object
Данные объявления Авито для type = 17, для остальных null
data.*.author_id
integer
Идентификатор автора примечания
data.*.created_at
integer
Создание примечания, unix timestamp
data.*.updated_at
integer
Последнее редактирование, unix timestamp
links
object
Ссылки на страницы: first, prev, next (null, если страницы нет)
meta
object
Данные пагинации: current_page, per_page, from, to, path

Возвращает примечания из ленты событий сделки, контакта или компании. Обязательно передать один из идентификаторов: lead_id, contact_id или company_id.

В список попадают только текстовые примечания (type = 4) и примечания с объявлением Авито (type = 17); остальные события ленты (звонки, письма, смены этапов и т. п.) не выводятся. Сортировка — от старых к новым.

Постраничный вывод: по 15 примечаний на страницу, номер страницы в параметре page. Ответ содержит data, links и meta; общее количество не возвращается — если links.next равен null, страница последняя.

Ошибки:

  • 403 — нет доступа к сущности;
  • 422 — не передан идентификатор сущности или сущность не найдена.

curl --location 'https://api.okocrm.com/v2/notes/?lead_id=1725&page=1' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}'

Добавить

POST https://api.okocrm.com/v2/notes/note
Добавить текстовое примечание к сущности
Параметр
Тип
Описание
Запрос
lead_id
integer
Идентификатор сделки. Обязателен, если не переданы contact_id и company_id
contact_id
integer
Идентификатор контакта. Обязателен, если не переданы lead_id и company_id
company_id
integer
Идентификатор компании. Обязателен, если не переданы lead_id и contact_id
text *
string
Текст примечания, до 5000 символов
Ответ
id
integer
Идентификатор примечания

Добавляет текстовое примечание в ленту событий сделки, контакта или компании. Нужно передать один из идентификаторов: lead_id, contact_id или company_id.

Ошибки:

  • 403 — нет доступа к сущности;
  • 422 — ошибка валидации.

curl --location 'https://api.okocrm.com/v2/notes/note' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'lead_id="1725"' \
--form 'text="Клиент просит перезвонить после 15:00"'
<?php

$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL => 'https://api.okocrm.com/v2/notes/note',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_POSTFIELDS => array('lead_id' => '1725', 'text' => 'Клиент просит перезвонить после 15:00'),
  CURLOPT_HTTPHEADER => array(
    'Accept: application/json',
    'Authorization: Bearer {{токен}}'
  ),
));

$response = curl_exec($curl);

curl_close($curl);
echo $response;
import requests

url = "https://api.okocrm.com/v2/notes/note"

payload = {'lead_id': '1725', 'text': 'Клиент просит перезвонить после 15:00'}
headers = {
  'Accept': 'application/json',
  'Authorization': 'Bearer {{токен}}'
}

response = requests.post(url, headers=headers, data=payload)

print(response.text)
fetch('https://api.okocrm.com/v2/notes/note', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer {{токен}}'
  },
  body: new URLSearchParams({ lead_id: '1725', text: 'Клиент просит перезвонить после 15:00' })
})
  .then((res) => res.json())
  .then(console.log);
package main

import (
  "fmt"
  "io"
  "net/http"
  "net/url"
  "strings"
)

func main() {
  form := url.Values{}
  form.Set("lead_id", "1725")
  form.Set("text", "Клиент просит перезвонить после 15:00")

  req, _ := http.NewRequest("POST", "https://api.okocrm.com/v2/notes/note", strings.NewReader(form.Encode()))
  req.Header.Add("Accept", "application/json")
  req.Header.Add("Authorization", "Bearer {{токен}}")
  req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

  res, err := http.DefaultClient.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer res.Body.Close()

  body, _ := io.ReadAll(res.Body)
  fmt.Println(string(body))
}

Прикрепить файл

POST https://api.okocrm.com/v2/notes/files
Прикрепить файл
Параметр
Тип
Описание
Запрос
lead_id
integer
Идентификатор сделки. Обязателен, если не переданы contact_id и company_id
contact_id
integer
Идентификатор контакта. Обязателен, если не переданы lead_id и company_id
company_id
integer
Идентификатор компании. Обязателен, если не переданы lead_id и contact_id
files *
array
Один или несколько файлов (files[]) в multipart/form-data, до 20 МБ каждый
Ответ
id
integer
Идентификатор примечания с файлами
files
array
Загруженные файлы
files.*.id
integer
Идентификатор файла
files.*.filename
string
Имя файла в хранилище
files.*.filename_original
string
Исходное имя файла
files.*.extension
string
Расширение файла
files.*.type
string
Тип файла
files.*.filesize
integer
Размер файла в байтах
files.*.url
string
Ссылка на файл

Загружает один или несколько файлов и прикрепляет их к сделке, контакту или компании одним примечанием в ленте событий. Запрос отправляется как multipart/form-data. Нужно передать один из идентификаторов: lead_id, contact_id или company_id.

Ошибки:

  • 403 — нет доступа к сущности;
  • 422 — ошибка валидации (нет файлов, файл больше 20 МБ);
  • 423 — закончилось место в хранилище аккаунта.

curl --location 'https://api.okocrm.com/v2/notes/files' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'lead_id="1725"' \
--form 'files[]=@"/path/to/contract.pdf"' \
--form 'files[]=@"/path/to/photo.jpg"'

Поля сущностей

Список полей

GET https://api.okocrm.com/v2/fields/
Получить список полей
Параметр
Тип
Описание
Запрос
type
string
Тип сущности: leads, contacts, companies или guides. Без параметра возвращаются поля всех типов
Ответ
data
array
Список полей
data.*.id
integer
Идентификатор поля
data.*.name
string
Название поля
data.*.type
string
Тип сущности: leads, contacts, companies или guides
data.*.fields_type_id
integer
Тип поля, справочник значений в описании метода
data.*.system
integer
1 — системное поле, null — дополнительное
data.*.var_name
string
Ключ поля в сущности, для дополнительных полей вида cf_123
data.*.enums
array
Варианты значений для списка, мультисписка и переключателя; для остальных полей пустой массив
data.*.enums.*.id
integer
Идентификатор варианта — передаётся как значение поля
data.*.enums.*.name
string
Название варианта

Возвращает поля сделок, контактов, компаний и справочников (системные и дополнительные), без пагинации. Список обёрнут в data. Ключ var_name используется как имя поля при создании и изменении сущностей.

Значения fields_type_id:

  • 1 — текстовая строка;
  • 2 — число;
  • 3 — флаг;
  • 4 — список;
  • 5 — мультисписок;
  • 6 — дата;
  • 7 — ссылка;
  • 9 — текстовая область;
  • 10 — переключатель;
  • 11 — дата и время;
  • 12 — телефоны;
  • 13 — e-mail адреса.

Ошибки: 422 — передано недопустимое значение type.

curl --location 'https://api.okocrm.com/v2/fields/?type=leads' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}'

Сообщения в мессенджерах

Список сообщений

GET https://api.okocrm.com/v2/messages/
Получить переписку
Параметр
Тип
Описание
Запрос
lead_id
integer
Идентификатор сделки
contact_id
integer
Идентификатор контакта
company_id
integer
Идентификатор компании
client_id
integer
Идентификатор диалога во входящих лидах
contact_messenger_id
integer
Идентификатор канала контакта — отфильтровать переписку по одному каналу
setting_id
integer
Идентификатор подключённой интеграции. Фильтрует только совместно с сущностью
last_id
integer
Курсор: last_id из предыдущего ответа. Вернутся сообщения после него
order
string
Сортировка по дате: asc или desc (в нижнем регистре), по умолчанию desc — сначала новые
limit
integer
Количество сообщений на страницу: по умолчанию 20, максимум 50
Ответ
data
array
Список сообщений
data.*.id
integer
Идентификатор сообщения
data.*.client_id
integer
Идентификатор диалога во входящих лидах
data.*.contact_messenger_id
integer
Идентификатор канала контакта
data.*.integration_id
integer
Идентификатор интеграции канала
data.*.setting_id
integer
Идентификатор подключённой интеграции
data.*.direction
string
Направление: outgoing — исходящее, incoming — входящее
data.*.text
string
Текст сообщения
data.*.created_at
integer
Создание сообщения в OkoCRM, unix timestamp
data.*.sent_at
integer
Фактическая отправка в мессенджер, unix timestamp
data.*.delivered_at
integer
Доставка клиенту, unix timestamp
data.*.read_at
integer
Прочтение клиентом, unix timestamp
data.*.edited_at
integer
Редактирование сообщения, unix timestamp
data.*.deleted_at
integer
Удаление сообщения, unix timestamp
data.*.author_type
string
Тип автора, значения в описании метода
data.*.author
object
Автор сообщения или null
data.*.author.id
integer
Идентификатор автора
data.*.author.name
string
Имя автора
data.*.files
array
Файлы сообщения
data.*.files.*.filename
string
Имя файла в хранилище
data.*.files.*.filename_original
string
Исходное имя файла
data.*.files.*.filesize
integer
Размер файла в байтах
data.*.files.*.url
string
Ссылка на файл
last_id
integer
Идентификатор последнего сообщения на странице (курсор для следующего запроса) или null
has_more
boolean
Есть ли ещё сообщения
limit
integer
Применённый размер страницы

Возвращает переписку в мессенджерах по сделке, контакту, компании или диалогу из входящих лидов. Обязательно передать один из параметров: lead_id, contact_id, company_id или client_id. Для сделки и компании выводятся сообщения всех их контактов.

Постраничный вывод по курсору: в ответе приходят last_id и has_more. Чтобы получить следующую страницу, передайте полученный last_id в запросе — вернутся сообщения, идущие строго после него в выбранном порядке сортировки. Пока has_more = true, есть ещё сообщения.

Все даты в ответе — unix timestamp.

Значения author_type: user — пользователь CRM, robot — робот, contact — контакт, client / unsorted_client — клиент из входящих лидов, chat_user — участник группового чата; null — автор не определён.

Ошибки:

  • 403 — нет доступа к сущности;
  • 404 — сущность не найдена или client_id относится к почтовому диалогу (почта этим методом не выводится);
  • 422 — ошибка валидации.

curl --location 'https://api.okocrm.com/v2/messages/?lead_id=1725&limit=20&order=desc' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}'

Отправить исходящее сообщение

POST https://api.okocrm.com/v2/messages/
Исходящее сообщение
Параметр
Тип
Описание
Запрос
client_id
integer
Идентификатор диалога во входящих лидах. Обязателен, если не передан contact_messenger_id
contact_messenger_id
integer
Идентификатор канала контакта. Обязателен, если не передан client_id
text
string
Текст сообщения, до 2000 символов. Обязателен, если не переданы files
files
array
Прикрепляемые файлы (files[]), до 20 МБ каждый. Обязательны, если не передан text
reply_message_id
integer
Идентификатор сообщения, на которое даётся ответ с цитированием
Ответ
data
array
Созданные сообщения
data.*.id
integer
Идентификатор сообщения
data.*.client_id
integer
Идентификатор диалога во входящих лидах
data.*.contact_messenger_id
integer
Идентификатор канала контакта
data.*.integration_id
integer
Идентификатор интеграции канала
data.*.setting_id
integer
Идентификатор подключённой интеграции
data.*.direction
string
Направление: всегда outgoing
data.*.text
string
Текст сообщения (null у сообщений с файлом)
data.*.created_at
integer
Создание сообщения, unix timestamp
data.*.author_type
string
Тип автора: user
data.*.author
object
Автор сообщения: id и name
data.*.files
array
Файл сообщения: filename, filename_original, filesize, url

Отправляет исходящее сообщение в мессенджер: в диалог из входящих лидов (client_id) или в канал контакта (contact_messenger_id). Нужно передать один из этих двух идентификаторов, а также text или files (хотя бы одно). Файлы передаются в multipart/form-data.

Каждый файл отправляется отдельным сообщением, поэтому в ответе возвращается список сообщений в data: сначала текстовое (если передан text), затем по одному на каждый файл.

Ошибки:

  • 404 — диалог или канал не найден;
  • 422 — ошибка валидации.

curl --location 'https://api.okocrm.com/v2/messages/' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'contact_messenger_id="3412"' \
--form 'text="Здравствуйте! Ваш заказ готов к выдаче."' \
--form 'files[]=@"/path/to/invoice.pdf"'

Финансы

Ссылки на оплату

GET https://api.okocrm.com/v2/payments/requests/
Список ссылок на оплату
Параметр
Тип
Описание
Запрос
page
integer
Номер страницы, по умолчанию 1
Ответ
data
array
Ссылки на оплату, 20 на странице
data.*.id
integer
ID ссылки на оплату
data.*.amount
string
Сумма строкой с двумя знаками после точки, например "12500.00"
data.*.number
string
Номер счёта
data.*.link
string
Ссылка на страницу оплаты. Последний сегмент — ULID
data.*.status_id
integer
Статус оплаты: null — выставлен, 1 — полная, 2 — частичная, 3 — переплата, 4 — возврат, 5 — частичный возврат
data.*.status
string
Название статуса на языке аккаунта
data.*.source_payment_id
integer
Источник: 1 — эквайринг, 2 — оплата по счёту, 3 — рассрочка банка, 4 — рассрочка компании, 5 — ручная оплата
data.*.source_payment
string
Название источника на языке аккаунта
data.*.type_payer_id
integer
Плательщик: 1 — физическое лицо, 2 — компания
data.*.type_payer
string
Название типа плательщика на языке аккаунта
data.*.payment_method_id
integer
Способ расчёта: 1 — предоплата 100%, 2 — предоплата, 3 — аванс, 4 — полный расчёт, 5 — частичный расчёт, или null
data.*.payment_method
string
Название способа расчёта или null
data.*.enable_invoice
integer
1 — к ссылке выставлен счёт, иначе null
data.*.is_manual
integer
1 — ручная оплата, иначе null
data.*.lead_id
integer
ID сделки или null
data.*.contact_id
integer
ID контакта или null
data.*.company_id
integer
ID компании или null
data.*.account_id
integer
ID расчётного счёта или null
data.*.signer_id
integer
ID подписанта или null
data.*.integration_id
integer
ID платёжной интеграции или null
data.*.comment
string
Комментарий или null
data.*.date_invoice
string
Дата счёта, Y-m-d, или null
data.*.date_pay_before
string
Оплатить до, Y-m-d, или null
data.*.paid_at
integer
Время оплаты, unix-время, или null
data.*.returned_at
integer
Время возврата, unix-время, или null
data.*.created_at
integer
Дата создания, unix-время
data.*.updated_at
integer
Дата изменения, unix-время
data.*.currency
object
Валюта: id, name, abbr, symbol, okv
data.*.lead
object
Сделка со значениями полей или null
data.*.contact
object
Контакт со значениями полей или null
data.*.company
object
Компания со значениями полей или null
current_page
integer
Текущая страница
per_page
integer
Размер страницы (20)
from
integer
Порядковый номер первой записи на странице или null
to
integer
Порядковый номер последней записи на странице или null
path
string
Адрес метода
first_page_url
string
Ссылка на первую страницу
next_page_url
string
Ссылка на следующую страницу. null — это последняя страница
prev_page_url
string
Ссылка на предыдущую страницу или null

Ссылки на оплату аккаунта, новые первыми (по убыванию id). Ручные оплаты (is_manual = 1) в список не попадают, но доступны по GET payments/requests/{id}.

На странице 20 записей. Пагинация простая, без общего количества: следующую страницу открывайте по next_page_url или с параметром page. Если next_page_url равен null, это последняя страница. Список не обёрнут в отдельный объект: записи лежат в data, параметры страницы — рядом на верхнем уровне.

Названия статуса, источника, плательщика и способа расчёта (status, source_payment, type_payer, payment_method) приходят на языке аккаунта. Для логики используйте поля с _id.

Ошибка 403 — нет права на просмотр ссылок на оплату.

curl -G 'https://api.okocrm.com/v2/payments/requests/' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}' \
  --data-urlencode 'page=2'
GET https://api.okocrm.com/v2/payments/requests/{id}
Получить по идентификатору
Параметр
Тип
Описание
Запрос
id *
integer
ID ссылки на оплату, в пути запроса
Ответ
data.id
integer
ID ссылки на оплату
data.amount
string
Сумма строкой с двумя знаками после точки, например "12500.00"
data.number
string
Номер счёта
data.link
string
Ссылка на страницу оплаты. Последний сегмент — ULID
data.status_id
integer
Статус оплаты: null — выставлен, 1 — полная, 2 — частичная, 3 — переплата, 4 — возврат, 5 — частичный возврат
data.status
string
Название статуса на языке аккаунта
data.source_payment_id
integer
Источник: 1 — эквайринг, 2 — оплата по счёту, 3 — рассрочка банка, 4 — рассрочка компании, 5 — ручная оплата
data.source_payment
string
Название источника на языке аккаунта
data.type_payer_id
integer
Плательщик: 1 — физическое лицо, 2 — компания
data.type_payer
string
Название типа плательщика на языке аккаунта
data.payment_method_id
integer
Способ расчёта: 1 — предоплата 100%, 2 — предоплата, 3 — аванс, 4 — полный расчёт, 5 — частичный расчёт, или null
data.payment_method
string
Название способа расчёта или null
data.enable_invoice
integer
1 — к ссылке выставлен счёт, иначе null
data.is_manual
integer
1 — ручная оплата, иначе null
data.lead_id
integer
ID сделки или null
data.contact_id
integer
ID контакта или null
data.company_id
integer
ID компании или null
data.account_id
integer
ID расчётного счёта или null
data.signer_id
integer
ID подписанта или null
data.integration_id
integer
ID платёжной интеграции или null
data.comment
string
Комментарий или null
data.date_invoice
string
Дата счёта, Y-m-d, или null
data.date_pay_before
string
Оплатить до, Y-m-d, или null
data.paid_at
integer
Время оплаты, unix-время, или null
data.returned_at
integer
Время возврата, unix-время, или null
data.created_at
integer
Дата создания, unix-время
data.updated_at
integer
Дата изменения, unix-время
data.currency
object
Валюта: id, name, abbr, symbol, okv
data.lead
object
Сделка со значениями полей или null
data.contact
object
Контакт со значениями полей или null
data.company
object
Компания со значениями полей или null

Ссылка на оплату по её id, включая ручные оплаты. Поля — как у элемента списка GET payments/requests/, ответ обёрнут в data.

Ошибки: 403 — нет права на просмотр ссылок на оплату; 404 — ссылка не найдена.

curl -X GET 'https://api.okocrm.com/v2/payments/requests/1542' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'
GET https://api.okocrm.com/v2/payments/requests/ulid/{ulid}
Получить по идентификатору ULID
Параметр
Тип
Описание
Запрос
ulid *
string
ULID ссылки на оплату, в пути запроса
Ответ
data.id
integer
ID ссылки на оплату
data.amount
string
Сумма строкой с двумя знаками после точки, например "12500.00"
data.number
string
Номер счёта
data.link
string
Ссылка на страницу оплаты. Последний сегмент — ULID
data.status_id
integer
Статус оплаты: null — выставлен, 1 — полная, 2 — частичная, 3 — переплата, 4 — возврат, 5 — частичный возврат
data.status
string
Название статуса на языке аккаунта
data.source_payment_id
integer
Источник: 1 — эквайринг, 2 — оплата по счёту, 3 — рассрочка банка, 4 — рассрочка компании, 5 — ручная оплата
data.source_payment
string
Название источника на языке аккаунта
data.type_payer_id
integer
Плательщик: 1 — физическое лицо, 2 — компания
data.type_payer
string
Название типа плательщика на языке аккаунта
data.payment_method_id
integer
Способ расчёта: 1 — предоплата 100%, 2 — предоплата, 3 — аванс, 4 — полный расчёт, 5 — частичный расчёт, или null
data.payment_method
string
Название способа расчёта или null
data.enable_invoice
integer
1 — к ссылке выставлен счёт, иначе null
data.is_manual
integer
1 — ручная оплата, иначе null
data.lead_id
integer
ID сделки или null
data.contact_id
integer
ID контакта или null
data.company_id
integer
ID компании или null
data.account_id
integer
ID расчётного счёта или null
data.signer_id
integer
ID подписанта или null
data.integration_id
integer
ID платёжной интеграции или null
data.comment
string
Комментарий или null
data.date_invoice
string
Дата счёта, Y-m-d, или null
data.date_pay_before
string
Оплатить до, Y-m-d, или null
data.paid_at
integer
Время оплаты, unix-время, или null
data.returned_at
integer
Время возврата, unix-время, или null
data.created_at
integer
Дата создания, unix-время
data.updated_at
integer
Дата изменения, unix-время
data.currency
object
Валюта: id, name, abbr, symbol, okv
data.lead
object
Сделка со значениями полей или null
data.contact
object
Контакт со значениями полей или null
data.company
object
Компания со значениями полей или null

Ссылка на оплату по ULID — последнему сегменту адреса из поля link (например, из ссылки, которую получил клиент). Поля — как у GET payments/requests/{id}, ответ обёрнут в data.

Ошибки: 403 — нет права на просмотр ссылок на оплату; 404 — ссылка не найдена.

curl -X GET 'https://api.okocrm.com/v2/payments/requests/ulid/01JAX4Q7M2R8T5V9W3Y6Z0B1CD' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Вебхуки

Список

GET https://api.okocrm.com/v2/webhooks/
Получить список вебхуков
Параметр
Тип
Описание
Ответ
data
array
Вебхуки
data.*.id
integer
ID вебхука
data.*.type
string
Тип события. client_message — входящее сообщение от клиента
data.*.status
string
active — запросы отправляются, failed — вебхук отключён после ошибок доставки, запросы не отправляются
data.*.secret
string
Ключ подписи запросов (HMAC-SHA256)
data.*.url
string
Адрес, на который отправляются запросы
data.*.errors_count
integer
Число неудачных доставок подряд. Сбрасывается в 0 после первой успешной
data.*.created_at
integer
Дата создания, unix-время

Все вебхуки аккаунта. В ответе есть ключ secret каждого вебхука — им проверяется подпись входящих запросов (см. раздел «Входящий запрос»).

Ошибка 403 — у пользователя токена нет права на изменение вебхуков.

curl -X GET 'https://api.okocrm.com/v2/webhooks/' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Создать

POST https://api.okocrm.com/v2/webhooks/
Добавить вебхук
Параметр
Тип
Описание
Запрос
type *
string
Тип события. Допустимо только client_message — входящее сообщение от клиента
url *
string
Адрес для запросов, до 255 символов, домен должен существовать
Ответ
data.id
integer
ID вебхука
data.type
string
Тип события
data.status
string
active — запросы отправляются, failed — вебхук отключён после ошибок доставки
data.secret
string
Ключ подписи запросов (HMAC-SHA256)
data.url
string
Адрес для запросов
data.errors_count
integer
Число неудачных доставок подряд
data.created_at
integer
Дата создания, unix-время

Регистрирует адрес, на который OkoCRM будет отправлять события. Сейчас доступно одно событие — client_message, входящее сообщение от клиента в мессенджере. Формат запроса и проверка подписи описаны в разделе «Входящий запрос».

Ключ подписи secret создаётся автоматически и возвращается в ответе — сохраните его. Его же можно получить позже в списке вебхуков.

Повторный запрос с теми же type и url не создаёт дубль, а возвращает существующий вебхук. Если он был отключён (failed) или копил ошибки, он снова включается: status = active, errors_count = 0.

Адрес должен быть корректным URL до 255 символов с существующим доменом.

Ошибки: 403 — нет права на изменение вебхуков; 422 — не передан или некорректен type или url.

curl -X POST 'https://api.okocrm.com/v2/webhooks/' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "type": "client_message",
    "url": "https://example.com/okocrm/webhook"
  }'

Удалить

DELETE https://api.okocrm.com/v2/webhooks/{webhook_id}/
Удалить вебхук по его идентификатору
Параметр
Тип
Описание
Запрос
webhook_id *
integer
ID вебхука, в пути запроса

Удаляет вебхук. Запросы на его адрес больше не отправляются.

Ответ 204 без тела.

Ошибки: 403 — нет права на изменение вебхуков; 404 — вебхук не найден.

curl -X DELETE 'https://api.okocrm.com/v2/webhooks/12/' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Входящий запрос

POST {your_webhook_url}
Запрос на адрес вебхука
Параметр
Тип
Описание
Запрос
Signature *
string
Заголовок: HMAC-SHA256 сырого тела запроса ключом secret вебхука, hex
User-Agent *
string
Заголовок: OkoCRM Webhook/1.0
webhook_type *
string
Тип события: client_message
data *
object
Входящее сообщение в формате GET messages
data.id *
integer
ID сообщения
data.client_id
integer
ID клиента во входящих лидах или null
data.contact_messenger_id
integer
ID канала контакта или null
data.integration_id
integer
ID интеграции (тип мессенджера)
data.setting_id
integer
ID подключённой интеграции
data.direction
string
Направление, всегда incoming
data.text
string
Текст сообщения или null
data.created_at
integer
Дата создания в OkoCRM, unix-время
data.sent_at
integer
Дата отправки, unix-время, или null
data.delivered_at
integer
Дата доставки, unix-время, или null
data.read_at
integer
Дата прочтения, unix-время, или null
data.edited_at
integer
Дата редактирования, unix-время, или null
data.deleted_at
integer
Дата удаления, unix-время, или null
data.author_type
string
Тип автора: contact, client, unsorted_client, chat_user или null
data.author
object
Автор сообщения или null
data.author.id
integer
ID автора
data.author.name
string
Имя автора
data.files
array
Вложения, пустой массив если их нет
data.files.*.filename
string
Имя файла в хранилище
data.files.*.filename_original
string
Исходное имя файла
data.files.*.filesize
integer
Размер в байтах
data.files.*.url
string
Ссылка на файл

Это не метод API OkoCRM, а запрос, который OkoCRM отправляет на адрес (url) зарегистрированного вебхука со статусом active.

Событие. client_message — клиент прислал входящее сообщение в подключённый мессенджер. Исходящие сообщения менеджеров вебхук не вызывают. Если вебхуков с этим типом несколько, запрос уходит на каждый адрес.

Запрос. Метод POST, тело — JSON. Заголовки:

  • Content-Type: application/json
  • User-Agent: OkoCRM Webhook/1.0
  • Signature — подпись тела: HMAC-SHA256 в hex (64 символа), ключ — secret вебхука из ответа POST webhooks или GET webhooks.

Проверка подписи. Посчитайте HMAC-SHA256 от сырого тела запроса (байты как пришли, без повторной сериализации JSON) с ключом secret и сравните с заголовком Signature функцией сравнения за постоянное время. Не совпало — отклоните запрос.

Тело. Сообщение в формате GET messages: объект data и поле webhook_type на верхнем уровне.

Ответ, таймаут и повторы. Доставка считается успешной при любом ответе с кодом 2xx. На ответ отводится 3 секунды. При ошибке, таймауте или коде не 2xx запрос повторяется: всего до 5 попыток, вторая — через 10 секунд, следующие — через 100 секунд. Отвечайте сразу, а долгую обработку делайте в фоне. Один и тот же запрос может прийти повторно — проверяйте data.id.

Отключение. Если все попытки не удались, у вебхука растёт errors_count. После 10 таких неудач вебхук переходит в статус failed и запросы на него больше не отправляются. Успешная доставка обнуляет errors_count. Чтобы снова включить отключённый вебхук, повторите POST webhooks с теми же type и url.

# Пример запроса, который OkoCRM отправляет на ваш адрес
curl -X POST 'https://example.com/okocrm/webhook' \
  -H 'Content-Type: application/json' \
  -H 'User-Agent: OkoCRM Webhook/1.0' \
  -H 'Signature: 5f2b6c1e9a0d4c7b8e3f1a2d6c9b0e4f7a1d3c5b8e2f6a9c0d4b7e1f3a5c8d2b' \
  -d '{
    "data": {
      "id": 918273,
      "client_id": null,
      "contact_messenger_id": 4410,
      "integration_id": 12,
      "setting_id": 377,
      "direction": "incoming",
      "text": "Добрый день! Заказ уже отправили?",
      "created_at": 1790580600,
      "sent_at": null,
      "delivered_at": null,
      "read_at": null,
      "edited_at": null,
      "deleted_at": null,
      "author_type": "contact",
      "author": {"id": 55, "name": "Ирина Соколова"},
      "files": []
    },
    "webhook_type": "client_message"
  }'
<?php
$secret = 'SECRET_ИЗ_ОТВЕТА_POST_WEBHOOKS';

$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $body, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);

if ($event['webhook_type'] === 'client_message') {
    // поставьте обработку в очередь и сразу ответьте
}

http_response_code(200);
const express = require('express');
const crypto = require('crypto');

const app = express();
const SECRET = process.env.OKOCRM_WEBHOOK_SECRET;

// Нужно сырое тело, без повторной сериализации JSON
app.post('/okocrm/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
  const signature = req.get('Signature') || '';

  if (signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));

  if (event.webhook_type === 'client_message') {
    // обработка в фоне
  }

  res.sendStatus(200);
});

app.listen(3000);
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ['OKOCRM_WEBHOOK_SECRET'].encode()


@app.post('/okocrm/webhook')
def okocrm_webhook():
    body = request.get_data()  # сырое тело
    expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    signature = request.headers.get('Signature', '')

    if not hmac.compare_digest(expected, signature):
        abort(401)

    event = json.loads(body)

    if event['webhook_type'] == 'client_message':
        pass  # обработка в фоне

    return '', 200

Входящие лиды

Прикрепить диалог к контакту

POST https://api.okocrm.com/v2/unsorted/attach/
Прикрепить диалог к контакту
Параметр
Тип
Описание
Запрос
client_id *
integer
Идентификатор диалога из входящих лидов
contact_id
integer
Идентификатор контакта. Обязателен, если не передан phone_id
phone_id
integer
Идентификатор телефона контакта. Обязателен, если не передан contact_id

Привязывает диалог из входящих лидов к существующему контакту: канал мессенджера сохраняется у контакта, переписка переносится в его карточку. Нужно передать contact_id или phone_id (идентификатор телефона контакта — тогда контакт определяется по телефону, а канал привязывается к этому номеру).

При успехе возвращается пустой ответ с кодом 204.

Ошибки:

  • 403 — нет права отвечать в этом диалоге или диалог групповой (групповые чаты привязать нельзя);
  • 404 — контакт не найден;
  • 422 — ошибка валидации.

curl --location 'https://api.okocrm.com/v2/unsorted/attach/' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'client_id="8841"' \
--form 'contact_id="1502"'

Шаблоны

Документы

GET https://api.okocrm.com/v2/templates/docs
Список шаблонов документов
Параметр
Тип
Описание
Ответ
data
array
Шаблоны документов
data.*.id
integer
ID шаблона
data.*.name
string
Название шаблона
data.*.category
object
Категория шаблона или null
data.*.category.id
integer
ID категории
data.*.category.name
string
Название категории
data.*.to_pdf
boolean
Документ конвертируется в PDF
data.*.numbering
boolean
У документов включена сквозная нумерация

Шаблоны документов аккаунта, отсортированные по категории и названию. id шаблона передаётся в POST templates/docs/generate.

Ошибка 403 — у пользователя нет доступа ни к шаблонам, ни к сделкам, контактам или компаниям.

curl -X GET 'https://api.okocrm.com/v2/templates/docs' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'
POST https://api.okocrm.com/v2/templates/docs/generate
Сгенерировать документ из шаблона
Параметр
Тип
Описание
Запрос
template_id *
integer
ID шаблона из GET templates/docs
lead_id
integer
ID сделки. Обязательно, если не переданы contact_id и company_id
contact_id
integer
ID контакта. Обязательно, если не переданы lead_id и company_id
company_id
integer
ID компании. Обязательно, если не переданы lead_id и contact_id
Ответ
data.id
integer
ID документа
data.number
string
Номер документа с префиксом из настроек шаблона
data.name
string
Название документа
data.template_id
integer
ID шаблона
data.item_type
string
Для какой сущности создан: lead, contact или company
data.item_id
integer
ID сущности
data.extension
string
Расширение файла: docx, xlsx, pdf…
data.filesize
integer
Размер файла в байтах
data.url
string
Ссылка на файл
data.author_id
integer
ID пользователя, создавшего документ
data.created_at
integer
Дата создания, unix-время

Создаёт документ по шаблону для сделки, контакта или компании — так же, как кнопка в карточке. Генерация синхронная: в ответе уже готовый файл и ссылка на него. Документ появляется в карточке сущности.

Передайте template_id и хотя бы одно из lead_id, contact_id, company_id. Если передано несколько, документ создаётся для первого по порядку: сделка, затем контакт, затем компания.

Ошибки: 403 — нет права на изменение сущности; 422 — не передан шаблон или сущность либо они не найдены в аккаунте; 500 — файл не удалось сформировать.

curl -X POST 'https://api.okocrm.com/v2/templates/docs/generate' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "template_id": 7,
    "lead_id": 2048
  }'

Справочники

Справочники товаров и услуг: «Товары», «Услуги» и созданные в аккаунте. Внутри справочника — дерево категорий и элементы с ценой, единицей измерения, НДС и дополнительными полями. Права берутся из раздела «Справочники» в роли пользователя токена: чтение — «Просмотр», создание, изменение и удаление элементов — соответствующие права. Если права нет, ответ 403.

Справочники и категории

GET https://api.okocrm.com/v2/guides
Список справочников
Параметр
Тип
Описание
Ответ
data
array
Список справочников
data.*.id
integer
ID справочника
data.*.name
string
Название
data.*.sort
integer
Порядок сортировки

Справочники аккаунта: «Товары», «Услуги» и созданные вручную. id справочника нужен для фильтра элементов и создания элемента.

curl -X GET 'https://api.okocrm.com/v2/guides' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'
GET https://api.okocrm.com/v2/guides/{guide_id}/categories
Категории справочника
Параметр
Тип
Описание
Запрос
guide_id *
integer
ID справочника, в пути запроса
Ответ
data
array
Список категорий
data.*.id
integer
ID категории
data.*.parent_id
integer
ID родительской категории, null у корневых
data.*.name
string
Название
data.*.sort
integer
Порядок внутри родителя

Плоский список категорий справочника. Дерево строится по parent_id: у корневых категорий он равен null.

Если справочник не найден — 404.

curl -X GET 'https://api.okocrm.com/v2/guides/11/categories' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Единицы измерения и НДС

GET https://api.okocrm.com/v2/guides/units
Единицы измерения
Параметр
Тип
Описание
Ответ
data
array
Список единиц измерения
data.*.id
integer
ID единицы
data.*.name
string
Полное название, например «Штука»
data.*.name_short
string
Сокращение, например «шт.»

Системные единицы измерения и созданные в аккаунте. id передаётся в unit_id при создании и изменении элемента справочника.

curl -X GET 'https://api.okocrm.com/v2/guides/units' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'
GET https://api.okocrm.com/v2/guides/nds
Ставки НДС
Параметр
Тип
Описание
Ответ
data
array
Список ставок
data.*.id
integer
ID ставки
data.*.name
string
Название ставки, например «20%», «20/120», «Без НДС»

Ставки НДС. id передаётся в nds_id при создании и изменении элемента справочника.

curl -X GET 'https://api.okocrm.com/v2/guides/nds' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Элементы справочника

GET https://api.okocrm.com/v2/guides/items
Список элементов справочников
Параметр
Тип
Описание
Запрос
guide_id
integer
Только из этого справочника. Без него — из всех
category_id
integer
Только из этой категории, без вложенных
search
string
Поиск по названию и артикулу, до 50 символов. Учитываются до 1000 лучших совпадений
cursor
string
Курсор из meta.next_cursor или meta.prev_cursor предыдущего ответа
Ответ
data
array
Элементы справочника
data.*.id
integer
ID элемента
data.*.guide_id
integer
ID справочника
data.*.category_id
integer
ID категории или null
data.*.name
string
Название
data.*.article
string
Артикул или null
data.*.price
float
Цена за единицу
data.*.currency
object
Валюта или null
data.*.currency.id
integer
ID валюты
data.*.currency.code
string
Код валюты: RUB, USD…
data.*.currency.symbol
string
Символ валюты: ₽, $…
data.*.unit
object
Единица измерения или null
data.*.unit.id
integer
ID единицы
data.*.unit.name
string
Полное название
data.*.unit.name_short
string
Сокращение
data.*.nds
object
Ставка НДС или null
data.*.nds.id
integer
ID ставки
data.*.nds.name
string
Название ставки
data.*.fields
object
Доп. поля по ключу var_name (cf_*) из GET fields?type=guides. Список — ID варианта, мультисписок — массив ID вариантов
data.*.created_at
integer
Дата создания, unix-время
data.*.updated_at
integer
Дата изменения, unix-время
links.next
string
Ссылка на следующую страницу или null
links.prev
string
Ссылка на предыдущую страницу или null
links.first
string
Всегда null
links.last
string
Всегда null
meta.path
string
Адрес метода
meta.per_page
integer
Размер страницы (50)
meta.next_cursor
string
Курсор следующей страницы. null — это последняя страница
meta.prev_cursor
string
Курсор предыдущей страницы или null

Элементы справочников, отсортированные по id: новые элементы в конце. На странице 50 элементов.

Пагинация курсорная, общего количества в ответе нет. Следующую страницу открывайте по ссылке links.next (фильтры в ней уже сохранены) или тем же запросом с параметром cursor из meta.next_cursor. Если next_cursor равен null, это последняя страница.

Ошибка 422: справочник guide_id не найден или передан некорректный cursor.

curl -G 'https://api.okocrm.com/v2/guides/items' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}' \
  --data-urlencode 'guide_id=11' \
  --data-urlencode 'search=Швабра'
GET https://api.okocrm.com/v2/guides/items/{id}
Получить элемент справочника
Параметр
Тип
Описание
Запрос
id *
integer
ID элемента, в пути запроса
Ответ
data.id
integer
ID элемента
data.guide_id
integer
ID справочника
data.category_id
integer
ID категории или null
data.name
string
Название
data.article
string
Артикул или null
data.price
float
Цена за единицу
data.currency
object
Валюта: id, code (RUB, USD…), symbol (₽, $…) или null
data.unit
object
Единица измерения: id, name, name_short или null
data.nds
object
Ставка НДС: id, name или null
data.fields
object
Доп. поля по ключу var_name (cf_*). Список — ID варианта, мультисписок — массив ID вариантов
data.created_at
integer
Дата создания, unix-время
data.updated_at
integer
Дата изменения, unix-время

Элемент справочника по его ID. Если элемент не найден — 404.

curl -X GET 'https://api.okocrm.com/v2/guides/items/5012' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'
POST https://api.okocrm.com/v2/guides/items
Создать элемент справочника
Параметр
Тип
Описание
Запрос
guide_id *
integer
ID справочника
name *
string
Название, до 500 символов
price *
float
Цена за единицу
unit_id *
integer
ID единицы измерения из GET guides/units
currency_id
integer
ID валюты. По умолчанию — валюта аккаунта
category_id
integer
ID категории того же справочника
article
string
Артикул
nds_id
integer
ID ставки НДС из GET guides/nds
cf_*
string
Доп. поля по var_name из GET fields?type=guides. Для списка — ID варианта, для мультисписка — массив ID вариантов
Ответ
data
object
Созданный элемент в формате GET guides/items/{id}
data.id
integer
ID созданного элемента

Создаёт товар или услугу в справочнике. В ответе возвращается созданный элемент в том же формате, что в GET guides/items/{id}.

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

curl -X POST 'https://api.okocrm.com/v2/guides/items' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "guide_id": 11,
    "name": "Смартфон X1",
    "price": 24990,
    "unit_id": 1,
    "category_id": 41,
    "article": "X1-128",
    "nds_id": 6
  }'
PUT https://api.okocrm.com/v2/guides/items/{id}
Изменить элемент справочника
Параметр
Тип
Описание
Запрос
id *
integer
ID элемента, в пути запроса
name
string
Название, до 500 символов. Не может быть null
price
float
Цена за единицу. Не может быть null
unit_id
integer
ID единицы измерения. Не может быть null
currency_id
integer
ID валюты. Не может быть null
category_id
integer
ID категории того же справочника, null — убрать из категории
article
string
Артикул, null — очистить
nds_id
integer
ID ставки НДС, null — очистить
cf_*
string
Доп. поля по var_name, null — очистить
Ответ
data
object
Обновлённый элемент в формате GET guides/items/{id}

Меняются только переданные поля, остальные остаются как были. Чтобы очистить необязательное поле, передайте null. Перенести элемент в другой справочник нельзя.

В ответе возвращается обновлённый элемент в формате GET guides/items/{id}.

Ошибки: 404 — элемент не найден; 422 — категория из другого справочника или null в обязательном поле.

curl -X PUT 'https://api.okocrm.com/v2/guides/items/5012' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "price": 22990,
    "article": null
  }'
DELETE https://api.okocrm.com/v2/guides/items/{id}
Удалить элемент справочника
Параметр
Тип
Описание
Запрос
id *
integer
ID элемента, в пути запроса

Удаляет элемент справочника. Как и в интерфейсе, вместе с ним удаляются его позиции во всех сделках.

Ответ 204 без тела. Если элемент не найден — 404.

curl -X DELETE 'https://api.okocrm.com/v2/guides/items/5012' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Вспомогательные данные

Список городов

GET https://api.okocrm.com/v2/cities/
Получить список городов
Параметр
Тип
Описание
Запрос
page
integer
Номер страницы, по умолчанию 1
Ответ
data
array
Города, 100 на странице
data.*.city_id
integer
ID города
data.*.name
string
Название
current_page
integer
Текущая страница
per_page
integer
Размер страницы (100)
from
integer
Порядковый номер первой записи на странице или null
to
integer
Порядковый номер последней записи на странице или null
path
string
Адрес метода
first_page_url
string
Ссылка на первую страницу
next_page_url
string
Ссылка на следующую страницу. null — это последняя страница
prev_page_url
string
Ссылка на предыдущую страницу или null

Справочник городов, по алфавиту.

На странице 100 городов. Пагинация простая, без общего количества: следующую страницу открывайте по next_page_url или с параметром page. Если next_page_url равен null, это последняя страница. Города лежат в data, параметры страницы — рядом на верхнем уровне.

curl -G 'https://api.okocrm.com/v2/cities/' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}' \
  --data-urlencode 'page=3'

Клиенты

Раздел повторных продаж. Клиент — человек или компания, которые покупают повторно: он всегда связан хотя бы с одним контактом или компанией CRM, у него есть покупки — оплаченные, предстоящие и отменённые. По ближайшей предстоящей покупке клиент попадает на этап воронки раздела «Клиенты». Права берутся из раздела «Клиенты» в роли пользователя токена: чтение — «Просмотр» (все / отдел / только свои по ответственному клиента), создание и изменение — «Редактирование». Клиент, которого пользователь не видит, отдаёт 404. На тарифе без раздела «Клиенты» — 451.

Клиенты

GET https://api.okocrm.com/v2/customers
Список клиентов
Параметр
Тип
Описание
Запрос
search
string
Поиск по названию клиента и по имени, телефону, email его контактов и компаний, до 50 символов
filter[customers][user_id]
integer
Ответственный: ID или массив ID
filter[customers][paused]
integer
1 — только на паузе, 0 — без стоящих на паузе
filter[customers][purchases_count]
object
Число оплаченных покупок: {from, to}
filter[customers][purchases_sum]
object
Сумма оплаченных покупок: {from, to}
filter[customers][avg_check]
object
Средний чек: {from, to}
filter[customers][last_purchase_at]
object
Дата последней покупки: {from, to}, Y-m-d
filter[customers][expected_at]
object
Срок ближайшей покупки: {from, to}, Y-m-d
filter[contacts][...]
object
Фильтр по полям контактов клиента, формат как у фильтра сделок
filter[companies][...]
object
Фильтр по полям компаний клиента, формат как у фильтра сделок
cursor
string
Курсор из meta.next_cursor предыдущего ответа
sort
string
Порядок по id: asc — новые в конце (по умолчанию), desc — новые первыми
Ответ
data
array
Клиенты
data.*.id
integer
ID клиента
data.*.name
string
Название
data.*.user_id
integer
ID ответственного или null
data.*.contacts
array
Контакты клиента, главный первым
data.*.contacts.*.id
integer
ID контакта
data.*.contacts.*.name
string
Имя контакта
data.*.contacts.*.main
boolean
Главный контакт
data.*.contacts.*.notify
boolean
Контакт получает автоматические касания
data.*.companies
array
Компании клиента, главная первой
data.*.companies.*.id
integer
ID компании
data.*.companies.*.name
string
Название компании
data.*.companies.*.main
boolean
Главная компания
data.*.purchases_count
integer
Число оплаченных покупок
data.*.purchases_sum
float
Сумма оплаченных покупок
data.*.avg_check
float
Средний чек
data.*.first_purchase_at
integer
Первая оплата, unix-время, или null
data.*.last_purchase_at
integer
Последняя оплата, unix-время, или null
data.*.expected_at
string
Срок ближайшей предстоящей покупки, Y-m-d, или null
data.*.expected_source
string
Откуда срок: series — регулярная серия, manual — задан вручную, forecast — прогноз по истории покупок, или null
data.*.paused_until
string
Клиент на паузе до этой даты, Y-m-d, или null
data.*.created_at
integer
Дата создания, unix-время
data.*.updated_at
integer
Дата изменения, unix-время
links.next
string
Ссылка на следующую страницу или null
links.prev
string
Ссылка на предыдущую страницу или null
links.first
string
Всегда null
links.last
string
Всегда null
meta.path
string
Адрес метода
meta.per_page
integer
Размер страницы (50)
meta.next_cursor
string
Курсор следующей страницы. null — это последняя страница
meta.prev_cursor
string
Курсор предыдущей страницы или null

Клиенты, отсортированные по id в направлении sort: по умолчанию asc — новые в конце, desc — новые первыми. При search сначала идут более точные совпадения. На странице 50 клиентов.

Для синхронизации удобнее asc: новые клиенты добавляются в конец, и выгрузку можно продолжать с сохранённого курсора.

Поиск и фильтры — в том же формате, что в разделе «Клиенты» интерфейса. Фильтр по полям контактов и компаний клиента: filter[contacts][var_name], filter[companies][var_name] — как у фильтра сделок, ключи var_name из GET fields.

Пагинация курсорная, общего количества в ответе нет. Следующую страницу открывайте по ссылке links.next (фильтры и сортировка в ней уже сохранены) или тем же запросом с параметром cursor из meta.next_cursor. Если next_cursor равен null, это последняя страница. Курсор из списка без поиска и фильтров не подходит к запросу с search/filter и наоборот. При смене search, filter или sort начинайте без cursor.

При search/filter на странице может быть меньше 50 клиентов (даже ни одного), если индекс поиска ещё не догнал изменения. Конец списка — только next_cursor = null.

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

Ошибка 422: некорректный cursor, filter или sort.

curl -G 'https://api.okocrm.com/v2/customers' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}' \
  --data-urlencode 'search=Ромашка' \
  --data-urlencode 'filter[customers][purchases_count][from]=2'
GET https://api.okocrm.com/v2/customers/{id}
Получить клиента
Параметр
Тип
Описание
Запрос
id *
integer
ID клиента, в пути запроса
Ответ
data.id
integer
ID клиента
data.name
string
Название
data.user_id
integer
ID ответственного или null
data.contacts
array
Контакты, главный первым: id, name, main — главный, notify — получает автоматические касания
data.companies
array
Компании, главная первой: id, name, main
data.purchases_count
integer
Число оплаченных покупок
data.purchases_sum
float
Сумма оплаченных покупок
data.avg_check
float
Средний чек
data.first_purchase_at
integer
Первая оплата, unix-время, или null
data.last_purchase_at
integer
Последняя оплата, unix-время, или null
data.expected_at
string
Срок ближайшей предстоящей покупки, Y-m-d, или null
data.expected_source
string
Откуда срок: series, manual, forecast или null
data.paused_until
string
На паузе до этой даты, Y-m-d, или null
data.created_at
integer
Дата создания, unix-время
data.updated_at
integer
Дата изменения, unix-время

Клиент по его ID. Если клиент не найден или не виден пользователю по правам — 404.

curl -X GET 'https://api.okocrm.com/v2/customers/310' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'
POST https://api.okocrm.com/v2/customers
Создать клиента
Параметр
Тип
Описание
Запрос
contact_id
integer
ID контакта. Обязательно, если не передан company_id
company_id
integer
ID компании. Обязательно, если не передан contact_id
name
string
Название, до 255 символов. По умолчанию — название главной компании или имя контакта
user_id
integer
ID ответственного. По умолчанию — ответственный контакта или компании
Ответ
data
object
Созданный или найденный клиент в формате GET customers/{id}
data.id
integer
ID клиента

Создаёт клиента из контакта или компании — передаётся ровно одно из двух. Переданы оба — 422 с ошибкой в company_id. Вторую связь добавляют через POST customers/{id}/contacts или POST customers/{id}/companies.

При создании по контакту к клиенту привязываются и компании этого контакта. Если у контакта или компании клиент уже есть, новый не создаётся: ответ 200 с существующим клиентом, name и user_id к нему не применяются (их меняют через PUT customers/{id}). Новый клиент — ответ 201. Поэтому запрос можно безопасно повторять.

Привязать можно только контакт или компанию, которые пользователь видит в CRM, иначе 403. Компании, которые подтягиваются от контакта автоматически, этой проверке не подлежат. Если у контакта или компании уже есть клиент, которого пользователь не может просматривать или изменять по своим правам, ответ 403, и к клиенту ничего не привязывается.

Если пользователь видит только своих клиентов, а ответственным созданного клиента стал другой сотрудник, клиент создаётся, но дальше пользователю не виден.

В ответе — клиент в формате GET customers/{id}.

curl -X POST 'https://api.okocrm.com/v2/customers' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "contact_id": 55,
    "name": "ООО Ромашка",
    "user_id": 12
  }'
PUT https://api.okocrm.com/v2/customers/{id}
Изменить клиента
Параметр
Тип
Описание
Запрос
id *
integer
ID клиента, в пути запроса
name
string
Название, до 255 символов. Не может быть пустым
user_id
integer
ID ответственного, null — убрать ответственного
Ответ
data
object
Обновлённый клиент в формате GET customers/{id}

Меняет название и ответственного клиента. Меняются только переданные поля, остальные остаются как были.

В ответе — обновлённый клиент в формате GET customers/{id}.

Ошибки: 404 — клиент не найден или не виден пользователю; 403 — нет права на изменение этого клиента; 422 — пустое название или ответственный не из аккаунта.

curl -X PUT 'https://api.okocrm.com/v2/customers/310' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "name": "ООО Ромашка (опт)",
    "user_id": 14
  }'

Контакты и компании клиента

POST https://api.okocrm.com/v2/customers/{id}/contacts
Привязать контакт к клиенту
Параметр
Тип
Описание
Запрос
id *
integer
ID клиента, в пути запроса
contact_id *
integer
ID контакта
main
boolean
Сделать главным контактом
notify
boolean
Отправлять контакту автоматические касания. У главного всегда включено
Ответ
data
object
Клиент в формате GET customers/{id}

Привязывает контакт к клиенту. Если контакт уже привязан, меняются переданные флаги.

Первый привязанный контакт становится главным. У главного контакта автоматические касания всегда включены.

Для новой связи контакт должен быть виден пользователю в CRM, иначе 403. Флаги уже привязанного контакта меняются без этой проверки.

В ответе — клиент в формате GET customers/{id}.

Ошибки: 404 — клиент не найден или не виден; 403 — нет права на изменение клиента или контакт не виден; 422 — контакт не найден.

curl -X POST 'https://api.okocrm.com/v2/customers/310/contacts' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "contact_id": 56,
    "main": true
  }'
DELETE https://api.okocrm.com/v2/customers/{id}/contacts/{contact_id}
Отвязать контакт от клиента
Параметр
Тип
Описание
Запрос
id *
integer
ID клиента, в пути запроса
contact_id *
integer
ID контакта, в пути запроса

Отвязывает контакт от клиента. Если отвязан главный, главным становится следующий.

Ответ 204 без тела.

Ошибки: 404 — клиент не найден или контакт к нему не привязан; 422 — это последний контакт или компания клиента: клиент без связей не остаётся.

curl -X DELETE 'https://api.okocrm.com/v2/customers/310/contacts/56' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'
POST https://api.okocrm.com/v2/customers/{id}/companies
Привязать компанию к клиенту
Параметр
Тип
Описание
Запрос
id *
integer
ID клиента, в пути запроса
company_id *
integer
ID компании
main
boolean
Сделать главной компанией
Ответ
data
object
Клиент в формате GET customers/{id}

Привязывает компанию к клиенту. Если компания уже привязана, меняется переданный флаг.

Первая привязанная компания становится главной.

Для новой связи компания должна быть видна пользователю в CRM, иначе 403. Флаг уже привязанной компании меняется без этой проверки.

В ответе — клиент в формате GET customers/{id}.

Ошибки: 404 — клиент не найден или не виден; 403 — нет права на изменение клиента или компания не видна; 422 — компания не найдена.

curl -X POST 'https://api.okocrm.com/v2/customers/310/companies' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "company_id": 81
  }'
DELETE https://api.okocrm.com/v2/customers/{id}/companies/{company_id}
Отвязать компанию от клиента
Параметр
Тип
Описание
Запрос
id *
integer
ID клиента, в пути запроса
company_id *
integer
ID компании, в пути запроса

Отвязывает компанию от клиента. Если отвязана главная, главной становится следующая.

Ответ 204 без тела.

Ошибки: 404 — клиент не найден или компания к нему не привязана; 422 — это последний контакт или компания клиента: клиент без связей не остаётся.

curl -X DELETE 'https://api.okocrm.com/v2/customers/310/companies/81' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}'

Покупки клиента

GET https://api.okocrm.com/v2/customers/{id}/purchases
Покупки клиента
Параметр
Тип
Описание
Запрос
id *
integer
ID клиента, в пути запроса
status
string
paid — оплаченные, expected — предстоящие, cancelled — отменённые. Без параметра — все
cursor
string
Курсор из meta.next_cursor или meta.prev_cursor предыдущего ответа
Ответ
data
array
Покупки
data.*.id
integer
ID покупки
data.*.customer_id
integer
ID клиента
data.*.status
string
paid — оплачена, expected — предстоящая, cancelled — отменена
data.*.origin
string
Откуда покупка: deal — из сделки, series — по регулярной серии, forecast — прогноз, manual — вручную или через API
data.*.title
string
Название или null
data.*.amount
float
Сумма
data.*.currency
object
Валюта
data.*.currency.id
integer
ID валюты
data.*.currency.code
string
Код валюты: RUB, USD…
data.*.currency.symbol
string
Символ валюты: ₽, $…
data.*.expected_at
string
Срок предстоящей покупки, Y-m-d, или null
data.*.paid_at
integer
Время оплаты, unix-время, или null
data.*.stage
object
Этап воронки раздела «Клиенты» или null
data.*.stage.id
integer
ID этапа
data.*.stage.name
string
Название этапа
data.*.series_id
integer
ID регулярной серии или null
data.*.lead_id
integer
ID сделки-источника или null
data.*.user_id
integer
ID ответственного за покупку или null
data.*.created_at
integer
Дата создания, unix-время
data.*.updated_at
integer
Дата изменения, unix-время
links.next
string
Ссылка на следующую страницу или null
links.prev
string
Ссылка на предыдущую страницу или null
meta.next_cursor
string
Курсор следующей страницы. null — это последняя страница
meta.prev_cursor
string
Курсор предыдущей страницы или null
links.first
string
Всегда null
links.last
string
Всегда null
meta.path
string
Адрес метода
meta.per_page
integer
Размер страницы (50)

Покупки клиента: оплаченные, предстоящие и отменённые. Сортировка по id, на странице 50 покупок, пагинация курсорная — как у списка клиентов.

Этап воронки (stage) выставляет система по сроку покупки, изменить его напрямую нельзя. У оплаченных и отменённых покупок — этап, на котором покупка была. null — раздел ещё не настроен или этап ещё не рассчитан.

Ошибки: 404 — клиент не найден или не виден пользователю; 422 — некорректный status или cursor.

curl -G 'https://api.okocrm.com/v2/customers/310/purchases' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer {token}' \
  --data-urlencode 'status=expected'
POST https://api.okocrm.com/v2/customers/{id}/purchases
Создать покупку
Параметр
Тип
Описание
Запрос
id *
integer
ID клиента, в пути запроса
title
string
Название, до 255 символов
amount
float
Сумма, от 0 до 9 999 999 999.99. По умолчанию 0
expected_at
string
Срок предстоящей покупки, Y-m-d. Без срока покупка попадает на первый этап. Для paid не учитывается
currency_id
integer
ID валюты. Не передана или null — валюта аккаунта
user_id
integer
ID ответственного за покупку
status
string
expected — предстоящая (по умолчанию), paid — уже оплаченная
paid_at
string
Время оплаты, Y-m-d H:i (UTC), не позже текущего момента. Обязательно при status = paid
Ответ
data
object
Созданная покупка в формате элемента GET customers/{id}/purchases
data.id
integer
ID созданной покупки

Создаёт покупку клиента с источником manual. По умолчанию создаётся предстоящая покупка (status = expected), так же как кнопкой в карточке клиента. Если передать status = paid, создаётся уже оплаченная покупка. Так удобно переносить историю покупок из другой системы.

У предстоящей покупки этап воронки рассчитывается в фоне через несколько секунд, поэтому в ответе stage может быть null. Если у клиента была прогнозная покупка (origin = forecast), новая предстоящая её заменяет.

Оплаченная покупка учитывается в показателях клиента (purchases_count, purchases_sum, last_purchase_at и др.) и в прогнозе следующей покупки. Показатели пересчитываются в фоне.

Повторный запрос создаёт ещё одну покупку. Перед переносом истории проверьте, какие покупки у клиента уже есть: GET customers/{id}/purchases?status=paid.

Ответ 201 с покупкой в формате элемента GET customers/{id}/purchases.

Ошибки: 404 — клиент не найден или не виден; 403 — нет права на изменение клиента; 422 — некорректная сумма или статус, дата не в нужном формате, время оплаты в будущем или не передано для paid, валюта или ответственный не найдены.

curl -X POST 'https://api.okocrm.com/v2/customers/310/purchases' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "title": "Продление КАСКО",
    "amount": 42000,
    "expected_at": "2026-11-01"
  }'

# Оплаченная покупка
curl -X POST 'https://api.okocrm.com/v2/customers/310/purchases' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {token}' \
  -d '{
    "status": "paid",
    "title": "КАСКО 2025",
    "amount": 39000,
    "paid_at": "2025-10-28 14:30"
  }'

Проекты

Задачи проекта

POST https://api.okocrm.com/v2/projects/tasks
Создать задачу в проекте
Параметр
Тип
Описание
Запрос
project_id *
integer
Идентификатор проекта
stage_id *
integer
Идентификатор этапа (колонки) проекта
name *
string
Название задачи, до 255 символов
text
string
Описание задачи, до 20000 символов
executor_id
integer
Идентификатор исполнителя (пользователя)
date_start
string
Дата начала в формате Y-m-d H:i
date_finish
string
Дата окончания в формате Y-m-d H:i, не раньше date_start
watchers
array
Массив идентификаторов пользователей-наблюдателей
tags
array
Массив идентификаторов тегов проекта
Ответ
id
integer
Идентификатор созданной задачи

Создаёт задачу в указанном проекте и этапе. Автором задачи становится пользователь, от имени которого выполнен запрос.

Даты передаются в формате Y-m-d H:i (например 2026-10-05 10:00) в часовом поясе аккаунта. Дата окончания не может быть раньше даты начала.

Ошибки:

  • 401 — неверный или отсутствующий токен;
  • 403 — нет прав на создание задачи в этом проекте;
  • 422 — ошибка валидации (проект, этап, пользователь или тег не найдены, неверный формат даты и т.п.).

curl --location 'https://api.okocrm.com/v2/projects/tasks' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}' \
--form 'project_id="12"' \
--form 'stage_id="48"' \
--form 'name="Подготовить коммерческое предложение"' \
--form 'text="Собрать цены по трём тарифам и согласовать скидку"' \
--form 'executor_id="7"' \
--form 'date_start="2026-10-05 10:00"' \
--form 'date_finish="2026-10-07 18:00"' \
--form 'watchers[]="3"' \
--form 'tags[]="15"'

# Ответ
{"id": 1043}

Комментарии к задачам

POST https://api.okocrm.com/v2/projects/comments
Добавить комментарий к задаче проекта
Параметр
Тип
Описание
Запрос
task_id *
integer
Идентификатор задачи проекта
text *
string
Текст комментария, до 2000 символов
Ответ
id
integer
Идентификатор созданного комментария

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

Ошибки:

  • 401 — неверный или отсутствующий токен;
  • 403 — нет доступа к задаче;
  • 422 — задача не найдена или текст пустой либо длиннее 2000 символов.

curl --location 'https://api.okocrm.com/v2/projects/comments' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}' \
--form 'task_id="1043"' \
--form 'text="КП отправлено клиенту, ждём ответа до пятницы"'

# Ответ
{"id": 5821}

Командный чат

Сообщения

POST https://api.okocrm.com/v2/chat/message
Написать сообщение в командный чат
Параметр
Тип
Описание
Запрос
dialog_id
integer
Идентификатор диалога. Обязателен, если не передан user_id или thread_id
user_id
integer
Идентификатор пользователя для личного диалога. Обязателен, если не передан dialog_id или thread_id
thread_id
integer
Идентификатор сообщения, в тред которого нужно ответить. Обязателен, если не передан dialog_id или user_id
reply_to_message_id
integer
Идентификатор сообщения, на которое отвечаем (цитата)
text *
string
Текст сообщения
Ответ
data
object
Результат
data.message
object
Созданное сообщение
data.message.id
integer
Идентификатор сообщения

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

  • dialog_id — в существующий диалог;
  • user_id — в личный диалог с пользователем (диалог создаётся, если его нет);
  • thread_id — в тред (ветку ответов) к сообщению.

Ошибки:

  • 401 — неверный или отсутствующий токен;
  • 403 — нет доступа к командному чату;
  • 404 — диалог не найден или недоступен пользователю;
  • 422 — ошибка валидации (не передан получатель, пустой текст, сообщение для треда недоступно пользователю).

curl --location 'https://api.okocrm.com/v2/chat/message' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}' \
--form 'user_id="7"' \
--form 'text="Клиент подтвердил встречу на завтра в 11:00"'

# Ответ
{"data": {"message": {"id": 90412}}}

Телефония: АТС-виджеты

Подключение облачной АТС к OkoCRM через виджет OkoStore — на уровне штатных интеграций: трубка у номера, звонок из карточки, уведомление о звонке, запись и транскрибация в ленте, правила по входящим, исходящим и пропущенным. Код внутри OkoCRM писать не нужно: вы описываете блок telephony в config.json виджета, принимаете вебхуки от OkoCRM и вызываете несколько эндпоинтов из этой группы.

Начните со страницы Интеграция телефонии с OkoCRM — там путь разработчика по шагам: что сделать, в каком порядке и какой результат ждать на каждом шаге. Разделы ниже — справочник по каждому запросу.

Схема подключения.

  1. Вы публикуете виджет в OkoStore с блоком telephony в config.json. OkoCRM автоматически даёт такому виджету категорию «Телефония»: трубки у номеров телефона, стандартные вкладки настроек (сотрудники, внешние линии, правила).
  2. Клиент устанавливает виджет и сохраняет настройки (например, вводит ключ API вашей АТС). Как только настройки сохранены, OkoCRM присылает на ваш webhook_url событие installed с токеном подключения.
  3. Ваш сервер проверяет доступ к своей АТС и вызывает PUT extensions и PUT numbers — передаёт список внутренних номеров сотрудников и внешних линий клиента. Если что-то не так — сообщает через POST status.
  4. По каждому звонку ваша АТС вызывает POST route (кому перевести входящий на общий номер) и шлёт события звонка через POST events: call.answered / call.started → call.ended, либо call.missed.
  5. Когда менеджер жмёт трубку в CRM, OkoCRM обращается к вашему click_to_call_url — это команда вашей АТС инициировать звонок.
  6. Клиент изменил настройки — вы получаете settings_updated. Удалил виджет — uninstalled, токен после этого не работает.

Блок telephony в config.json виджета.

"telephony": {
  "webhook_url": "https://pbx.example.com/okocrm/events",
  "click_to_call_url": "https://pbx.example.com/okocrm/call"
}
  • Оба поля обязательны, оба адреса — только https://, реальный внешний адрес: без логина и пароля в строке, без localhost, без локальных и служебных диапазонов (127.0.0.1, 10.0.0.0/8, 192.168.0.0/16, 169.254.0.0/16 и т.п.) — такие адреса OkoCRM отклонит.
  • Один виджет — либо АТС, либо SMS-шлюз: блоки telephony и sms в одном config.json вместе быть не могут.
  • Если блок некорректен, версия виджета не публикуется — вы получите ту же ошибку, что при любом другом битом config.json.
  • Блок работает и для личного (personal) виджета, который вы ставите себе для отладки без модерации, и для публичного — после обычной модерации.
  • script.js обязателен как файл, но может быть пустым модулем: export default async function () {}. Вся логика живёт на вашем сервере.

Токен подключения. Выпускается автоматически при установке виджета и живёт, пока подключение активно. Формат — {ulid}.{секрет}; не разбирайте его, храните строкой целиком. Впервые вы получаете токен в теле вебхука installed (поле token). Отдельного способа запросить токен через API нет. Если токен потерялся, администратор клиента видит его во вкладке «Подключение» настроек виджета и может нажать «Перевыпустить» — вы получите новый installed с новым токеном, старый сразу перестанет работать.

Токен передаётся только в заголовке:

Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f
Accept: application/json

Никогда не передавайте токен в адресе запроса — такие запросы не пройдут проверку. Заголовок Accept: application/json советуем слать всегда: тогда любая ошибка гарантированно придёт в JSON.

Секретный ключ виджета (private_key) — отдельный от токена, один на весь виджет, а не на конкретного клиента. Он находится на странице виджета: OkoStore → «Мои виджеты» → ваш виджет → строка «Секретный ключ». На экране ключ скрыт звёздочками, по клику копируется целиком; там же его можно сбросить — после сброса старый ключ перестаёт работать сразу у всех клиентов, установивших виджет. Ключ нужен для проверки подписи вебхуков, которые шлёт вам OkoCRM.

Базовый адрес API: https://webhook.okocrm.com/v4/telephony/. Все запросы — JSON. Лимит: не больше 5 запросов в секунду и 600 в минуту на каждый эндпоинт в рамках одного подключения (токена); при превышении — 429 и заголовок Retry-After.

Коды ошибок:

  • 401 — токен не передан, неверного формата или секрет не совпал: {"message": "Не авторизован."};
  • 402 — у клиента не оплачен тариф (демо закончилось, платный истёк или бесплатный тариф). Запрос не обработан, повторите позже. Тело — простой текст, не JSON;
  • 404 — токен не найден, подключение отключено или удалено, либо это токен виджета другого типа (например, SMS-шлюза): {"message": "Страница не найдена."}. Также неизвестный путь и любой GET;
  • 405 — неподдерживаемый метод на существующем пути, например POST extensions вместо PUT;
  • 409 — только POST status и POST events: подключение создано, но клиент ещё не сохранил настройки виджета, рано слать события: {"message": "Сначала сохраните настройки подключения."};
  • 422 — тело не прошло валидацию: {"message": "The given data was invalid.", "errors": {"поле": ["текст ошибки"]}};
  • 429 — превышен лимит запросов: {"message": "Too Many Attempts."}.

Пока по подключению не приходил installed, не вызывайте POST status и POST events — они ответят 409. PUT extensions, PUT numbers и POST route такой проверки не делают.

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

  1. webhook_url и click_to_call_url в config.json — оба https://, оба реально отвечают с внешнего адреса.
  2. Виджет установлен, настройки сохранены → пришёл installed с корректной подписью в Signature и валидным token.
  3. В ответ на installed вы вызвали PUT extensions и PUT numbers — в CRM во вкладке «Сотрудники» видны присланные номера, во вкладке «Звонки» — внешние линии.
  4. POST status с error гасит трубки в CRM, ok — включает обратно.
  5. POST route отвечает route_to для клиента с ответственным менеджером и null, когда правило не применяется.
  6. Полный цикл событий: входящий принятый (call.answered → call.ended), исходящий (call.started → call.ended), пропущенный (call.missed) — во всех случаях звонок появляется в ленте клиента.
  7. Запись разговора: либо сразу в call.ended, либо позже отдельным call.recorded — плеер и транскрибация появляются в карточке звонка.
  8. Нажатие трубки в карточке контакта доходит до вашего click_to_call_url с ожидаемыми полями, звонок инициируется, а при ошибке ваш {"message": "..."} виден менеджеру.
  9. Повторная отправка одного и того же события в течение нескольких секунд не создаёт дублей в ленте.
  10. Изменение настроек виджета присылает settings_updated; удаление — uninstalled, и старый токен после этого возвращает 404.
  11. Перевыпуск токена во вкладке «Подключение» присылает новый installed — вы обновили токен у себя.

После этого публикуйте виджет как публичный — он пройдёт стандартную модерацию OkoStore.

Запросы от OkoCRM

OkoCRM сама обращается к адресам из блока telephony вашего виджета в четырёх случаях: installed, settings_updated, uninstalled (адрес webhook_url) и click_to_call (адрес click_to_call_url). Это не методы API OkoCRM, а запросы к вашему серверу. Тело — JSON, метод POST, заголовки Content-Type: application/json, X-Oko-Event с именем события и Signature с подписью тела.

Проверка подписи. Signature — это HMAC-SHA256 в hex от сырого тела запроса с ключом private_key вашего виджета (OkoStore → «Мои виджеты» → ваш виджет → «Секретный ключ»). Считайте подпись по байтам тела как они пришли, не пересериализуйте JSON, и сравнивайте функцией сравнения за постоянное время. Не совпало — отклоняйте запрос.

POST {webhook_url}
Установка, изменение настроек и удаление подключения
Параметр
Тип
Описание
Запрос
Signature *
string
Заголовок: HMAC-SHA256 сырого тела запроса ключом private_key виджета, hex
X-Oko-Event *
string
Заголовок: installed, settings_updated или uninstalled
event *
string
Тип события: installed, settings_updated, uninstalled
site_id *
integer
ID аккаунта клиента в OkoCRM
subdomain *
string
Поддомен аккаунта клиента
setting_ulid *
string
ID подключения (первая часть токена)
time *
integer
Unix-время события, UTC
token
string
Только в installed: токен подключения для запросов к API
settings
object
installed и settings_updated: значения полей settings.fields виджета по name
user
object
installed и settings_updated: кто сохранил настройки, или null
user.id
integer
ID пользователя CRM
user.name
string
Имя пользователя
user.email
string
E-mail пользователя

Запрос на ваш webhook_url из блока telephony. Событие — в поле event тела и в заголовке X-Oko-Event. Таймаут ответа 5 секунд, при ошибке или коде не 2xx — до 3 попыток с паузой. Отвечайте любым кодом 2xx сразу, тяжёлую обработку делайте после ответа.

installed — подключение готово к работе. Отправляется, когда клиент сохранил настройки виджета и подключение стало рабочим (если у виджета нет настраиваемых полей — сразу при установке), а также повторно при перевыпуске токена. В теле есть token — сохраните пару setting_ulid → token у себя и вызовите PUT extensions и PUT numbers.

settings_updated — клиент изменил настройки. То же тело, что у installed, но без поля token (он не меняется).

uninstalled — клиент удалил подключение. В теле только event, site_id, subdomain, setting_ulid, time. Приходит, только если по подключению раньше уходил installed. Токен этого подключения сразу перестаёт приниматься — дальнейшие запросы с ним получат 404. Историю звонков OkoCRM не удаляет.

setting_ulid — идентификатор конкретного подключения клиента, совпадает с первой частью токена до точки. Один виджет может стоять у разных клиентов, у каждого свой setting_ulid и свой токен. settings — значения полей из config.json → settings.fields вашего виджета, ключ — name поля. user — администратор клиента, который сохранил настройки. time — unix-время в UTC.

# Пример запроса, который OkoCRM отправляет на ваш webhook_url
curl -X POST 'https://pbx.example.com/okocrm/events' \
  -H 'Content-Type: application/json' \
  -H 'X-Oko-Event: installed' \
  -H 'Signature: 5f2b6c1e9a0d4c7b8e3f1a2d6c9b0e4f7a1d3c5b8e2f6a9c0d4b7e1f3a5c8d2b' \
  -d '{
    "event": "installed",
    "site_id": 123,
    "subdomain": "demo",
    "setting_ulid": "01J8Z4N6QYUXH2FA3K5T8DGN2E",
    "time": 1790000000,
    "token": "01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f",
    "settings": {"api_key": "xxx-xxx-xxx"},
    "user": {"id": 45, "name": "Иван Петров", "email": "ivan@example.com"}
  }'
<?php
$privateKey = 'PRIVATE_KEY_ИЗ_РАЗДЕЛА_МОИ_ВИДЖЕТЫ';

$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $privateKey);

if (!hash_equals($expected, $_SERVER['HTTP_SIGNATURE'] ?? '')) {
    http_response_code(403);
    exit;
}

$event = json_decode($body, true);

switch ($event['event']) {
    case 'installed':
        // сохранить $event['setting_ulid'] => $event['token'],
        // затем в фоне вызвать PUT extensions и PUT numbers
        break;
    case 'settings_updated':
        // перечитать $event['settings']
        break;
    case 'uninstalled':
        // удалить токен подключения $event['setting_ulid']
        break;
}

http_response_code(200);
const express = require('express');
const crypto = require('crypto');

const app = express();
const PRIVATE_KEY = process.env.OKOCRM_WIDGET_PRIVATE_KEY;

// Нужно сырое тело, без повторной сериализации JSON
app.post('/okocrm/events', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', PRIVATE_KEY).update(req.body).digest('hex');
  const signature = req.get('Signature') || '';

  if (signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.sendStatus(403);
  }

  const event = JSON.parse(req.body.toString('utf8'));

  if (event.event === 'installed') {
    // сохранить event.setting_ulid -> event.token, затем PUT extensions и PUT numbers
  }

  res.sendStatus(200);
});

app.listen(3000);
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
PRIVATE_KEY = os.environ['OKOCRM_WIDGET_PRIVATE_KEY'].encode()


@app.post('/okocrm/events')
def okocrm_events():
    body = request.get_data()  # сырое тело
    expected = hmac.new(PRIVATE_KEY, body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, request.headers.get('Signature', '')):
        abort(403)

    event = json.loads(body)

    if event['event'] == 'installed':
        pass  # сохранить setting_ulid -> token, затем PUT extensions и PUT numbers

    return '', 200
POST {click_to_call_url}
Звонок из CRM (click to call)
Параметр
Тип
Описание
Запрос
Signature *
string
Заголовок: HMAC-SHA256 сырого тела запроса ключом private_key виджета, hex
X-Oko-Event *
string
Заголовок: click_to_call
event *
string
Всегда click_to_call
site_id *
integer
ID аккаунта клиента в OkoCRM
subdomain *
string
Поддомен аккаунта клиента
setting_ulid *
string
ID подключения (первая часть токена)
time *
integer
Unix-время, UTC
extension_id *
integer
id внутреннего номера сотрудника из PUT extensions
number *
string
number внутреннего номера сотрудника из PUT extensions
user_id
integer
ID сотрудника CRM, привязанного к номеру, или null
phone *
string
Номер клиента, E.164
external_phone
string
Ваша внешняя линия для исходящего, E.164, или null
Ответ
message
string
Только при ответе не 2xx: причина отказа, показывается менеджеру

Менеджер нажал трубку у номера в карточке клиента или сработало автодействие «Позвонить клиенту». OkoCRM вызывает ваш click_to_call_url — это команда вашей АТС набрать сначала внутренний номер сотрудника, затем номер клиента. Подпись и заголовки — как у остальных запросов от OkoCRM, X-Oko-Event: click_to_call.

Таймаут 7 секунд (на установку соединения — 3), без повторов и без редиректов: отвечайте сразу 2xx, а сам звонок инициируйте асинхронно — не держите запрос, пока АТС дозванивается до сотрудника. Ответ 3xx считается ошибкой.

  • Ответ 2xx — команда принята, дальше OkoCRM ждёт обычных событий звонка: call.started → call.ended через POST events.
  • Любой другой код или таймаут — звонок считается не начавшимся. Если в теле ответа есть {"message": "..."}, этот текст покажется менеджеру в CRM как причина ошибки, например {"message": "Сотрудник не в сети"}.

extension_id и number — внутренний номер сотрудника: те же id и number, что вы прислали через PUT extensions. user_id — сотрудник CRM, привязанный к этому номеру; может быть null, если администратор клиента ещё никого не привязал — всё равно звоните на number. phone — номер клиента, external_phone — с какой вашей линии звонить; поле есть всегда, null, если избранная линия не выбрана. Номера нормализованы в формат E.164 с плюсом. Имя и карточка клиента не передаются — звонок свяжется с клиентом автоматически по номеру, когда придёт call.ended.

# Пример запроса, который OkoCRM отправляет на ваш click_to_call_url
curl -X POST 'https://pbx.example.com/okocrm/call' \
  -H 'Content-Type: application/json' \
  -H 'X-Oko-Event: click_to_call' \
  -H 'Signature: 5f2b6c1e9a0d4c7b8e3f1a2d6c9b0e4f7a1d3c5b8e2f6a9c0d4b7e1f3a5c8d2b' \
  -d '{
    "event": "click_to_call",
    "site_id": 123,
    "subdomain": "demo",
    "setting_ulid": "01J8Z4N6QYUXH2FA3K5T8DGN2E",
    "time": 1790002000,
    "extension_id": 17,
    "number": "101",
    "user_id": 45,
    "phone": "+79991234567",
    "external_phone": "+74951234567"
  }'

# Ответ при отказе (любой код не 2xx):
# {"message": "Сотрудник не в сети"}
<?php
$privateKey = 'PRIVATE_KEY_ИЗ_РАЗДЕЛА_МОИ_ВИДЖЕТЫ';

$body = file_get_contents('php://input');

if (!hash_equals(hash_hmac('sha256', $body, $privateKey), $_SERVER['HTTP_SIGNATURE'] ?? '')) {
    http_response_code(403);
    exit;
}

$call = json_decode($body, true);

if (!$pbx->isExtensionOnline($call['number'])) {
    http_response_code(409);
    header('Content-Type: application/json');
    echo json_encode(['message' => 'Сотрудник не в сети']);
    exit;
}

// Ответить сразу, звонок поставить в очередь
http_response_code(200);
$queue->push('originate', [
    'from' => $call['number'],
    'to' => $call['phone'],
    'line' => $call['external_phone'],
]);

Справочники и статус

Списки внутренних номеров и внешних линий клиента вы присылаете сами — после installed и при каждом изменении на вашей стороне. Оба списка присылаются целиком: то, чего нет в списке, OkoCRM сочтёт удалённым. Через POST status вы сообщаете, что подключение клиента на вашей стороне сломалось или снова заработало.

PUT https://webhook.okocrm.com/v4/telephony/extensions
Список внутренних номеров
Параметр
Тип
Описание
Запрос
*.id *
integer
Ваш идентификатор добавочного, целое ≥ 1. Именно он вернётся в click_to_call и ожидается в extension_id событий
*.number *
string
Номер, который увидит сотрудник, до 50 символов
*.name
string
Имя для показа в CRM, до 255 символов; если нет — используется number
*.email
string
E-mail сотрудника, до 255 символов — для автоматической привязки
Ответ
extensions *
integer
Сколько номеров сохранено

Полный список внутренних номеров сотрудников клиента. Присылайте его целиком при каждом изменении: номера, которых нет в списке, OkoCRM сочтёт удалёнными и снимет их связку с сотрудником. Если у номера сменился number при том же id, связка сохраняется, номер обновляется.

Тело запроса — JSON-массив (не объект), до 1000 элементов. Пустой массив [] очищает список. Тело, которое не является массивом, отклоняется с 422 и ничего не меняет.

Если прислан email, у клиента есть активный сотрудник с таким адресом, а номер ещё ни к кому не привязан — OkoCRM свяжет их автоматически. Остальные связки администратор клиента задаёт во вкладке «Сотрудники» настроек виджета.

Ответ 200: {"extensions": 2} — сколько номеров сохранено.

curl -X PUT https://webhook.okocrm.com/v4/telephony/extensions \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '[
    {"id": 101, "number": "101", "name": "Иван Петров", "email": "ivan@example.com"},
    {"id": 102, "number": "102", "name": "Мария Сидорова"}
  ]'

# {"extensions": 2}
<?php
$response = $http->put('https://webhook.okocrm.com/v4/telephony/extensions', [
    'headers' => [
        'Authorization' => 'Bearer ' . $token,
        'Accept' => 'application/json',
    ],
    'json' => [
        ['id' => 101, 'number' => '101', 'name' => 'Иван Петров', 'email' => 'ivan@example.com'],
        ['id' => 102, 'number' => '102', 'name' => 'Мария Сидорова'],
    ],
]);

$saved = json_decode((string) $response->getBody(), true)['extensions']; // 2
import requests

response = requests.put(
    'https://webhook.okocrm.com/v4/telephony/extensions',
    headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
    json=[
        {'id': 101, 'number': '101', 'name': 'Иван Петров', 'email': 'ivan@example.com'},
        {'id': 102, 'number': '102', 'name': 'Мария Сидорова'},
    ],
    timeout=10,
)
response.raise_for_status()
print(response.json()['extensions'])  # 2
PUT https://webhook.okocrm.com/v4/telephony/numbers
Список внешних линий
Параметр
Тип
Описание
Запрос
*.phone *
string
Номер линии, до 32 символов, в любом читаемом формате
Ответ
numbers *
integer
Сколько линий сохранено

Полный список внешних номеров (линий), с которых АТС клиента звонит и принимает звонки. Как и extensions, присылается целиком: исчезнувшие из списка номера удаляются на нашей стороне. Тело — JSON-массив, до 1000 элементов; пустой массив очищает список; не-массив — 422.

Формат номера любой читаемый: +7 (495) 123-45-67, 74951234567 — OkoCRM сам приводит его к цифрам и определяет город. Администратор клиента отмечает избранную линию и город во вкладке «Звонки» настроек виджета.

Ответ 200: {"numbers": 2}.

curl -X PUT https://webhook.okocrm.com/v4/telephony/numbers \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '[{"phone": "+7 (495) 123-45-67"}, {"phone": "74959876543"}]'

# {"numbers": 2}
<?php
$response = $http->put('https://webhook.okocrm.com/v4/telephony/numbers', [
    'headers' => [
        'Authorization' => 'Bearer ' . $token,
        'Accept' => 'application/json',
    ],
    'json' => [
        ['phone' => '+7 (495) 123-45-67'],
        ['phone' => '74959876543'],
    ],
]);
POST https://webhook.okocrm.com/v4/telephony/status
Состояние подключения со стороны АТС
Параметр
Тип
Описание
Запрос
status *
string
ok или error
message
string
Только для error: причина, до 500 символов; в CRM сохраняются первые 255

Сообщите, если на вашей стороне что-то не так с подключением клиента — например, ключ API стал невалидным. При error у интеграции в CRM загорается ошибка с вашим текстом, трубки у номеров гаснут, администратор получает уведомление. При ok ошибка снимается, трубки возвращаются.

Ответ 204 No Content. Если клиент ещё не сохранил настройки виджета (по подключению не было installed) — 409.

Не дёргайте статус на каждый чих: каждое переключение ok → error уведомляет администратора клиента письмом. Шлите error, когда проблема подтверждена, и ok, когда она устранена.

curl -X POST https://webhook.okocrm.com/v4/telephony/status \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"status": "error", "message": "Неверный ключ API"}'

# 204 No Content

# Когда проблема решена:
curl -X POST https://webhook.okocrm.com/v4/telephony/status \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"status": "ok"}'

Звонки

Два эндпоинта: POST route — синхронный вопрос «кому перевести входящий», и POST events — все события звонка. call_id — один и тот же идентификатор на весь звонок от первого до последнего события по нему; придумываете и храните его вы (например, идентификатор звонка в вашей АТС).

POST https://webhook.okocrm.com/v4/telephony/route
Куда перевести входящий звонок
Параметр
Тип
Описание
Запрос
call_id *
string
Ваш идентификатор звонка, до 100 символов — тот же, что потом в событиях
phone *
string
Номер звонящего клиента, любой формат
external_phone
string
На какую вашу линию звонят
Ответ
route_to *
object
Куда перевести, или null — маршрутизируйте сами
route_to.user_id
integer
ID ответственного сотрудника в CRM
route_to.extension_id
integer
id его внутреннего номера из PUT extensions
route_to.number
string
number его внутреннего номера

Вызывается синхронно на входящем звонке на общий номер клиента — до того, как звонок начал маршрутизироваться внутри вашей АТС. OkoCRM отвечает в том же запросе; бюджет на нашей стороне около 1 секунды, поэтому вызывайте из шага IVR или маршрута, который умеет ждать ответ.

Ответ 200:

{"route_to": {"user_id": 45, "extension_id": 101, "number": "101"}}

Переведите звонок на этот внутренний номер. Если в правилах клиента выключен «перевод на ответственного», клиент не найден в CRM по номеру или у ответственного менеджера нет привязанного добавочного — придёт {"route_to": null}: маршрутизируйте звонок сами по вашим обычным правилам.

curl -X POST https://webhook.okocrm.com/v4/telephony/route \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"call_id": "abc-123-xyz", "phone": "+79991234567", "external_phone": "+74951234567"}'

# {"route_to": {"user_id": 45, "extension_id": 101, "number": "101"}}
# или
# {"route_to": null}
<?php
$response = $http->post('https://webhook.okocrm.com/v4/telephony/route', [
    'headers' => ['Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json'],
    'json' => ['call_id' => $callId, 'phone' => $callerNumber, 'external_phone' => $didNumber],
    'timeout' => 2,
]);

$routeTo = json_decode((string) $response->getBody(), true)['route_to'];

if ($routeTo) {
    $pbx->transfer($callId, $routeTo['number']);
} else {
    $pbx->defaultRouting($callId);
}
POST https://webhook.okocrm.com/v4/telephony/events
События звонка
Параметр
Тип
Описание
Запрос
event *
string
call.answered, call.started, call.ended, call.missed или call.recorded
call_id *
string
Ваш идентификатор звонка, до 100 символов, один на все события звонка
phone
string
Номер клиента. Обязателен для всех событий, кроме call.recorded
extension_id
integer
id внутреннего номера из PUT extensions. Обязателен для answered, started, ended
direction
string
Только call.ended, обязателен: in — входящий, out — исходящий
duration
integer
Только call.ended, обязателен: длительность разговора в секундах, 0 — не дозвонились
external_phone
string
Ваша внешняя линия; answered, ended, missed
record_url
string
Ссылка https на запись, до 1000 символов; необязательна в ended, обязательна в recorded

Основной канал: сюда вы шлёте всё, что происходит со звонком. Тело — общий конверт (event, call_id) плюс поля конкретного события. Ответ 204 No Content, если событие принято (в том числе если extension_id не привязан ни к одному сотруднику — событие пропускается, ответ всё равно 204) или это повторная доставка. 409 — клиент ещё не сохранил настройки виджета.

События и их поля:

  • call.answered — входящий принят сотрудником. Обязательно: call_id, phone, extension_id; необязательно: external_phone. В CRM появляется уведомление о звонке.
  • call.started — начался исходящий. Обязательно: call_id, phone, extension_id.
  • call.ended — звонок завершён (входящий или исходящий). Обязательно: call_id, direction (in или out), phone, extension_id, duration (секунды, целое ≥ 0; 0 — не дозвонились); необязательно: external_phone, record_url. Звонок попадает в ленту клиента.
  • call.missed — входящий никто не принял. Обязательно: call_id, phone; необязательно: external_phone. Срабатывают правила клиента по пропущенным.
  • call.recorded — запись стала доступна уже после call.ended. Обязательно: call_id, record_url.

Порядок по одному call_id: входящий с ответом — call.answered → call.ended; исходящий — call.started → call.ended; входящий без ответа — только call.missed. phone и external_phone — в любом читаемом формате. extension_id — тот же id, что вы прислали в PUT extensions.

Повторная доставка. Тот же event + call_id в течение 5 секунд игнорируется с ответом 204 — при сомнениях в доставке (таймаут, обрыв) безопасно повторить запрос сразу. За пределами 5 секунд дедупликации нет.

Запись разговора (record_url). Только https://, до 1000 символов, ссылка должна оставаться доступной не меньше суток (скачивается асинхронно с повторами; подписанные ссылки с ограниченным сроком подходят, если срок не меньше суток). Файл — mp3, wav, ogg/oga, opus или m4a, до 200 МБ; формат определяется по имени файла в Content-Disposition, иначе по Content-Type, иначе считается mp3. До 3 редиректов, каждый — тоже https:// и не на внутренний адрес. Если запись готова к моменту call.ended — присылайте её в нём; если позже — не присылайте record_url в call.ended, а когда появится, отправьте call.recorded с тем же call_id. call.recorded по неизвестному или ещё не завершённому звонку игнорируется.

B='https://webhook.okocrm.com/v4/telephony/events'
A='Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...'

# Входящий: перевели на сотрудника, он ответил
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "call.answered", "call_id": "abc-123", "phone": "+79991234567", "extension_id": 101, "external_phone": "+74951234567"}'

# Разговор окончен, запись уже есть
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "call.ended", "call_id": "abc-123", "direction": "in", "phone": "+79991234567", "extension_id": 101, "duration": 42, "record_url": "https://pbx.example.com/records/abc-123.mp3"}'

# Исходящий
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "call.started", "call_id": "def-456", "phone": "+79991234567", "extension_id": 101}'
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "call.ended", "call_id": "def-456", "direction": "out", "phone": "+79991234567", "extension_id": 101, "duration": 15}'

# Запись появилась позже
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "call.recorded", "call_id": "def-456", "record_url": "https://pbx.example.com/records/def-456.mp3"}'

# Пропущенный
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "call.missed", "call_id": "ghi-789", "phone": "+79991234567", "external_phone": "+74951234567"}'
<?php
function okoEvent(string $token, array $event): void
{
    $response = $GLOBALS['http']->post('https://webhook.okocrm.com/v4/telephony/events', [
        'headers' => ['Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json'],
        'json' => $event,
        'timeout' => 5,
    ]);

    // 204 — принято; повтор того же event + call_id в течение 5 с безопасен
    if ($response->getStatusCode() !== 204) {
        throw new RuntimeException('OkoCRM: ' . $response->getStatusCode() . ' ' . $response->getBody());
    }
}

okoEvent($token, ['event' => 'call.answered', 'call_id' => 'abc-123', 'phone' => '+79991234567', 'extension_id' => 101]);
okoEvent($token, ['event' => 'call.ended', 'call_id' => 'abc-123', 'direction' => 'in', 'phone' => '+79991234567', 'extension_id' => 101, 'duration' => 42]);
okoEvent($token, ['event' => 'call.recorded', 'call_id' => 'abc-123', 'record_url' => 'https://pbx.example.com/records/abc-123.mp3']);
import requests

BASE = 'https://webhook.okocrm.com/v4/telephony/events'


def oko_event(token: str, **event) -> None:
    response = requests.post(
        BASE,
        headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
        json=event,
        timeout=5,
    )
    if response.status_code != 204:
        raise RuntimeError(f'OkoCRM: {response.status_code} {response.text}')


oko_event(token, event='call.started', call_id='def-456', phone='+79991234567', extension_id=101)
oko_event(token, event='call.ended', call_id='def-456', direction='out', phone='+79991234567',
          extension_id=101, duration=15, record_url='https://pbx.example.com/records/def-456.mp3')

SMS: виджеты SMS-шлюзов

Подключение SMS-шлюза к OkoCRM через виджет OkoStore — на уровне штатных SMS-интеграций: отправка из карточки клиента, из шаблонов, из автодействия «Отправить SMS» и через ИИ-ассистента, статусы доставки и входящие SMS в ленте, имена отправителей. Код внутри OkoCRM писать не нужно: вы описываете блок sms в config.json виджета, принимаете вебхуки от OkoCRM (в том числе запрос «отправить SMS») и вызываете три эндпоинта из этой группы.

Начните со страницы Интеграция SMS-шлюза с OkoCRM — там путь разработчика по шагам: что сделать, в каком порядке и какой результат ждать на каждом шаге. Разделы ниже — справочник по каждому запросу.

Схема подключения.

  1. Вы публикуете виджет в OkoStore с блоком sms в config.json. OkoCRM автоматически даёт такому виджету категорию «SMS»: кнопка отправки SMS в карточке, выбор отправителя в шаблонах и автодействиях, вкладка «Подключение» в настройках виджета.
  2. Клиент устанавливает виджет и сохраняет настройки (например, вводит ключ API вашего шлюза). Как только настройки сохранены, OkoCRM присылает на ваш webhook_url событие installed с токеном подключения.
  3. Ваш сервер проверяет доступ к своему шлюзу и вызывает PUT senders — передаёт список имён отправителей, доступных клиенту. Если что-то не так (неверный ключ, нулевой баланс) — сообщает через POST status.
  4. Менеджер, автодействие или ИИ-ассистент отправляет SMS — OkoCRM присылает на ваш send_url запрос sms.send. Вы принимаете сообщение в отправку и отвечаете своим идентификатором сообщения message_id.
  5. По мере доставки вы присылаете статусы через POST events, событие sms.status: sent, delivered, read или failed. Менеджер видит их в ленте: одна галка, две галки, прочитано, красная заметка с причиной ошибки.
  6. Если ваш шлюз принимает ответные SMS — присылайте их событием sms.incoming. Это необязательно.
  7. Клиент изменил настройки — вы получаете settings_updated. Удалил виджет — uninstalled, токен после этого не работает.

Блок sms в config.json виджета.

"sms": {
  "webhook_url": "https://sms.example.com/okocrm/events",
  "send_url": "https://sms.example.com/okocrm/send"
}
  • webhook_url — адрес для вебхуков installed, settings_updated, uninstalled; send_url — адрес для запросов «отправить SMS». Оба поля обязательны, строки до 500 символов.
  • Оба адреса — только https://, реальный внешний адрес: без логина и пароля в строке, без localhost, без локальных и служебных диапазонов (127.0.0.1, 10.0.0.0/8, 192.168.0.0/16, 169.254.0.0/16 и т.п.) — такие адреса OkoCRM отклонит.
  • Один виджет — либо АТС, либо SMS-шлюз: блоки telephony и sms в одном config.json вместе быть не могут.
  • Если блок некорректен, версия виджета не публикуется — вы получите ту же ошибку, что при любом другом битом config.json.
  • Блок работает и для личного (personal) виджета, который вы ставите себе для отладки без модерации, и для публичного — после обычной модерации.
  • script.js обязателен как файл, но может быть пустым модулем: export default async function () {}. Вся логика живёт на вашем сервере.

Токен подключения. Выпускается автоматически при установке виджета и живёт, пока подключение активно. Формат — {ulid}.{секрет}; не разбирайте его, храните строкой целиком. Впервые вы получаете токен в теле вебхука installed (поле token). Отдельного способа запросить токен через API нет. Если токен потерялся, администратор клиента видит его во вкладке «Подключение» настроек виджета и может нажать «Перевыпустить» — вы получите новый installed с новым токеном, старый сразу перестанет работать. Токен SMS-виджета работает только с эндпоинтами /v4/sms/: токен АТС-виджета к ним не подходит, и наоборот — ответ 404.

Токен передаётся только в заголовке:

Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f
Accept: application/json

Никогда не передавайте токен в адресе запроса — такие запросы не пройдут проверку. Заголовок Accept: application/json советуем слать всегда: тогда любая ошибка гарантированно придёт в JSON.

Секретный ключ виджета (private_key) — отдельный от токена, один на весь виджет, а не на конкретного клиента. Он находится на странице виджета: OkoStore → «Мои виджеты» → ваш виджет → строка «Секретный ключ». На экране ключ скрыт звёздочками, по клику копируется целиком; там же его можно сбросить — после сброса старый ключ перестаёт работать сразу у всех клиентов, установивших виджет. Ключ нужен для проверки подписи запросов, которые шлёт вам OkoCRM.

Базовый адрес API: https://webhook.okocrm.com/v4/sms/. Все запросы — JSON. Лимит: не больше 5 запросов в секунду и 600 в минуту на каждый эндпоинт в рамках одного подключения (токена); при превышении — 429 и заголовок Retry-After.

Коды ошибок:

  • 401 — токен не передан, неверного формата или секрет не совпал: {"message": "Не авторизован."};
  • 402 — у клиента не оплачен тариф (демо закончилось, платный истёк или бесплатный тариф). Запрос не обработан, повторите позже. Тело — простой текст, не JSON;
  • 404 — токен не найден, подключение отключено или удалено, либо это токен виджета другого типа (например, АТС): {"message": "Страница не найдена."}. Также неизвестный путь и любой GET;
  • 405 — неподдерживаемый метод на существующем пути, например POST senders вместо PUT;
  • 409 — подключение создано, но клиент ещё не сохранил настройки виджета, рано слать данные: {"message": "Сначала сохраните настройки подключения."}. Так отвечают все три эндпоинта, пока по подключению не приходил installed;
  • 422 — тело не прошло валидацию: {"message": "The given data was invalid.", "errors": {"поле": ["текст ошибки"]}}. Текст ошибки зависит от языка аккаунта клиента — ориентируйтесь на ключи в errors;
  • 429 — превышен лимит запросов: {"message": "Too Many Attempts."}.

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

  1. webhook_url и send_url в блоке sms — оба https://, оба реально отвечают с внешнего адреса, блока telephony в том же виджете нет.
  2. Виджет установлен, настройки сохранены → пришёл installed с корректной подписью в Signature и валидным token. Подпись проверяется по сырому телу, и у sms.send тоже.
  3. В ответ на installed вы вызвали PUT senders — во вкладке «Подключение» настроек виджета видны присланные имена, они же доступны при отправке SMS из карточки, в шаблонах и в автодействии «Отправить SMS».
  4. Отправка SMS из карточки доходит до send_url с ожидаемыми полями, ответ с message_id укладывается в 4 секунды, в ленте появляется SMS с одной галкой.
  5. Идемпотентность: повторный sms.send с тем же Idempotency-Key не отправляет SMS второй раз и возвращает тот же message_id. «Повторить» в ленте приходит с новым ключом.
  6. Ошибка отправки: ваш error.message виден менеджеру в красной заметке; «Повторить» после отказа отправляет сообщение заново.
  7. Отложенная отправка: при непустом send_at сообщение уходит получателю в указанное время.
  8. Статусы: sms.status с delivered даёт две галки, с failed — красную заметку с вашим error.message; повтор события в течение нескольких секунд не меняет результат.
  9. POST status с error при проблемах с балансом или ключом гасит кнопку SMS в карточке и показывает ваш текст в настройках виджета, ok — включает обратно.
  10. Если поддерживаете входящие: sms.incoming с номером существующего контакта появляется в его ленте, ответственный получает уведомление, повтор с тем же message_id второй заметки не создаёт.
  11. Изменение настроек виджета присылает settings_updated; удаление — uninstalled, и старый токен после этого возвращает 404.
  12. Перевыпуск токена во вкладке «Подключение» присылает новый installed — вы обновили токен у себя.

После этого публикуйте виджет как публичный — он пройдёт стандартную модерацию OkoStore.

Запросы от OkoCRM

OkoCRM сама обращается к адресам из блока sms вашего виджета в четырёх случаях: installed, settings_updated, uninstalled (адрес webhook_url) и sms.send (адрес send_url). Это не методы API OkoCRM, а запросы к вашему серверу. Тело — JSON, метод POST, заголовки Content-Type: application/json, X-Oko-Event с именем события и Signature с подписью тела; у sms.send ещё Idempotency-Key.

Проверка подписи. Signature — это HMAC-SHA256 в hex от сырого тела запроса с ключом private_key вашего виджета (OkoStore → «Мои виджеты» → ваш виджет → «Секретный ключ»). Считайте подпись по байтам тела как они пришли, не пересериализуйте JSON: кириллица в теле приходит в виде \uXXXX, и подпись посчитана именно по такой строке. Сравнивайте функцией сравнения за постоянное время. Не совпало — отклоняйте запрос.

POST {sms.webhook_url}
Установка, изменение настроек и удаление подключения SMS-шлюза
Параметр
Тип
Описание
Запрос
Signature *
string
Заголовок: HMAC-SHA256 сырого тела запроса ключом private_key виджета, hex
X-Oko-Event *
string
Заголовок: installed, settings_updated или uninstalled
event *
string
Тип события: installed, settings_updated, uninstalled
site_id *
integer
ID аккаунта клиента в OkoCRM
subdomain *
string
Поддомен аккаунта клиента
setting_ulid *
string
ID подключения (первая часть токена), 26 символов
time *
integer
Unix-время события, UTC
token
string
Только в installed: токен подключения для запросов к API
settings
object
installed и settings_updated: значения полей settings.fields виджета по name
user
object
installed и settings_updated: кто сохранил настройки, или null
user.id
integer
ID пользователя CRM
user.name
string
Имя пользователя
user.email
string
E-mail пользователя

Запрос на ваш webhook_url из блока sms. Событие — в поле event тела и в заголовке X-Oko-Event. Таймаут ответа 5 секунд, при ошибке или коде не 2xx — до 3 попыток с паузой. Отвечайте любым кодом 2xx сразу, тяжёлую обработку делайте после ответа.

installed — подключение готово к работе. Отправляется, когда клиент сохранил настройки виджета и подключение стало рабочим (если у виджета нет настраиваемых полей — сразу при установке), а также повторно при перевыпуске токена. В теле есть token — сохраните пару setting_ulid → token у себя и вызовите PUT senders. Пока вы не прислали ни одного имени отправителя, отправить SMS через ваш шлюз нельзя.

settings_updated — клиент изменил настройки. То же тело, что у installed, но без поля token (он не меняется). Приходит, только если значения полей действительно изменились. Если от настроек зависит список отправителей — пришлите новый полный список через PUT senders.

uninstalled — клиент удалил подключение. В теле только event, site_id, subdomain, setting_ulid, time. Приходит, только если по подключению раньше уходил installed. Токен этого подключения сразу перестаёт приниматься — дальнейшие запросы с ним получат 404. Имена отправителей подключения удаляются из CRM, историю отправленных и полученных SMS OkoCRM не удаляет.

setting_ulid — идентификатор конкретного подключения клиента, 26 символов, совпадает с первой частью токена до точки. Один виджет может стоять у разных клиентов, у каждого свой setting_ulid и свой токен. settings — значения полей из config.json → settings.fields вашего виджета, ключ — name поля; если полей нет — пустой массив. user — администратор клиента, который сохранил настройки, или null. time — unix-время в UTC.

# Пример запроса, который OkoCRM отправляет на ваш webhook_url
curl -X POST 'https://sms.example.com/okocrm/events' \
  -H 'Content-Type: application/json' \
  -H 'X-Oko-Event: installed' \
  -H 'Signature: 5f2b6c1e9a0d4c7b8e3f1a2d6c9b0e4f7a1d3c5b8e2f6a9c0d4b7e1f3a5c8d2b' \
  -d '{
    "event": "installed",
    "site_id": 123,
    "subdomain": "demo",
    "setting_ulid": "01J8Z4N6QYUXH2FA3K5T8DGN2E",
    "time": 1790000000,
    "token": "01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f",
    "settings": {"api_key": "xxx-xxx-xxx"},
    "user": {"id": 45, "name": "Иван Петров", "email": "ivan@example.com"}
  }'
<?php
$privateKey = 'СЕКРЕТНЫЙ_КЛЮЧ_СО_СТРАНИЦЫ_ВИДЖЕТА';

$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $privateKey);

if (!hash_equals($expected, $_SERVER['HTTP_SIGNATURE'] ?? '')) {
    http_response_code(403);
    exit;
}

$event = json_decode($body, true);

switch ($event['event']) {
    case 'installed':
        // сохранить $event['setting_ulid'] => $event['token'],
        // затем в фоне вызвать PUT senders
        break;
    case 'settings_updated':
        // перечитать $event['settings'], при необходимости заново PUT senders
        break;
    case 'uninstalled':
        // удалить токен подключения $event['setting_ulid']
        break;
}

http_response_code(200);
const express = require('express');
const crypto = require('crypto');

const app = express();
const PRIVATE_KEY = process.env.OKOCRM_WIDGET_PRIVATE_KEY;

// Нужно сырое тело, без повторной сериализации JSON
app.post('/okocrm/events', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', PRIVATE_KEY).update(req.body).digest('hex');
  const signature = req.get('Signature') || '';

  if (signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.sendStatus(403);
  }

  const event = JSON.parse(req.body.toString('utf8'));

  if (event.event === 'installed') {
    // сохранить event.setting_ulid -> event.token, затем PUT senders
  }

  res.sendStatus(200);
});

app.listen(3000);
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
PRIVATE_KEY = os.environ['OKOCRM_WIDGET_PRIVATE_KEY'].encode()


@app.post('/okocrm/events')
def okocrm_events():
    body = request.get_data()  # сырое тело
    expected = hmac.new(PRIVATE_KEY, body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, request.headers.get('Signature', '')):
        abort(403)

    event = json.loads(body)

    if event['event'] == 'installed':
        pass  # сохранить setting_ulid -> token, затем PUT senders

    return '', 200
POST {sms.send_url}
Отправить SMS (sms.send)
Параметр
Тип
Описание
Запрос
Signature *
string
Заголовок: HMAC-SHA256 сырого тела запроса ключом private_key виджета, hex
X-Oko-Event *
string
Заголовок: sms.send
Idempotency-Key *
string
Заголовок: ключ идемпотентности, то же значение, что message_key
event *
string
Всегда sms.send
site_id *
integer
ID аккаунта клиента в OkoCRM
subdomain *
string
Поддомен аккаунта клиента
setting_ulid *
string
ID подключения — тот же, что в installed. По нему вы находите клиента и его настройки
message_key *
string
Ключ идемпотентности, 32 hex-символа. Тот же при повторе запроса после сбоя на нашей стороне; при «Повторить» в ленте — новый
phone *
string
Номер получателя, E.164, например +79991234567. Нераспознанный номер приходит как ввёл менеджер: цифры и +
text *
string
Текст SMS, уже транслитерирован, если менеджер это включил. Отправляйте как есть
sender *
string
Имя отправителя — одно из имён, присланных в PUT senders
send_at
integer
null — отправить сейчас; число — unix-время (UTC) отложенной отправки
time *
integer
Unix-время запроса, UTC
Ответ
message_id
string
Успех (2xx): ваш идентификатор сообщения, до 255 символов. Также при 409 — повтор по ключу идемпотентности
error
object
Ошибка (4xx/5xx): причина отказа
error.code
string
Ваш машинный код ошибки; менеджер видит его, только если message пуст
error.message
string
Текст для менеджера на языке клиента, до 255 символов, без HTML — показывается в ленте как есть

Менеджер отправил SMS из карточки клиента, сработало автодействие «Отправить SMS» или SMS отправил ИИ-ассистент. OkoCRM вызывает ваш send_url — это команда вашему шлюзу отправить сообщение. Подпись и заголовки — как у остальных запросов от OkoCRM, X-Oko-Event: sms.send, плюс Idempotency-Key с тем же значением, что поле message_key.

Таймаут 4 секунды (на установку соединения — 2), без повторов и без редиректов. Отвечайте сразу, как только приняли сообщение в свою очередь, а не когда оператор связи подтвердил отправку — для этого есть статусы. Ответ 3xx считается ошибкой.

  • Успех — любой код 2xx и JSON {"message_id": "msg-5f1c2a9e"}. message_id — ваш идентификатор сообщения, непустая строка до 255 символов; по нему вы потом присылаете статусы. Ответ 2xx без message_id считается ошибкой.
  • Ошибка — любой код 4xx/5xx и JSON {"error": {"code": "low_balance", "message": "Недостаточно средств на балансе шлюза"}}. error.message — текст для менеджера на языке клиента, до 255 символов, без HTML: он показывается в ленте CRM в красной заметке как есть. error.code — ваш машинный код; менеджер увидит его, только если message пуст, поэтому всегда заполняйте message. Понимаем и упрощённые формы {"message": "..."} и {"error": "..."}.

Что увидит менеджер: ваш error.message; при таймауте или ошибке соединения — «SMS-шлюз не отвечает, попробуйте позже»; при ответе без текста ошибки — «SMS-шлюз отклонил сообщение без объяснения причины»; если вы не прислали ни одного имени отправителя, запрос к вам не отправляется — «У SMS-шлюза нет ни одного имени отправителя». У красной заметки есть кнопка «Повторить». Ошибка отправки одного сообщения не отключает подключение.

Поля. phone — номер получателя в формате E.164 с плюсом. text — текст SMS; если менеджер включил транслитерацию, текст уже транслитерирован, отправляйте его как есть, деление на части — на вашей стороне. sender — одно из имён, которые вы прислали в PUT senders; если отправитель не выбран (например, в автодействии), подставляется самое раннее из действующих имён подключения. send_at: null — отправить сейчас; число — unix-время (UTC) отложенной отправки: сообщение нужно принять сейчас, а отправить получателю в это время.

Повторы и идемпотентность. Мы не повторяем запрос при таймауте или сетевой ошибке: если вы не ответили за 4 секунды, отправка считается неудачной. Тот же message_key может прийти повторно только при редком сбое на нашей стороне уже после вашего ответа — тогда ответьте тем же message_id (кодом 200 или 409), не отправляя SMS второй раз. Когда менеджер нажимает «Повторить» в ленте, приходит новый message_key — это новая отправка. Ключ, на который вы ответили ошибкой без message_id, можно считать неиспользованным. Храните использованные ключи хотя бы несколько часов.

Внимание. Если вы не ответили за 4 секунды, менеджер может нажать «Повторить», и запрос придёт с новым ключом — возможна двойная отправка. Поэтому отвечайте сразу после постановки сообщения в очередь.

Подключение в ошибке. Пока действует ваш POST status с error, OkoCRM не отправляет sms.send через это подключение: менеджер видит в ленте текст из message вашего статуса (если его нет — «SMS-шлюз не отвечает, попробуйте позже»). После ok отправка возобновляется.

# Пример запроса, который OkoCRM отправляет на ваш send_url
curl -X POST 'https://sms.example.com/okocrm/send' \
  -H 'Content-Type: application/json' \
  -H 'X-Oko-Event: sms.send' \
  -H 'Idempotency-Key: 9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b' \
  -H 'Signature: 5f2b6c1e9a0d4c7b8e3f1a2d6c9b0e4f7a1d3c5b8e2f6a9c0d4b7e1f3a5c8d2b' \
  -d '{
    "event": "sms.send",
    "site_id": 123,
    "subdomain": "demo",
    "setting_ulid": "01J8Z4N6QYUXH2FA3K5T8DGN2E",
    "message_key": "9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b",
    "phone": "+79991234567",
    "text": "Ваш заказ готов",
    "sender": "OkoCRM",
    "send_at": null,
    "time": 1790002000
  }'

# Успех (любой 2xx):
# {"message_id": "msg-5f1c2a9e"}

# Отказ (любой 4xx/5xx):
# {"error": {"code": "low_balance", "message": "Недостаточно средств на балансе шлюза"}}
<?php
$privateKey = 'СЕКРЕТНЫЙ_КЛЮЧ_СО_СТРАНИЦЫ_ВИДЖЕТА';

$body = file_get_contents('php://input');

if (!hash_equals(hash_hmac('sha256', $body, $privateKey), $_SERVER['HTTP_SIGNATURE'] ?? '')) {
    http_response_code(403);
    exit;
}

$sms = json_decode($body, true);
header('Content-Type: application/json');

// Идемпотентность: этот ключ уже принят — вернуть тот же message_id, SMS не слать
if ($existing = $storage->findByKey($sms['message_key'])) {
    echo json_encode(['message_id' => $existing->id]);
    exit;
}

$client = $clients->findByUlid($sms['setting_ulid']);

if ($client->balance <= 0) {
    http_response_code(402);
    echo json_encode(['error' => ['code' => 'low_balance', 'message' => 'Недостаточно средств на балансе шлюза']]);
    exit;
}

// Принять в очередь и ответить сразу: на ответ есть 4 секунды
$message = $queue->push([
    'key' => $sms['message_key'],
    'to' => $sms['phone'],
    'from' => $sms['sender'],
    'text' => $sms['text'],
    'send_at' => $sms['send_at'],
]);

echo json_encode(['message_id' => (string) $message->id]);
const express = require('express');
const crypto = require('crypto');

const app = express();
const PRIVATE_KEY = process.env.OKOCRM_WIDGET_PRIVATE_KEY;

app.post('/okocrm/send', express.raw({ type: 'application/json' }), async (req, res) => {
  const expected = crypto.createHmac('sha256', PRIVATE_KEY).update(req.body).digest('hex');
  const signature = req.get('Signature') || '';

  if (signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.sendStatus(403);
  }

  const sms = JSON.parse(req.body.toString('utf8'));

  // Идемпотентность по message_key / Idempotency-Key
  const existing = await storage.findByKey(sms.message_key);
  if (existing) {
    return res.json({ message_id: existing.id });
  }

  // Принять в очередь и ответить сразу: на ответ есть 4 секунды
  const message = await queue.push({
    key: sms.message_key,
    to: sms.phone,
    from: sms.sender,
    text: sms.text,
    sendAt: sms.send_at,
  });

  res.json({ message_id: String(message.id) });
});

Отправители и статус

Список имён отправителей клиента вы присылаете сами — после installed и при каждом изменении на вашей стороне. Список присылается целиком: то, чего в нём нет, OkoCRM сочтёт удалённым и уберёт из выбора. Через POST status вы сообщаете, что подключение клиента на вашей стороне сломалось (неверный ключ, кончился баланс) или снова заработало.

PUT https://webhook.okocrm.com/v4/sms/senders
Список имён отправителей
Параметр
Тип
Описание
Запрос
Authorization *
string
Заголовок: Bearer {токен подключения}
*.name *
string
Имя отправителя, 1–15 символов: A-Z a-z 0-9, пробел, точка, подчёркивание, дефис. Без повторов в списке
Ответ
senders *
integer
Сколько имён сохранено

Полный список имён отправителей (альфа-имён или номеров), от которых клиент может отправлять SMS через ваш шлюз. Менеджер выбирает из них отправителя в карточке, в шаблонах и в автодействиях. Присылайте список целиком после installed и при каждом изменении: то, чего нет в списке, OkoCRM сочтёт удалённым и уберёт из выбора.

Тело запроса — JSON-массив (не объект) от 1 до 100 элементов. name — строка от 1 до 15 символов: латинские буквы, цифры, пробел, точка, подчёркивание, дефис. Имена в списке не должны повторяться, пробелы по краям обрезаются.

  • Пустой список прислать нельзя (422). Если у клиента не осталось ни одного доступного отправителя или шлюз временно не может отправлять — сообщите об этом через POST status с error.
  • Имя, которое вы убрали из списка, а потом вернули, восстанавливается: шаблоны и автодействия, где оно было выбрано, снова работают с ним.
  • Тело не JSON-массив — 422 с ошибкой в поле body. Ошибки конкретных элементов приходят с ключами вида senders.0.name (номер — позиция элемента в массиве с нуля).
  • Если одновременно пришли два запроса по одному подключению, второй может получить 409 — просто повторите его.

Ответ 200: {"senders": 2} — сколько имён сохранено. Пока клиент не сохранил настройки виджета (не было installed) — 409.

curl -X PUT https://webhook.okocrm.com/v4/sms/senders \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '[{"name": "OkoCRM"}, {"name": "Shop-24"}]'

# {"senders": 2}
<?php
$response = $http->put('https://webhook.okocrm.com/v4/sms/senders', [
    'headers' => [
        'Authorization' => 'Bearer ' . $token,
        'Accept' => 'application/json',
    ],
    'json' => [
        ['name' => 'OkoCRM'],
        ['name' => 'Shop-24'],
    ],
]);

$saved = json_decode((string) $response->getBody(), true)['senders']; // 2
import requests

response = requests.put(
    'https://webhook.okocrm.com/v4/sms/senders',
    headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
    json=[{'name': 'OkoCRM'}, {'name': 'Shop-24'}],
    timeout=10,
)
response.raise_for_status()
print(response.json()['senders'])  # 2
POST https://webhook.okocrm.com/v4/sms/status
Состояние подключения со стороны SMS-шлюза
Параметр
Тип
Описание
Запрос
Authorization *
string
Заголовок: Bearer {токен подключения}
status *
string
ok или error
message
string
Только для error: причина, до 500 символов; в CRM сохраняются первые 255

Сообщите, если на вашей стороне что-то не так с подключением клиента: ключ API стал невалидным, закончился баланс, заблокирован кабинет. При error кнопка отправки SMS в карточке клиента гаснет, в настройках виджета клиент видит ваш текст, администратору приходит письмо об ошибке подключения. При ok ошибка снимается, отправка возобновляется.

Пока подключение в ошибке, OkoCRM не отправляет вам sms.send: SMS, запущенные автодействиями, получают в ленте текст из вашего message. Статусы уже отправленных сообщений и входящие SMS через POST events при этом принимаются как обычно.

Ответ 204 No Content. Если клиент ещё не сохранил настройки виджета (по подключению не было installed) — 409.

Повторный error с тем же текстом ничего не меняет и писем не шлёт. Каждое переключение ok → error уведомляет администратора клиента письмом, поэтому шлите error, когда проблема подтверждена, и ok, когда она устранена.

curl -X POST https://webhook.okocrm.com/v4/sms/status \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"status": "error", "message": "Закончился баланс в кабинете шлюза"}'

# 204 No Content

# Когда проблема решена:
curl -X POST https://webhook.okocrm.com/v4/sms/status \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"status": "ok"}'

Статусы доставки и входящие SMS

Один эндпоинт POST events принимает два события: sms.status — изменился статус отправленного сообщения, и sms.incoming — на номер шлюза пришла SMS от клиента. message_id в статусах — тот идентификатор, который вы вернули в ответ на sms.send; присылайте его строкой, символ в символ.

POST https://webhook.okocrm.com/v4/sms/events
Статусы доставки и входящие SMS
Параметр
Тип
Описание
Запрос
Authorization *
string
Заголовок: Bearer {токен подключения}
event *
string
sms.status или sms.incoming
message_id *
string
Строка до 255 символов. sms.status — id из вашего ответа на sms.send; sms.incoming — ваш id входящего сообщения
status
string
Только sms.status, обязателен: sent, delivered, read или failed
error
object
Только sms.status со статусом failed: причина недоставки
error.code
string
Ваш машинный код ошибки, до 100 символов. Показывается менеджеру, только если message пуст
error.message
string
Текст причины для менеджера на языке клиента, до 255 символов, без HTML
phone
string
Только sms.incoming, обязателен: номер клиента, с которого пришла SMS, до 32 символов, любой читаемый формат
text
string
Только sms.incoming, обязателен: текст SMS, до 1000 символов
sender
string
Только sms.incoming: номер или имя шлюза, на которое пришла SMS, до 50 символов. В ленте пока не показывается
time
integer
Unix-время события в секундах, UTC. Если нет — берём время получения

Сюда вы шлёте статусы доставки отправленных сообщений и входящие SMS. Тело — объект с полем event плюс поля конкретного события. Ответ 204 No Content — всегда, когда тело прошло проверку: событие обработано, пропущено (например, сообщение с таким message_id не найдено) или это повторная доставка. 409 — клиент ещё не сохранил настройки виджета.

sms.status — статус доставки. Обязательно: message_id, status; необязательно: error, time.

  • sent — ушло оператору связи: одна галка в ленте; delivered — доставлено: две галки; read — прочитано (если ваш канал это умеет); failed — окончательно не доставлено: красная заметка с вашим error.message и кнопкой «Повторить». Если error.message пуст, менеджер увидит error.code, если нет и его — «SMS-шлюз отклонил сообщение без объяснения причины».
  • Порядок событий не важен. Более поздний статус заполняет и пропущенные ранние: delivered без sent отметит сообщение и отправленным, и доставленным.
  • Понижения игнорируются: sent после delivered ничего не меняет. failed после delivered тоже игнорируется — доставленное сообщение не «ломается». Присылайте failed, только когда шлюз точно не доставит сообщение.
  • Сообщение с неизвестным message_id пропускается без ошибки. Мы узнаём message_id из вашего ответа на sms.send, поэтому не присылайте статус раньше, чем ответили на запрос; мгновенный статус отправьте через 1–2 секунды после ответа.
  • message_id ищется только среди сообщений этого подключения — у разных клиентов идентификаторы могут совпадать.

sms.incoming — входящая SMS (необязательное событие). Обязательно: message_id, phone, text; необязательно: sender, time.

  • Заметка «Входящая SMS» создаётся в ленте контакта клиента, у которого есть этот номер телефона; если такого контакта нет — в ленте компании с этим номером. Ответственный за контакт получает уведомление.
  • Если номер не найден ни у одного контакта или компании, событие пропускается без ошибки — новые контакты по входящим SMS не создаются.
  • Повтор события с тем же message_id по этому подключению игнорируется в любое время — вторая заметка не появится.

Повторная доставка. Одно и то же событие в течение 5 секунд игнорируется с ответом 204: для sms.status одинаковыми считаются события с тем же message_id и тем же status, для sms.incoming — с тем же message_id. При сомнениях в доставке (таймаут, обрыв) запрос можно смело повторить; повтор позже тоже безопасен.

time — unix-время события в секундах (UTC). Если не прислано или явно неверно (например, в миллисекундах) — берётся время получения.

B='https://webhook.okocrm.com/v4/sms/events'
A='Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...'

# Сообщение доставлено
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "sms.status", "message_id": "msg-5f1c2a9e", "status": "delivered", "time": 1790002030}'

# Сообщение не доставлено
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "sms.status", "message_id": "msg-7a2b3c4d", "status": "failed", "error": {"code": "absent_subscriber", "message": "Абонент недоступен"}}'

# Входящая SMS от клиента
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "sms.incoming", "message_id": "in-7a2b3c4d", "phone": "+79991234567", "text": "Спасибо, заберу завтра", "sender": "+74951234567"}'

# На каждый запрос: 204 No Content
<?php
function okoSmsEvent(string $token, array $event): void
{
    $response = $GLOBALS['http']->post('https://webhook.okocrm.com/v4/sms/events', [
        'headers' => ['Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json'],
        'json' => $event,
        'timeout' => 5,
    ]);

    // 204 — принято; повтор того же события безопасен
    if ($response->getStatusCode() !== 204) {
        throw new RuntimeException('OkoCRM: ' . $response->getStatusCode() . ' ' . $response->getBody());
    }
}

okoSmsEvent($token, ['event' => 'sms.status', 'message_id' => 'msg-5f1c2a9e', 'status' => 'sent']);
okoSmsEvent($token, ['event' => 'sms.status', 'message_id' => 'msg-5f1c2a9e', 'status' => 'delivered']);
okoSmsEvent($token, [
    'event' => 'sms.incoming',
    'message_id' => 'in-7a2b3c4d',
    'phone' => '+79991234567',
    'text' => 'Спасибо, заберу завтра',
]);
import requests

BASE = 'https://webhook.okocrm.com/v4/sms/events'


def oko_sms_event(token: str, **event) -> None:
    response = requests.post(
        BASE,
        headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
        json=event,
        timeout=5,
    )
    if response.status_code != 204:
        raise RuntimeError(f'OkoCRM: {response.status_code} {response.text}')


oko_sms_event(token, event='sms.status', message_id='msg-5f1c2a9e', status='delivered')
oko_sms_event(token, event='sms.status', message_id='msg-7a2b3c4d', status='failed',
              error={'code': 'absent_subscriber', 'message': 'Абонент недоступен'})

Рассылки: виджеты сервисов рассылок

Подключение сервиса email-рассылок к OkoCRM через виджет OkoStore — на уровне штатных интеграций с сервисами рассылок: автодействия «Добавить в список рассылки» и «Удалить из списка рассылки» в воронках продаж и в правилах раздела «Клиенты», а в ленте клиента — всё, что происходит с его адресом: подписан, отписан, письмо отправлено, доставлено, прочитано, переход по ссылке, не доставлено, жалоба на спам. Код внутри OkoCRM писать не нужно: вы описываете блок mailing в config.json виджета, принимаете запросы от OkoCRM (в том числе команды «добавить в список» и «удалить из списка») и вызываете три эндпоинта из этой группы. Почтовые ящики и отправка отдельных писем менеджером из карточки сюда не относятся.

Начните со страницы Интеграция сервиса рассылок с OkoCRM — там путь разработчика по шагам. Разделы ниже — справочник по каждому запросу.

Схема подключения.

  1. Вы публикуете виджет в OkoStore с блоком mailing в config.json. OkoCRM автоматически даёт такому виджету категорию «Рассылка» и вкладки «Подключение» и «Списки рассылок» в настройках виджета.
  2. Клиент устанавливает виджет и сохраняет настройки (например, вводит ключ API вашего сервиса). Как только настройки сохранены, OkoCRM присылает на ваш webhook_url событие installed с токеном подключения.
  3. Ваш сервер проверяет доступ к кабинету клиента у себя и вызывает PUT lists — передаёт полный перечень списков рассылок клиента. Если что-то не так (неверный ключ, кабинет заблокирован) — сообщает через POST status.
  4. В CRM у клиента появляются автодействия «Добавить в список рассылки» и «Удалить из списка рассылки». Когда автодействие срабатывает, OkoCRM присылает на ваш subscribe_url или unsubscribe_url команду по одному адресу и одному списку. Вы отвечаете статусом: сделано сразу либо ждёт подтверждения человеком.
  5. Всё, что происходит с подпиской и письмами на вашей стороне, вы присылаете через POST events: подтверждение подписки, отписку по ссылке в письме, статусы писем. События можно слать по одному или пачками до 100 штук.
  6. Клиент изменил настройки — вы получаете settings_updated. Удалил виджет — uninstalled, токен после этого не работает.

Блок mailing в config.json виджета.

"mailing": {
  "webhook_url": "https://mail.example.com/okocrm/events",
  "subscribe_url": "https://mail.example.com/okocrm/subscribe",
  "unsubscribe_url": "https://mail.example.com/okocrm/unsubscribe"
}
  • webhook_url — адрес для вебхуков installed, settings_updated, uninstalled; subscribe_url — для команды «добавить адрес в список»; unsubscribe_url — для команды «удалить адрес из списка». Все три поля обязательны, строки до 500 символов.
  • Адреса — только https://, реальный внешний адрес: без логина и пароля в строке, без localhost, без локальных и служебных диапазонов (127.0.0.1, 10.0.0.0/8, 192.168.0.0/16, 169.254.0.0/16 и т.п.) — такие адреса OkoCRM отклонит.
  • Один виджет — один вид сервиса: блоки telephony, sms и mailing в одном config.json вместе быть не могут.
  • Если блок некорректен, версия виджета не публикуется — вы получите ту же ошибку, что при любом другом битом config.json.
  • Блок работает и для личного (personal) виджета, который вы ставите себе для отладки без модерации, и для публичного — после обычной модерации.
  • script.js обязателен как файл, но может быть пустым модулем: export default async function () {}. Вся логика живёт на вашем сервере.

Токен подключения. Выпускается автоматически при установке виджета и живёт, пока подключение активно. Формат — {ulid}.{секрет}; не разбирайте его, храните строкой целиком. Впервые вы получаете токен в теле вебхука installed (поле token). Отдельного способа запросить токен через API нет. Если токен потерялся, администратор клиента видит его во вкладке «Подключение» настроек виджета и может нажать «Перевыпустить» — вы получите новый installed с новым токеном, старый сразу перестанет работать. Токен виджета рассылок работает только с эндпоинтами /v4/mailing/: токены АТС- и SMS-виджетов к ним не подходят, и наоборот — ответ 404.

Токен передаётся только в заголовке:

Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f
Accept: application/json

Никогда не передавайте токен в адресе запроса — такие запросы не пройдут проверку. Заголовок Accept: application/json советуем слать всегда: тогда любая ошибка гарантированно придёт в JSON.

Секретный ключ виджета (private_key) — отдельный от токена, один на весь виджет, а не на конкретного клиента. Он находится на странице виджета: OkoStore → «Мои виджеты» → ваш виджет → строка «Секретный ключ». На экране ключ скрыт звёздочками, по клику копируется целиком; там же его можно сбросить — после сброса старый ключ перестаёт работать сразу у всех клиентов, установивших виджет. Ключ нужен для проверки подписи запросов, которые шлёт вам OkoCRM.

Базовый адрес API: https://webhook.okocrm.com/v4/mailing/. Все запросы — JSON. Лимит: не больше 5 запросов в секунду и 600 в минуту на каждый эндпоинт в рамках одного подключения (токена); при превышении — 429 и заголовок Retry-After. Лимит считается в запросах, а не в событиях: пачка из 100 событий — один запрос.

Коды ошибок:

  • 401 — токен не передан, неверного формата или секрет не совпал: {"message": "Не авторизован."};
  • 402 — у клиента не оплачен тариф (демо закончилось, платный истёк или бесплатный тариф). Запрос не обработан, повторите позже. Тело — простой текст, не JSON;
  • 404 — токен не найден, подключение отключено или удалено, либо это токен виджета другого типа (АТС, SMS): {"message": "Страница не найдена."}. Также неизвестный путь и любой GET;
  • 405 — неподдерживаемый метод на существующем пути, например POST lists вместо PUT;
  • 409 — подключение создано, но клиент ещё не сохранил настройки виджета, рано слать данные: {"message": "Сначала сохраните настройки подключения."}. Для PUT lists также: предыдущий такой же запрос ещё выполняется — просто повторите;
  • 422 — тело не прошло валидацию: {"message": "The given data was invalid.", "errors": {"поле": ["текст ошибки"]}}. Текст ошибки зависит от языка аккаунта клиента — ориентируйтесь на ключи в errors;
  • 429 — превышен лимит запросов: {"message": "Too Many Attempts."}.

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

  1. Три адреса в блоке mailing — все https://, все реально отвечают с внешнего адреса, блоков telephony и sms в том же виджете нет.
  2. Виджет установлен, настройки сохранены → пришёл installed с корректной подписью в Signature и валидным token. Подпись проверяется по сырому телу, и у команд тоже.
  3. В ответ на installed вы вызвали PUT lists — во вкладке «Списки рассылок» настроек виджета видны присланные списки, в настройках воронки появились автодействия «Добавить в список рассылки» и «Удалить из списка рассылки», в них выбираются ваш сервис и его списки.
  4. Автодействие «Добавить в список рассылки» доходит до subscribe_url с ожидаемыми полями, ответ {"status": "subscribed"} укладывается в 4 секунды, в ленте контакта появляется «… добавлен в список контактов … для рассылки».
  5. Double opt-in: ответ {"status": "pending"} даёт в ленте «ждём подтверждения»; после события mailing.subscription со статусом subscribed та же запись становится «добавлен в список».
  6. Повторное срабатывание автодействия для уже подписанного адреса возвращает subscribed и не создаёт дублей у вас; повторный запрос с тем же Idempotency-Key не выполняется дважды.
  7. Отказ: ответ 4xx с {"error": {"code": "...", "message": "..."}} показывает менеджеру ваш message; ответы 5xx и 429 приводят к повтору запроса с тем же ключом.
  8. «Удалить из списка рассылки» доходит до unsubscribe_url; удаление отсутствующего адреса отвечает unsubscribed.
  9. Отписка по ссылке в письме присылает mailing.subscription со статусом unsubscribed — в ленте «… отписан от списка …».
  10. Статусы писем уходят пачками через POST events: sent, delivered, opened, clicked дают одну запись письма с метками; campaign.id и list_id передаются строками; ответ пачки с rejected обрабатывается, 429 — пауза по Retry-After.
  11. POST status с error при проблемах с ключом или кабинетом показывает ваш текст в настройках виджета и останавливает команды, ok — включает обратно.
  12. Изменение настроек виджета присылает settings_updated; удаление — uninstalled, и старый токен после этого возвращает 404.
  13. Перевыпуск токена во вкладке «Подключение» присылает новый installed — вы обновили токен у себя.

После этого публикуйте виджет как публичный — он пройдёт стандартную модерацию OkoStore.

Запросы от OkoCRM

OkoCRM сама обращается к адресам из блока mailing вашего виджета в пяти случаях: installed, settings_updated, uninstalled (адрес webhook_url), mailing.subscribe (адрес subscribe_url) и mailing.unsubscribe (адрес unsubscribe_url). Это не методы API OkoCRM, а запросы к вашему серверу. Тело — JSON, метод POST, заголовки Content-Type: application/json, X-Oko-Event с именем события и Signature с подписью тела; у команд ещё Idempotency-Key.

Проверка подписи. Signature — это HMAC-SHA256 в hex от сырого тела запроса с ключом private_key вашего виджета (OkoStore → «Мои виджеты» → ваш виджет → «Секретный ключ»). Считайте подпись по байтам тела как они пришли, не пересериализуйте JSON: кириллица в теле приходит в виде \uXXXX, и подпись посчитана именно по такой строке. Сравнивайте функцией сравнения за постоянное время. Не совпало — отклоняйте запрос.

POST {mailing.webhook_url}
Установка, изменение настроек и удаление подключения сервиса рассылок
Параметр
Тип
Описание
Запрос
Signature *
string
Заголовок: HMAC-SHA256 сырого тела запроса ключом private_key виджета, hex
X-Oko-Event *
string
Заголовок: installed, settings_updated или uninstalled
event *
string
Тип события: installed, settings_updated, uninstalled
site_id *
integer
ID аккаунта клиента в OkoCRM
subdomain *
string
Поддомен аккаунта клиента
setting_ulid *
string
ID подключения (первая часть токена), 26 символов
time *
integer
Unix-время события, UTC
token
string
Только в installed: токен подключения для запросов к API
settings
object
installed и settings_updated: значения полей settings.fields виджета по name
user
object
installed и settings_updated: кто сохранил настройки, или null
user.id
integer
ID пользователя CRM
user.name
string
Имя пользователя
user.email
string
E-mail пользователя

Запрос на ваш webhook_url из блока mailing. Событие — в поле event тела и в заголовке X-Oko-Event. Таймаут ответа 5 секунд, при ошибке или коде не 2xx — до 3 попыток с паузой. Отвечайте любым кодом 2xx сразу, тяжёлую обработку делайте после ответа.

installed — подключение готово к работе. Отправляется, когда клиент сохранил настройки виджета и подключение стало рабочим (если у виджета нет настраиваемых полей — сразу при установке), а также повторно при перевыпуске токена. В теле есть token — сохраните пару setting_ulid → token у себя и вызовите PUT lists. Пока вы не прислали перечень списков, клиенту нечего выбрать в автодействиях.

settings_updated — клиент изменил настройки. То же тело, что у installed, но без поля token (он не меняется). Приходит, только если значения полей действительно изменились. Если от настроек зависит перечень списков (клиент ввёл ключ другого кабинета) — пришлите новый полный перечень через PUT lists.

uninstalled — клиент удалил подключение. В теле только event, site_id, subdomain, setting_ulid, time. Приходит, только если по подключению раньше уходил installed. Токен этого подключения сразу перестаёт приниматься — дальнейшие запросы с ним получат 404. Списки подключения пропадают из выбора в автодействиях, историю событий в ленте клиентов OkoCRM не удаляет.

setting_ulid — идентификатор конкретного подключения клиента, 26 символов, совпадает с первой частью токена до точки. Один виджет может стоять у разных клиентов, у каждого свой setting_ulid и свой токен. settings — значения полей из config.json → settings.fields вашего виджета, ключ — name поля; если полей нет — пустой массив. user — администратор клиента, который сохранил настройки, или null. time — unix-время в UTC.

# Пример запроса, который OkoCRM отправляет на ваш webhook_url
curl -X POST 'https://mail.example.com/okocrm/events' \
  -H 'Content-Type: application/json' \
  -H 'X-Oko-Event: installed' \
  -H 'Signature: 5f2b6c1e9a0d4c7b8e3f1a2d6c9b0e4f7a1d3c5b8e2f6a9c0d4b7e1f3a5c8d2b' \
  -d '{
    "event": "installed",
    "site_id": 123,
    "subdomain": "demo",
    "setting_ulid": "01J8Z4N6QYUXH2FA3K5T8DGN2E",
    "time": 1790000000,
    "token": "01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f",
    "settings": {"api_key": "xxx-xxx-xxx"},
    "user": {"id": 45, "name": "Иван Петров", "email": "ivan@example.com"}
  }'
<?php
$privateKey = 'СЕКРЕТНЫЙ_КЛЮЧ_СО_СТРАНИЦЫ_ВИДЖЕТА';

$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $privateKey);

if (!hash_equals($expected, $_SERVER['HTTP_SIGNATURE'] ?? '')) {
    http_response_code(403);
    exit;
}

$event = json_decode($body, true);

switch ($event['event']) {
    case 'installed':
        // сохранить $event['setting_ulid'] => $event['token'],
        // затем в фоне вызвать PUT lists
        break;
    case 'settings_updated':
        // перечитать $event['settings'], при необходимости заново PUT lists
        break;
    case 'uninstalled':
        // удалить токен подключения $event['setting_ulid']
        break;
}

http_response_code(200);
const express = require('express');
const crypto = require('crypto');

const app = express();
const PRIVATE_KEY = process.env.OKOCRM_WIDGET_PRIVATE_KEY;

// Нужно сырое тело, без повторной сериализации JSON
app.post('/okocrm/events', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', PRIVATE_KEY).update(req.body).digest('hex');
  const signature = req.get('Signature') || '';

  if (signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.sendStatus(403);
  }

  const event = JSON.parse(req.body.toString('utf8'));

  if (event.event === 'installed') {
    // сохранить event.setting_ulid -> event.token, затем PUT lists
  }

  res.sendStatus(200);
});

app.listen(3000);
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
PRIVATE_KEY = os.environ['OKOCRM_WIDGET_PRIVATE_KEY'].encode()


@app.post('/okocrm/events')
def okocrm_events():
    body = request.get_data()  # сырое тело
    expected = hmac.new(PRIVATE_KEY, body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, request.headers.get('Signature', '')):
        abort(403)

    event = json.loads(body)

    if event['event'] == 'installed':
        pass  # сохранить setting_ulid -> token, затем PUT lists

    return '', 200
POST {mailing.subscribe_url}
Добавить адрес в список (mailing.subscribe)
Параметр
Тип
Описание
Запрос
Signature *
string
Заголовок: HMAC-SHA256 сырого тела запроса ключом private_key виджета, hex
X-Oko-Event *
string
Заголовок: mailing.subscribe
Idempotency-Key *
string
Заголовок: ключ идемпотентности, то же значение, что request_key
event *
string
Всегда mailing.subscribe
site_id *
integer
ID аккаунта клиента в OkoCRM
subdomain *
string
Поддомен аккаунта клиента
setting_ulid *
string
ID подключения — тот же, что в installed. По нему вы находите клиента и его настройки
request_key *
string
Ключ идемпотентности, 32 hex-символа. Одинаков при наших повторах; при новом срабатывании автодействия — новый
list_id *
string
Идентификатор списка — тот id, который вы прислали в PUT lists
email *
string
Адрес в нижнем регистре
contact *
object
Контакт в CRM, которому принадлежит адрес
contact.id *
integer
ID контакта в CRM
contact.name *
string
Имя контакта, можно использовать для персонализации писем
time *
integer
Unix-время запроса, UTC
Ответ
status
string
Успех (2xx): subscribed — адрес в списке; pending — ждёт подтверждения человеком
error
object
Отказ (4xx): причина
error.code
string
Ваш машинный код ошибки; менеджер видит его, только если message пуст
error.message
string
Текст для менеджера на языке клиента, до 255 символов, без HTML — показывается в ленте как есть

Сработало автодействие клиента «Добавить в список рассылки». OkoCRM вызывает ваш subscribe_url. Один запрос — один адрес и один список: если у контакта два адреса, а в автодействии выбрано три списка, придёт шесть запросов. Подпись и заголовки — как у остальных запросов от OkoCRM, X-Oko-Event: mailing.subscribe, плюс Idempotency-Key с тем же значением, что поле request_key.

Таймаут 4 секунды (на установку соединения — 2), без редиректов. Отвечайте сразу, как только приняли команду.

Ответ: статус обязателен. Код 2xx и JSON с полем status — по нему CRM показывает менеджеру, что произошло:

  • {"status": "subscribed"} — адрес в списке: вы добавили его сейчас или он был там раньше. В ленте клиента: «{контакт} добавлен в список контактов {список} для рассылки».
  • {"status": "pending"} — вы отправили человеку письмо с просьбой подтвердить подписку (double opt-in). В ленте: «Запрос на добавление … отправлен, ждём подтверждения подписки» и метка «Ждёт подтверждения». Итог сообщите событием mailing.subscription — та же запись сменит состояние.

Ответ 2xx без status или с другим значением считается ошибкой и не повторяется. «Адрес уже в списке» — это успех, а не ошибка: автодействие у клиента может сработать повторно.

Отказ — любой код 4xx (кроме 408 и 429) и JSON {"error": {"code": "invalid_email", "message": "Адрес в стоп-листе сервиса"}}. error.message — текст для менеджера на языке клиента, до 255 символов, без HTML (теги вырезаем). error.code менеджер увидит, только если message пуст. Понимаем и упрощённые формы {"message": "..."} и {"error": "..."}. Отказ не повторяется.

Что увидит менеджер при неудаче: запись «Не удалось добавить {контакт} в список {список}» с меткой «Ошибка», под ней адрес и причина — ваш error.message (или error.code); при таймауте, сетевой ошибке, ответах 5xx, 408, 429 после всех попыток — «Сервис рассылок не отвечает, попробуйте позже»; при ответе без текста ошибки, в том числе 2xx без status или 3xx — «Сервис рассылок отклонил запрос без объяснения причины».

Повторы и идемпотентность. Если вы не ответили за 4 секунды или ответили 5xx, 408, 429, мы повторим запрос с тем же request_key — всего до 3 попыток с паузой около 10 секунд. Запрос с ключом, который вы уже выполнили, не выполняйте второй раз — ответьте тем же статусом. Храните использованные ключи хотя бы несколько часов. Новое срабатывание автодействия — новый ключ. Команды по одной паре «список + адрес» никогда не уходят к вам одновременно, но порядок после паузы на повтор не гарантирован: считайте каждую команду желаемым итоговым состоянием на момент её прихода. Чтобы притормозить поток, отвечайте 429.

Подключение в ошибке. Пока действует ваш POST status с error, OkoCRM не отправляет команды: менеджер видит «не удалось» с текстом из message вашего статуса. После ok команды возобновляются. Отказ по одной команде подключение не отключает.

# Пример запроса, который OkoCRM отправляет на ваш subscribe_url
curl -X POST 'https://mail.example.com/okocrm/subscribe' \
  -H 'Content-Type: application/json' \
  -H 'X-Oko-Event: mailing.subscribe' \
  -H 'Idempotency-Key: 9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b' \
  -H 'Signature: 5f2b6c1e9a0d4c7b8e3f1a2d6c9b0e4f7a1d3c5b8e2f6a9c0d4b7e1f3a5c8d2b' \
  -d '{
    "event": "mailing.subscribe",
    "site_id": 123,
    "subdomain": "demo",
    "setting_ulid": "01J8Z4N6QYUXH2FA3K5T8DGN2E",
    "request_key": "9f2c1e4a7b6d5c3f8a1b2c3d4e5f6a7b",
    "list_id": "clients-2026",
    "email": "ivan@mail.ru",
    "contact": {"id": 456, "name": "Иван Петров"},
    "time": 1790002000
  }'

# Добавили сразу (2xx):
# {"status": "subscribed"}

# Отправили письмо для подтверждения подписки (2xx):
# {"status": "pending"}

# Отказ (4xx):
# {"error": {"code": "invalid_email", "message": "Адрес в стоп-листе сервиса"}}
<?php
$privateKey = 'СЕКРЕТНЫЙ_КЛЮЧ_СО_СТРАНИЦЫ_ВИДЖЕТА';

$body = file_get_contents('php://input');

if (!hash_equals(hash_hmac('sha256', $body, $privateKey), $_SERVER['HTTP_SIGNATURE'] ?? '')) {
    http_response_code(403);
    exit;
}

$command = json_decode($body, true);
header('Content-Type: application/json');

// Идемпотентность: команду с этим ключом уже выполняли — вернуть тот же статус
if ($done = $storage->findByKey($command['request_key'])) {
    echo json_encode(['status' => $done->status]);
    exit;
}

$client = $clients->findByUlid($command['setting_ulid']);

if ($client->isStopListed($command['email'])) {
    http_response_code(422);
    echo json_encode(['error' => ['code' => 'stop_list', 'message' => 'Адрес в стоп-листе сервиса']]);
    exit;
}

// Double opt-in: отправить письмо-подтверждение и ответить pending, итог — событием mailing.subscription
$status = $client->doubleOptIn
    ? $lists->requestConfirmation($command['list_id'], $command['email'], $command['contact']['name'])
    : $lists->subscribe($command['list_id'], $command['email']);

$storage->remember($command['request_key'], $status); // 'subscribed' | 'pending'

echo json_encode(['status' => $status]);
app.post('/okocrm/subscribe', express.raw({ type: 'application/json' }), async (req, res) => {
  const expected = crypto.createHmac('sha256', PRIVATE_KEY).update(req.body).digest('hex');
  const signature = req.get('Signature') || '';

  if (signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.sendStatus(403);
  }

  const command = JSON.parse(req.body.toString('utf8'));

  // Идемпотентность по request_key / Idempotency-Key
  const done = await storage.findByKey(command.request_key);
  if (done) {
    return res.json({ status: done.status });
  }

  // Адрес уже в списке — тоже subscribed
  const status = await lists.subscribe(command.list_id, command.email); // 'subscribed' | 'pending'
  await storage.remember(command.request_key, status);

  res.json({ status });
});
POST {mailing.unsubscribe_url}
Удалить адрес из списка (mailing.unsubscribe)
Параметр
Тип
Описание
Запрос
Signature *
string
Заголовок: HMAC-SHA256 сырого тела запроса ключом private_key виджета, hex
X-Oko-Event *
string
Заголовок: mailing.unsubscribe
Idempotency-Key *
string
Заголовок: ключ идемпотентности, то же значение, что request_key
event *
string
Всегда mailing.unsubscribe
site_id *
integer
ID аккаунта клиента в OkoCRM
subdomain *
string
Поддомен аккаунта клиента
setting_ulid *
string
ID подключения — тот же, что в installed
request_key *
string
Ключ идемпотентности, 32 hex-символа. Одинаков при наших повторах; при новом срабатывании автодействия — новый
list_id *
string
Идентификатор списка — тот id, который вы прислали в PUT lists
email *
string
Адрес в нижнем регистре
contact *
object
Контакт в CRM, которому принадлежит адрес
contact.id *
integer
ID контакта в CRM
contact.name *
string
Имя контакта
time *
integer
Unix-время запроса, UTC
Ответ
status
string
Успех (2xx): unsubscribed — адреса в списке нет; pending — удаление завершится позже
error
object
Отказ (4xx): причина
error.code
string
Ваш машинный код ошибки; менеджер видит его, только если message пуст
error.message
string
Текст для менеджера на языке клиента, до 255 символов, без HTML

Сработало автодействие клиента «Удалить из списка рассылки». OkoCRM вызывает ваш unsubscribe_url. Тело, заголовки (X-Oko-Event: mailing.unsubscribe, Idempotency-Key), таймаут, повторы, идемпотентность и формат отказа — как у mailing.subscribe.

Ответ: статус обязателен.

  • {"status": "unsubscribed"} — адреса в списке нет: вы удалили его сейчас или его там и не было. В ленте клиента: «{контакт} отписан от списка {список}».
  • {"status": "pending"} — удаление у вас не мгновенное, итог сообщите позже событием mailing.subscription со статусом unsubscribed. В ленте: «Запрос на удаление … отправлен, ждём подтверждения» и метка «Ждёт подтверждения».

«Адреса нет в списке» — это успех, а не ошибка: автодействие у клиента может сработать повторно. При неудаче менеджер видит запись «Не удалось удалить {контакт} из списка {список}» с меткой «Ошибка» и причиной — по тем же правилам, что у mailing.subscribe.

# Пример запроса, который OkoCRM отправляет на ваш unsubscribe_url
curl -X POST 'https://mail.example.com/okocrm/unsubscribe' \
  -H 'Content-Type: application/json' \
  -H 'X-Oko-Event: mailing.unsubscribe' \
  -H 'Idempotency-Key: 4b7e1f3a5c8d2b9f2c1e4a7b6d5c3f8a' \
  -H 'Signature: 5f2b6c1e9a0d4c7b8e3f1a2d6c9b0e4f7a1d3c5b8e2f6a9c0d4b7e1f3a5c8d2b' \
  -d '{
    "event": "mailing.unsubscribe",
    "site_id": 123,
    "subdomain": "demo",
    "setting_ulid": "01J8Z4N6QYUXH2FA3K5T8DGN2E",
    "request_key": "4b7e1f3a5c8d2b9f2c1e4a7b6d5c3f8a",
    "list_id": "clients-2026",
    "email": "ivan@mail.ru",
    "contact": {"id": 456, "name": "Иван Петров"},
    "time": 1790003000
  }'

# Удалили или адреса в списке и не было (2xx):
# {"status": "unsubscribed"}

Списки рассылок и статус

Перечень списков рассылок клиента вы присылаете сами — после installed и при каждом изменении на вашей стороне. Перечень присылается целиком: список, которого в нём нет, пропадёт из выбора в автодействиях (но не удалится — записи о нём в ленте сохраняют название). Через POST status вы сообщаете, что подключение клиента на вашей стороне сломалось (неверный ключ, заблокирован кабинет) или снова заработало.

PUT https://webhook.okocrm.com/v4/mailing/lists
Перечень списков рассылок
Параметр
Тип
Описание
Запрос
Authorization *
string
Заголовок: Bearer {токен подключения}
*.id *
string
Идентификатор списка, строка 1–100 символов. Без повторов в перечне, регистр не различается
*.name *
string
Название списка для клиента, 1–255 символов
Ответ
lists *
integer
Сколько списков в перечне

Полный перечень списков (адресных баз, сегментов — того, куда у вас добавляют подписчиков) в кабинете клиента. Клиент выбирает из них списки в автодействиях «Добавить в список рассылки» и «Удалить из списка рассылки» и видит их во вкладке «Списки рассылок» настроек виджета. Присылайте перечень целиком после installed и при каждом изменении.

Тело запроса — JSON-массив (не объект) от 1 до 500 элементов. id — ваш идентификатор списка, строка от 1 до 100 символов (числовой идентификатор передавайте строкой: "12345"); не должен повторяться в перечне, регистр букв не различается. Именно он приходит вам в list_id команд и ожидается в list_id событий. name — название списка для клиента, от 1 до 255 символов. Пробелы по краям обрезаются.

  • Пустой перечень прислать нельзя (422). Если у клиента не осталось ни одного списка или кабинет недоступен — сообщите об этом через POST status с error.
  • Список с уже известным id и новым name переименовывается.
  • Список, который вы убрали из перечня, скрывается из выбора, но не удаляется: записи о нём в ленте клиентов сохраняют название. Если вы вернёте его в перечень — он снова доступен, и автодействия, где он был выбран, снова работают с ним.
  • Тело не JSON-массив — 422 с ошибкой в поле body. Ошибки конкретных элементов приходят с ключами вида lists.0.id (номер — позиция элемента в массиве с нуля).
  • Если одновременно пришли два запроса по одному подключению, второй может получить 409 — просто повторите его.

Ответ 200: {"lists": 2} — сколько списков в перечне. Пока клиент не сохранил настройки виджета (не было installed) — 409.

curl -X PUT https://webhook.okocrm.com/v4/mailing/lists \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '[{"id": "clients-2026", "name": "Клиенты 2026"}, {"id": "news", "name": "Новости и акции"}]'

# {"lists": 2}
<?php
$response = $http->put('https://webhook.okocrm.com/v4/mailing/lists', [
    'headers' => [
        'Authorization' => 'Bearer ' . $token,
        'Accept' => 'application/json',
    ],
    'json' => array_map(fn ($list) => [
        'id' => (string) $list->id, // строкой, даже если у вас число
        'name' => $list->title,
    ], $client->lists()),
]);

$count = json_decode((string) $response->getBody(), true)['lists'];
import requests

response = requests.put(
    'https://webhook.okocrm.com/v4/mailing/lists',
    headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
    json=[{'id': str(item.id), 'name': item.title} for item in client_lists],
    timeout=10,
)
response.raise_for_status()
print(response.json()['lists'])
POST https://webhook.okocrm.com/v4/mailing/status
Состояние подключения со стороны сервиса рассылок
Параметр
Тип
Описание
Запрос
Authorization *
string
Заголовок: Bearer {токен подключения}
status *
string
ok или error
message
string
Только для error: причина, до 500 символов; в CRM сохраняются первые 255

Сообщите, если на вашей стороне что-то не так с подключением клиента: ключ API стал невалидным, заблокирован кабинет, закончился тариф. При error в настройках виджета клиент видит ваш текст, администратору приходит письмо об ошибке подключения, а команды автодействий перестают отправляться: в ленте клиента появляется «не удалось» с текстом из вашего message. При ok ошибка снимается, команды возобновляются.

В состоянии ошибки POST events и PUT lists продолжают работать как обычно.

Ответ 204 No Content. Если клиент ещё не сохранил настройки виджета (по подключению не было installed) — 409.

Повторный error с тем же текстом ничего не меняет и писем не шлёт. Каждое переключение ok → error уведомляет администратора клиента письмом, поэтому шлите error, когда проблема подтверждена, и ok, когда она устранена.

curl -X POST https://webhook.okocrm.com/v4/mailing/status \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"status": "error", "message": "Ключ API отозван в кабинете сервиса"}'

# 204 No Content

# Когда проблема решена:
curl -X POST https://webhook.okocrm.com/v4/mailing/status \
  -H 'Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"status": "ok"}'

События подписок и писем

Один эндпоинт POST events принимает два события: mailing.subscription — изменилась подписка адреса на список, и mailing.letter — что-то произошло с письмом рассылки. События можно слать по одному или пачкой до 100 штук. Идентификаторы списков и рассылок передавайте строками.

POST https://webhook.okocrm.com/v4/mailing/events
События подписок и писем
Параметр
Тип
Описание
Запрос
Authorization *
string
Заголовок: Bearer {токен подключения}
events
array
Пачка: 1–100 событий. Без неё тело — одно событие с полями ниже
event *
string
mailing.subscription или mailing.letter
status *
string
subscription: subscribed, unsubscribed; letter: sent, delivered, opened, clicked, bounced, complained
email *
string
Адрес получателя, корректный email, до 255 символов
time
integer
Unix-время события в секундах, UTC. Вне окна −30 дней…+1 день или без него — берём время получения
list_id
string
subscription: id списка из PUT lists, строкой. Обязателен для subscribed; без него unsubscribed = отписан от всех
campaign
object
letter, обязателен: рассылка
campaign.id
string
letter, обязателен: id рассылки строкой до 255. Одна запись в ленте на пару campaign.id + контакт
campaign.name
string
letter: название рассылки для менеджера, до 255
subject
string
letter: тема письма у этого получателя, до 255; запоминается первая
url
string
Только clicked: ссылка http:// или https://, до 500; запоминается первая
reason
string
Только bounced: причина недоставки для менеджера, до 255, без HTML
Ответ
accepted
integer
Пачка (200): сколько событий принято
rejected
array
Пачка (200): отклонённые события
rejected.*.index
integer
Позиция события в массиве events, с нуля
rejected.*.error
string
Текст ошибки проверки либо код unknown_list

Сюда вы шлёте всё, что происходит с подписками и письмами на вашей стороне. Один адрес принимает два формата:

  • Одно событие — тело есть само событие. Ответ 204 No Content; ошибка формата или неизвестный список — 422.
  • Пачка — объект {"events": [ ... ]}, от 1 до 100 событий любых видов вперемешку. Ответ 200: {"accepted": 98, "rejected": [{"index": 3, "error": "..."}]}. Событие с ошибкой пропускается и попадает в rejected (index — позиция в массиве с нуля, error — текст ошибки проверки либо код unknown_list), остальные обрабатываются. Повторять нужно только исправленные события. Неверный конверт (events не массив, пустой или длиннее 100) — 422 на весь запрос.

Общие поля: event, status, email (корректный адрес, до 255 символов, регистр не важен), необязательный time — unix-время в секундах (UTC); если не прислано или выходит за окно от 30 дней назад до суток вперёд (например, прислано в миллисекундах) — берём время получения. Событие с адресом, которого нет среди контактов клиента в CRM (или контакт с этим адресом удалён в CRM), считается принятым и пропускается без ошибки: новые контакты по событиям рассылок не создаются. Повторы безопасны в любое время — повторное событие ничего не меняет и не создаёт дублей в ленте. Лимит считается в запросах: пачка — один запрос.

mailing.subscription — подписка и отписка. status: subscribed или unsubscribed; list_id — идентификатор списка из PUT lists, строкой: обязателен для subscribed, необязателен для unsubscribed. unsubscribed без list_id означает «адрес отписан от всех рассылок»: CRM отметит отписку во всех списках вашего сервиса, где этот адрес подписан или ждёт решения. Когда слать: вы ответили pending на команду и человек подтвердил подписку или удаление завершилось; человек сам отписался по ссылке в письме; адрес добавлен в список не из CRM (форма на сайте, импорт); вы сами исключили адрес (после жёсткого возврата, жалобы). Менеджер видит «{контакт} добавлен в список контактов {список} для рассылки» или «{контакт} отписан от списка {список}», под записью — адрес. Если в ленте есть запись «ждём подтверждения» по этому адресу и списку и событие завершает ту же операцию — меняется она, новая не создаётся. Неизвестный list_id — ошибка unknown_list: сначала пришлите обновлённый перечень.

mailing.letter — статусы письма. Обязательно campaign.id — ваш идентификатор рассылки, строкой до 255 символов. В ленте одна запись на пару «campaign.id + контакт»; если в одной цепочке несколько писем — передавайте для каждого свой идентификатор, например welcome:1, welcome:2. Необязательно: campaign.name (название для менеджера; пока его не было, видно campaign.id), subject (тема у этого получателя, может быть персональной, запоминается первая), url (только для clicked, http:// или https://, до 500 символов, запоминается первая), reason (только для bounced, причина для менеджера, без HTML).

  • sent — запись «название рассылки → тема» с временем отправки; delivered — метка «Доставлено»; opened — «Прочитано»; clicked — «Переход по ссылке» и сама ссылка; bounced — красная «Не доставлено» и причина; complained — красная «Жалоба на спам».
  • Поддерживать все статусы не обязательно. Порядок не важен: запись создаётся по первому пришедшему событию, более поздний статус заполняет и пропущенные ранние (opened без delivered отметит письмо и доставленным).
  • Каждый статус фиксируется один раз: десять открытий — одна метка со временем первого.
  • bounced после delivered игнорируется — доставленное письмо не «ломается».
  • После bounced и complained CRM сама не считает адрес отписанным: если вы исключили адрес из списка — пришлите mailing.subscription с unsubscribed.
  • Статусы писем видны менеджеру при открытии карточки клиента; записи о подписке обновляются сразу.

B='https://webhook.okocrm.com/v4/mailing/events'
A='Authorization: Bearer 01J8Z4N6QYUXH2FA3K5T8DGN2E.9f2c1e4a...'

# Одно событие: человек подтвердил подписку
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"event": "mailing.subscription", "status": "subscribed", "email": "ivan@mail.ru", "list_id": "news"}'
# 204 No Content

# Пачка статусов рассылки
curl -X POST $B -H "$A" -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"events": [
    {"event": "mailing.letter", "status": "sent", "email": "ivan@mail.ru", "campaign": {"id": "autumn-sale", "name": "Осенняя распродажа"}, "subject": "Иван, скидка 20% до пятницы", "time": 1790000000},
    {"event": "mailing.letter", "status": "clicked", "email": "ivan@mail.ru", "campaign": {"id": "autumn-sale"}, "url": "https://shop.example.com/sale", "time": 1790003700},
    {"event": "mailing.subscription", "status": "unsubscribed", "email": "petr@mail.ru", "list_id": "news"}
  ]}'
# {"accepted": 3, "rejected": []}
<?php
// Копим события рассылки и шлём пачками по 100
foreach (array_chunk($events, 100) as $chunk) {
    do {
        $response = $http->post('https://webhook.okocrm.com/v4/mailing/events', [
            'headers' => ['Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json'],
            'json' => ['events' => $chunk],
            'timeout' => 10,
            'http_errors' => false,
        ]);

        // 429 — подождать и повторить эту же пачку
        $isThrottled = $response->getStatusCode() === 429;

        if ($isThrottled) {
            sleep((int) $response->getHeaderLine('Retry-After') ?: 1);
        }
    } while ($isThrottled);

    $result = json_decode((string) $response->getBody(), true);

    foreach ($result['rejected'] ?? [] as $item) {
        // исправить событие $chunk[$item['index']] или пропустить; unknown_list — сначала PUT lists
    }
}
import time

import requests

BASE = 'https://webhook.okocrm.com/v4/mailing/events'


def send_events(token: str, events: list) -> None:
    for start in range(0, len(events), 100):
        chunk = events[start:start + 100]
        while True:
            response = requests.post(
                BASE,
                headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
                json={'events': chunk},
                timeout=10,
            )
            if response.status_code == 429:
                time.sleep(int(response.headers.get('Retry-After', 1)))
                continue
            response.raise_for_status()
            for item in response.json()['rejected']:
                print('rejected', chunk[item['index']], item['error'])
            break
API OkoCRM — документация для разработчиков | OkoCRM
Нет времени разбираться самостоятельно?
Настроим CRM так, чтобы система работала на ваш бизнес, выручка росла, а менеджеры продавали вдвое больше
Заказать консультацию
Наверх
Продолжая использовать сайт, Вы даете согласие на обработку файлов cookies в соответствии с Политикой обработки персональных данных.
Я согласен
API OkoCRM — документация для разработчиков | OkoCRM