Открытый API OkoCRM: интеграция и разработка
Введение
Возможности
API OkoCRM поддерживает чтение и запись контактов, компаний, сделок и других элементов системы. Это даёт вам возможность как просматривать, так и использовать данные, которые хранятся в OkoCRM, в других своих сервисах.
Вы можете использовать API для импорта внешних данных в свою учётную запись OkoCRM.
Мы также работаем над интеграцией других сервисов в платформу OkoCRM. Эти сервисы будут доступны для всех пользователей OkoCRM. Если вы планируете создать свою интеграцию, свяжитесь с нами по адресу support@okocrm.com.
Как это работает?
API OkoCRM основано на REST. Базовый URL: https://api.okocrm.com/v2/.
Пользователи
Список пользователей
Возвращает пользователей аккаунта по 15 на страницу. В список попадают и удалённые пользователи: у них заполнено deleted_at. Сначала идут действующие, внутри — по имени.
Общее количество записей не возвращается: чтобы узнать, есть ли следующая страница, проверьте links.next.
Ошибки: 403 — нет прав на просмотр пользователей.
curl --location 'https://api.okocrm.com/v2/users?page=1' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'
Контакты
Список контактов
Возвращает список контактов по 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}'
Получить контакт
Возвращает контакт с телефонами, e-mail, городом и тегами. Кроме стандартных полей приходят дополнительные поля, настроенные в аккаунте (ключ — var_name), и описание вкладок карточки с их полями в tabs.
Ошибки: 403 — нет прав на просмотр контакта, 404 — контакт не найден.
curl --location 'https://api.okocrm.com/v2/contacts/125' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'
Добавить контакт
Создаёт контакт. Кроме перечисленных полей можно передать дополнительные поля, настроенные в аккаунте: ключ — 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)
Изменить контакт
Изменяет поля контакта. Кроме перечисленных полей можно передать дополнительные поля, настроенные в аккаунте: ключ — 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'
Удалить контакт
Удаляет контакт. В случае успеха возвращает true.
Ошибки: 403 — нет прав на удаление контакта, 404 — контакт не найден.
curl --location --request DELETE 'https://api.okocrm.com/v2/contacts/125' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'
Привязка сущностей
Добавляет связи контакта с компаниями и сделками. Уже существующие связи пропускаются без ошибки. В случае успеха возвращает 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"'
Компании
Список компаний
Возвращает список компаний по 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}'
Получить компанию
Возвращает компанию с телефонами, e-mail и тегами. Кроме стандартных полей приходят дополнительные поля, настроенные в аккаунте (ключ — var_name), и описание вкладок карточки с их полями в tabs.
Ошибки: 403 — нет прав на просмотр компании, 404 — компания не найдена.
curl --location 'https://api.okocrm.com/v2/companies/48' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'
Добавить компанию
Создаёт компанию. Кроме перечисленных полей можно передать дополнительные поля, настроенные в аккаунте: ключ — 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)
Изменить компанию
Изменяет поля компании. Кроме перечисленных полей можно передать дополнительные поля, настроенные в аккаунте: ключ — 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'
Удалить компанию
Удаляет компанию. В случае успеха возвращает true.
Ошибки: 403 — нет прав на удаление компании, 404 — компания не найдена.
curl --location --request DELETE 'https://api.okocrm.com/v2/companies/48' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {token}'
Привязка сущностей
Добавляет связи компании с контактами и сделками. Уже существующие связи пропускаются без ошибки. В случае успеха возвращает 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"'
Сделки
Список сделок
Список сделок, новые сначала, по 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}'
Получить сделку
Сделка со связанными контактами, компаниями и участниками. Кроме стандартных полей выводятся доп. поля с ключами cf_*: их набор свой в каждом аккаунте. В tabs приходит описание вкладок карточки и полей на них.
Ошибки: 403 — нет прав на просмотр сделки; 404 — сделка не найдена.
curl -X GET 'https://api.okocrm.com/v2/leads/51877' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Создать сделку
Создаёт сделку. Кроме перечисленных параметров можно передавать доп. поля сделки с ключами 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))
}
Изменить сделку
Меняет переданные поля сделки. Кроме перечисленных параметров можно передавать доп. поля сделки с ключами 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 октября" }
}'
Удалить сделку
Удаляет сделку. В ответ приходит true.
Ошибки: 403 — нет прав на удаление сделки; 404 — сделка не найдена.
curl -X DELETE 'https://api.okocrm.com/v2/leads/51877' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Привязка сущностей
Привязывает к сделке существующие контакты и компании. Уже привязанные пропускаются без ошибки. В ответ приходит 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 }]
}'
Справочники
Позиции из справочников, добавленные в сделку.
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}'
Передавайте только позиции, которые нужно добавить или изменить: позиции, которых нет в запросе, не меняются. Если позиции ещё нет в сделке, она добавится, если есть — обновятся количество и скидка.
Добавлять можно только элементы справочников, подключённых к воронке сделки. Бюджет сделки не пересчитывается: при необходимости измените его через 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 }
]
}'
Убирает одну позицию из сделки. Сам элемент в справочнике остаётся.
Ответ 204 без тела. Ошибки: 403 — нет прав на изменение сделки; 404 — сделка не найдена или такой позиции в сделке нет.
curl -X DELETE 'https://api.okocrm.com/v2/leads/51877/guides/5012' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Воронки
Список воронок
Воронки, доступные пользователю, в порядке сортировки в CRM.
Ошибки: 403 — нет прав на просмотр сделок.
curl -X GET 'https://api.okocrm.com/v2/pipelines' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Этапы воронок
Список этапов
Этапы воронки: сначала обычные в порядке сортировки в CRM, затем системные этапы закрытия сделки. Закрывающие этапы отличаются полем type: 1 — успешно реализована, 2 — закрыта и не реализована.
Ошибки: 403 — нет прав на просмотр воронок.
curl -X GET 'https://api.okocrm.com/v2/pipelines/stages/2' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Причины отказа
Список
Причины отказа. Без 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}'
Задачи
Список задач
Возвращает задачи 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 {{токен}}'
Создать задачу
Создаёт задачу 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))
}
Изменить задачу
Изменяет задачу 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=Перенести звонок на вторник'
Удалить задачу
Удаляет задачу CRM. При успехе возвращается true.
Ошибки:
- 403 — нет права на удаление задачи;
- 404 — задача не найдена.
curl --location --request DELETE 'https://api.okocrm.com/v2/tasks/5310' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}'
Задача выполнена + комментарий
Закрывает задачу CRM (отмечает выполненной). Если передан text, он добавляется к задаче комментарием с результатом. При успехе возвращается true.
Ошибки:
- 403 — нет права на выполнение задачи;
- 404 — задача не найдена.
curl --location 'https://api.okocrm.com/v2/tasks/done/5310' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}' \
--form 'text="Клиент подтвердил доставку на пятницу"'
Отметить пункт чеклиста
Отмечает пункт чеклиста задачи 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"'
Типы задач
Список типов
Возвращает все типы задач аккаунта в порядке сортировки, без пагинации. Список обёрнут в data.
curl --location 'https://api.okocrm.com/v2/tasks/types/' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{токен}}'
Лента событий в сущности
Список событий
Возвращает примечания из ленты событий сделки, контакта или компании. Обязательно передать один из идентификаторов: 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 {{токен}}'
Добавить
Добавляет текстовое примечание в ленту событий сделки, контакта или компании. Нужно передать один из идентификаторов: 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))
}
Прикрепить файл
Загружает один или несколько файлов и прикрепляет их к сделке, контакту или компании одним примечанием в ленте событий. Запрос отправляется как 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"'
Поля сущностей
Список полей
Возвращает поля сделок, контактов, компаний и справочников (системные и дополнительные), без пагинации. Список обёрнут в 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 {{токен}}'
Сообщения в мессенджерах
Список сообщений
Возвращает переписку в мессенджерах по сделке, контакту, компании или диалогу из входящих лидов. Обязательно передать один из параметров: 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 {{токен}}'
Отправить исходящее сообщение
Отправляет исходящее сообщение в мессенджер: в диалог из входящих лидов (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"'
Финансы
Ссылки на оплату
Ссылки на оплату аккаунта, новые первыми (по убыванию 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'
Ссылка на оплату по её 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}'
Ссылка на оплату по 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}'
Вебхуки
Список
Все вебхуки аккаунта. В ответе есть ключ secret каждого вебхука — им проверяется подпись входящих запросов (см. раздел «Входящий запрос»).
Ошибка 403 — у пользователя токена нет права на изменение вебхуков.
curl -X GET 'https://api.okocrm.com/v2/webhooks/' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Создать
Регистрирует адрес, на который 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"
}'
Удалить
Удаляет вебхук. Запросы на его адрес больше не отправляются.
Ответ 204 без тела.
Ошибки: 403 — нет права на изменение вебхуков; 404 — вебхук не найден.
curl -X DELETE 'https://api.okocrm.com/v2/webhooks/12/' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Входящий запрос
Это не метод API OkoCRM, а запрос, который OkoCRM отправляет на адрес (url) зарегистрированного вебхука со статусом active.
Событие. client_message — клиент прислал входящее сообщение в подключённый мессенджер. Исходящие сообщения менеджеров вебхук не вызывают. Если вебхуков с этим типом несколько, запрос уходит на каждый адрес.
Запрос. Метод POST, тело — JSON. Заголовки:
Content-Type: application/jsonUser-Agent: OkoCRM Webhook/1.0Signature— подпись тела: 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
Входящие лиды
Прикрепить диалог к контакту
Привязывает диалог из входящих лидов к существующему контакту: канал мессенджера сохраняется у контакта, переписка переносится в его карточку. Нужно передать 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"'
Шаблоны
Документы
Шаблоны документов аккаунта, отсортированные по категории и названию. id шаблона передаётся в POST templates/docs/generate.
Ошибка 403 — у пользователя нет доступа ни к шаблонам, ни к сделкам, контактам или компаниям.
curl -X GET 'https://api.okocrm.com/v2/templates/docs' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Создаёт документ по шаблону для сделки, контакта или компании — так же, как кнопка в карточке. Генерация синхронная: в ответе уже готовый файл и ссылка на него. Документ появляется в карточке сущности.
Передайте 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
}'
Справочники
Справочники и категории
Справочники аккаунта: «Товары», «Услуги» и созданные вручную. id справочника нужен для фильтра элементов и создания элемента.
curl -X GET 'https://api.okocrm.com/v2/guides' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Плоский список категорий справочника. Дерево строится по parent_id: у корневых категорий он равен null.
Если справочник не найден — 404.
curl -X GET 'https://api.okocrm.com/v2/guides/11/categories' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Единицы измерения и НДС
Системные единицы измерения и созданные в аккаунте. id передаётся в unit_id при создании и изменении элемента справочника.
curl -X GET 'https://api.okocrm.com/v2/guides/units' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Ставки НДС. id передаётся в nds_id при создании и изменении элемента справочника.
curl -X GET 'https://api.okocrm.com/v2/guides/nds' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Элементы справочника
Элементы справочников, отсортированные по 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=Швабра'
Элемент справочника по его ID. Если элемент не найден — 404.
curl -X GET 'https://api.okocrm.com/v2/guides/items/5012' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Создаёт товар или услугу в справочнике. В ответе возвращается созданный элемент в том же формате, что в 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
}'
Меняются только переданные поля, остальные остаются как были. Чтобы очистить необязательное поле, передайте 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
}'
Удаляет элемент справочника. Как и в интерфейсе, вместе с ним удаляются его позиции во всех сделках.
Ответ 204 без тела. Если элемент не найден — 404.
curl -X DELETE 'https://api.okocrm.com/v2/guides/items/5012' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Вспомогательные данные
Список городов
Справочник городов, по алфавиту.
На странице 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'
Клиенты
Клиенты
Клиенты, отсортированные по 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'
Клиент по его ID. Если клиент не найден или не виден пользователю по правам — 404.
curl -X GET 'https://api.okocrm.com/v2/customers/310' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Создаёт клиента из контакта или компании — передаётся ровно одно из двух. Переданы оба — 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
}'
Меняет название и ответственного клиента. Меняются только переданные поля, остальные остаются как были.
В ответе — обновлённый клиент в формате 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
}'
Контакты и компании клиента
Привязывает контакт к клиенту. Если контакт уже привязан, меняются переданные флаги.
Первый привязанный контакт становится главным. У главного контакта автоматические касания всегда включены.
Для новой связи контакт должен быть виден пользователю в 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
}'
Отвязывает контакт от клиента. Если отвязан главный, главным становится следующий.
Ответ 204 без тела.
Ошибки: 404 — клиент не найден или контакт к нему не привязан; 422 — это последний контакт или компания клиента: клиент без связей не остаётся.
curl -X DELETE 'https://api.okocrm.com/v2/customers/310/contacts/56' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Привязывает компанию к клиенту. Если компания уже привязана, меняется переданный флаг.
Первая привязанная компания становится главной.
Для новой связи компания должна быть видна пользователю в 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
}'
Отвязывает компанию от клиента. Если отвязана главная, главной становится следующая.
Ответ 204 без тела.
Ошибки: 404 — клиент не найден или компания к нему не привязана; 422 — это последний контакт или компания клиента: клиент без связей не остаётся.
curl -X DELETE 'https://api.okocrm.com/v2/customers/310/companies/81' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer {token}'
Покупки клиента
Покупки клиента: оплаченные, предстоящие и отменённые. Сортировка по 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'
Создаёт покупку клиента с источником 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"
}'
Проекты
Задачи проекта
Создаёт задачу в указанном проекте и этапе. Автором задачи становится пользователь, от имени которого выполнен запрос.
Даты передаются в формате 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}
Комментарии к задачам
Добавляет комментарий к задаче проекта от имени пользователя, выполняющего запрос. Пользователь должен иметь доступ к задаче.
Ошибки:
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}
Командный чат
Сообщения
Отправляет сообщение в командный чат от имени пользователя, выполняющего запрос. Нужно передать хотя бы один из параметров, определяющих, куда писать:
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 — там путь разработчика по шагам: что сделать, в каком порядке и какой результат ждать на каждом шаге. Разделы ниже — справочник по каждому запросу.
Схема подключения.
- Вы публикуете виджет в OkoStore с блоком
telephonyвconfig.json. OkoCRM автоматически даёт такому виджету категорию «Телефония»: трубки у номеров телефона, стандартные вкладки настроек (сотрудники, внешние линии, правила). - Клиент устанавливает виджет и сохраняет настройки (например, вводит ключ API вашей АТС). Как только настройки сохранены, OkoCRM присылает на ваш
webhook_urlсобытиеinstalledс токеном подключения. - Ваш сервер проверяет доступ к своей АТС и вызывает
PUT extensionsиPUT numbers— передаёт список внутренних номеров сотрудников и внешних линий клиента. Если что-то не так — сообщает черезPOST status. - По каждому звонку ваша АТС вызывает
POST route(кому перевести входящий на общий номер) и шлёт события звонка черезPOST events:call.answered/call.started→call.ended, либоcall.missed. - Когда менеджер жмёт трубку в CRM, OkoCRM обращается к вашему
click_to_call_url— это команда вашей АТС инициировать звонок. - Клиент изменил настройки — вы получаете
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) — он устанавливается на ваш собственный аккаунт без модерации и ведёт себя точно так же, как публичный. Проверьте на нём:
webhook_urlиclick_to_call_urlвconfig.json— обаhttps://, оба реально отвечают с внешнего адреса.- Виджет установлен, настройки сохранены → пришёл
installedс корректной подписью вSignatureи валиднымtoken. - В ответ на
installedвы вызвалиPUT extensionsиPUT numbers— в CRM во вкладке «Сотрудники» видны присланные номера, во вкладке «Звонки» — внешние линии. POST statusсerrorгасит трубки в CRM,ok— включает обратно.POST routeотвечаетroute_toдля клиента с ответственным менеджером иnull, когда правило не применяется.- Полный цикл событий: входящий принятый (
call.answered→call.ended), исходящий (call.started→call.ended), пропущенный (call.missed) — во всех случаях звонок появляется в ленте клиента. - Запись разговора: либо сразу в
call.ended, либо позже отдельнымcall.recorded— плеер и транскрибация появляются в карточке звонка. - Нажатие трубки в карточке контакта доходит до вашего
click_to_call_urlс ожидаемыми полями, звонок инициируется, а при ошибке ваш{"message": "..."}виден менеджеру. - Повторная отправка одного и того же события в течение нескольких секунд не создаёт дублей в ленте.
- Изменение настроек виджета присылает
settings_updated; удаление —uninstalled, и старый токен после этого возвращает404. - Перевыпуск токена во вкладке «Подключение» присылает новый
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, и сравнивайте функцией сравнения за постоянное время. Не совпало — отклоняйте запрос.
Запрос на ваш 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
Менеджер нажал трубку у номера в карточке клиента или сработало автодействие «Позвонить клиенту». 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 вы сообщаете, что подключение клиента на вашей стороне сломалось или снова заработало.
Полный список внутренних номеров сотрудников клиента. Присылайте его целиком при каждом изменении: номера, которых нет в списке, 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
Полный список внешних номеров (линий), с которых АТС клиента звонит и принимает звонки. Как и 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'],
],
]);
Сообщите, если на вашей стороне что-то не так с подключением клиента — например, ключ 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 — один и тот же идентификатор на весь звонок от первого до последнего события по нему; придумываете и храните его вы (например, идентификатор звонка в вашей АТС).
Вызывается синхронно на входящем звонке на общий номер клиента — до того, как звонок начал маршрутизироваться внутри вашей АТС. 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);
}
Основной канал: сюда вы шлёте всё, что происходит со звонком. Тело — общий конверт (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 — там путь разработчика по шагам: что сделать, в каком порядке и какой результат ждать на каждом шаге. Разделы ниже — справочник по каждому запросу.
Схема подключения.
- Вы публикуете виджет в OkoStore с блоком
smsвconfig.json. OkoCRM автоматически даёт такому виджету категорию «SMS»: кнопка отправки SMS в карточке, выбор отправителя в шаблонах и автодействиях, вкладка «Подключение» в настройках виджета. - Клиент устанавливает виджет и сохраняет настройки (например, вводит ключ API вашего шлюза). Как только настройки сохранены, OkoCRM присылает на ваш
webhook_urlсобытиеinstalledс токеном подключения. - Ваш сервер проверяет доступ к своему шлюзу и вызывает
PUT senders— передаёт список имён отправителей, доступных клиенту. Если что-то не так (неверный ключ, нулевой баланс) — сообщает черезPOST status. - Менеджер, автодействие или ИИ-ассистент отправляет SMS — OkoCRM присылает на ваш
send_urlзапросsms.send. Вы принимаете сообщение в отправку и отвечаете своим идентификатором сообщенияmessage_id. - По мере доставки вы присылаете статусы через
POST events, событиеsms.status:sent,delivered,readилиfailed. Менеджер видит их в ленте: одна галка, две галки, прочитано, красная заметка с причиной ошибки. - Если ваш шлюз принимает ответные SMS — присылайте их событием
sms.incoming. Это необязательно. - Клиент изменил настройки — вы получаете
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) — он устанавливается на ваш собственный аккаунт без модерации и ведёт себя точно так же, как публичный. Проверьте на нём:
webhook_urlиsend_urlв блокеsms— обаhttps://, оба реально отвечают с внешнего адреса, блокаtelephonyв том же виджете нет.- Виджет установлен, настройки сохранены → пришёл
installedс корректной подписью вSignatureи валиднымtoken. Подпись проверяется по сырому телу, и уsms.sendтоже. - В ответ на
installedвы вызвалиPUT senders— во вкладке «Подключение» настроек виджета видны присланные имена, они же доступны при отправке SMS из карточки, в шаблонах и в автодействии «Отправить SMS». - Отправка SMS из карточки доходит до
send_urlс ожидаемыми полями, ответ сmessage_idукладывается в 4 секунды, в ленте появляется SMS с одной галкой. - Идемпотентность: повторный
sms.sendс тем жеIdempotency-Keyне отправляет SMS второй раз и возвращает тот жеmessage_id. «Повторить» в ленте приходит с новым ключом. - Ошибка отправки: ваш
error.messageвиден менеджеру в красной заметке; «Повторить» после отказа отправляет сообщение заново. - Отложенная отправка: при непустом
send_atсообщение уходит получателю в указанное время. - Статусы:
sms.statusсdeliveredдаёт две галки, сfailed— красную заметку с вашимerror.message; повтор события в течение нескольких секунд не меняет результат. POST statusсerrorпри проблемах с балансом или ключом гасит кнопку SMS в карточке и показывает ваш текст в настройках виджета,ok— включает обратно.- Если поддерживаете входящие:
sms.incomingс номером существующего контакта появляется в его ленте, ответственный получает уведомление, повтор с тем жеmessage_idвторой заметки не создаёт. - Изменение настроек виджета присылает
settings_updated; удаление —uninstalled, и старый токен после этого возвращает404. - Перевыпуск токена во вкладке «Подключение» присылает новый
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, и подпись посчитана именно по такой строке. Сравнивайте функцией сравнения за постоянное время. Не совпало — отклоняйте запрос.
Запрос на ваш 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
Менеджер отправил 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 вы сообщаете, что подключение клиента на вашей стороне сломалось (неверный ключ, кончился баланс) или снова заработало.
Полный список имён отправителей (альфа-имён или номеров), от которых клиент может отправлять 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
Сообщите, если на вашей стороне что-то не так с подключением клиента: ключ 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; присылайте его строкой, символ в символ.
Сюда вы шлёте статусы доставки отправленных сообщений и входящие 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 — там путь разработчика по шагам. Разделы ниже — справочник по каждому запросу.
Схема подключения.
- Вы публикуете виджет в OkoStore с блоком
mailingвconfig.json. OkoCRM автоматически даёт такому виджету категорию «Рассылка» и вкладки «Подключение» и «Списки рассылок» в настройках виджета. - Клиент устанавливает виджет и сохраняет настройки (например, вводит ключ API вашего сервиса). Как только настройки сохранены, OkoCRM присылает на ваш
webhook_urlсобытиеinstalledс токеном подключения. - Ваш сервер проверяет доступ к кабинету клиента у себя и вызывает
PUT lists— передаёт полный перечень списков рассылок клиента. Если что-то не так (неверный ключ, кабинет заблокирован) — сообщает черезPOST status. - В CRM у клиента появляются автодействия «Добавить в список рассылки» и «Удалить из списка рассылки». Когда автодействие срабатывает, OkoCRM присылает на ваш
subscribe_urlилиunsubscribe_urlкоманду по одному адресу и одному списку. Вы отвечаете статусом: сделано сразу либо ждёт подтверждения человеком. - Всё, что происходит с подпиской и письмами на вашей стороне, вы присылаете через
POST events: подтверждение подписки, отписку по ссылке в письме, статусы писем. События можно слать по одному или пачками до 100 штук. - Клиент изменил настройки — вы получаете
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) — он устанавливается на ваш собственный аккаунт без модерации и ведёт себя точно так же, как публичный. Проверьте на нём:
- Три адреса в блоке
mailing— всеhttps://, все реально отвечают с внешнего адреса, блоковtelephonyиsmsв том же виджете нет. - Виджет установлен, настройки сохранены → пришёл
installedс корректной подписью вSignatureи валиднымtoken. Подпись проверяется по сырому телу, и у команд тоже. - В ответ на
installedвы вызвалиPUT lists— во вкладке «Списки рассылок» настроек виджета видны присланные списки, в настройках воронки появились автодействия «Добавить в список рассылки» и «Удалить из списка рассылки», в них выбираются ваш сервис и его списки. - Автодействие «Добавить в список рассылки» доходит до
subscribe_urlс ожидаемыми полями, ответ{"status": "subscribed"}укладывается в 4 секунды, в ленте контакта появляется «… добавлен в список контактов … для рассылки». - Double opt-in: ответ
{"status": "pending"}даёт в ленте «ждём подтверждения»; после событияmailing.subscriptionсо статусомsubscribedта же запись становится «добавлен в список». - Повторное срабатывание автодействия для уже подписанного адреса возвращает
subscribedи не создаёт дублей у вас; повторный запрос с тем жеIdempotency-Keyне выполняется дважды. - Отказ: ответ
4xxс{"error": {"code": "...", "message": "..."}}показывает менеджеру вашmessage; ответы5xxи429приводят к повтору запроса с тем же ключом. - «Удалить из списка рассылки» доходит до
unsubscribe_url; удаление отсутствующего адреса отвечаетunsubscribed. - Отписка по ссылке в письме присылает
mailing.subscriptionсо статусомunsubscribed— в ленте «… отписан от списка …». - Статусы писем уходят пачками через
POST events:sent,delivered,opened,clickedдают одну запись письма с метками;campaign.idиlist_idпередаются строками; ответ пачки сrejectedобрабатывается,429— пауза поRetry-After. POST statusсerrorпри проблемах с ключом или кабинетом показывает ваш текст в настройках виджета и останавливает команды,ok— включает обратно.- Изменение настроек виджета присылает
settings_updated; удаление —uninstalled, и старый токен после этого возвращает404. - Перевыпуск токена во вкладке «Подключение» присылает новый
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, и подпись посчитана именно по такой строке. Сравнивайте функцией сравнения за постоянное время. Не совпало — отклоняйте запрос.
Запрос на ваш 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
Сработало автодействие клиента «Добавить в список рассылки». 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 });
});
Сработало автодействие клиента «Удалить из списка рассылки». 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 вы сообщаете, что подключение клиента на вашей стороне сломалось (неверный ключ, заблокирован кабинет) или снова заработало.
Полный перечень списков (адресных баз, сегментов — того, куда у вас добавляют подписчиков) в кабинете клиента. Клиент выбирает из них списки в автодействиях «Добавить в список рассылки» и «Удалить из списка рассылки» и видит их во вкладке «Списки рассылок» настроек виджета. Присылайте перечень целиком после 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'])
Сообщите, если на вашей стороне что-то не так с подключением клиента: ключ 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 штук. Идентификаторы списков и рассылок передавайте строками.
Сюда вы шлёте всё, что происходит с подписками и письмами на вашей стороне. Один адрес принимает два формата:
- Одно событие — тело есть само событие. Ответ
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иcomplainedCRM сама не считает адрес отписанным: если вы исключили адрес из списка — пришлите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