WhatsApp API
Отправка первого сообщения#
Обычное текстовое сообщение. Тело представляет собой стандартный payload Cloud API.
POST/v26.0/{phone-number-id}/messagesAuthorization: Bearer <DEVICE_KEY>
curl -X POST https://api.sybox.io/v26.0/PHONE_NUMBER_ID/messages \
-H "Authorization: Bearer YOUR_DEVICE_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "text",
"text": { "body": "Hello from SyBox" }
}'const res = await fetch('https://api.sybox.io/v26.0/PHONE_NUMBER_ID/messages', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.SYBOX_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
messaging_product: 'whatsapp',
to: '15551234567',
type: 'text',
text: { body: 'Hello from SyBox' }
})
});
console.log(res.status, await res.json());import os, requests
r = requests.post(
"https://api.sybox.io/v26.0/PHONE_NUMBER_ID/messages",
headers={"Authorization": "Bearer " + os.environ["SYBOX_KEY"]},
json={
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "text",
"text": {"body": "Hello from SyBox"},
},
timeout=30,
)
print(r.status_code, r.json())$ch = curl_init('https://api.sybox.io/v26.0/PHONE_NUMBER_ID/messages');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SYBOX_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'messaging_product' => 'whatsapp',
'to' => '15551234567',
'type' => 'text',
'text' => ['body' => 'Hello from SyBox'],
]),
]);
echo curl_exec($ch);using System.Net.Http.Headers;
using System.Text;
using var http = new HttpClient(); // in a real app, keep one HttpClient for the whole app
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("SYBOX_KEY"));
string json = """
{
"messaging_product": "whatsapp",
"to": "15551234567",
"type": "text",
"text": { "body": "Hello from SyBox" }
}
""";
var res = await http.PostAsync(
"https://api.sybox.io/v26.0/PHONE_NUMBER_ID/messages",
new StringContent(json, Encoding.UTF8, "application/json"));
Console.WriteLine(await res.Content.ReadAsStringAsync());
Ответ#
Вы получаете ровно то, что возвращает Meta, с тем же статусом HTTP.
200 OK · application/json {
"messaging_product": "whatsapp",
"contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
"messages": [{ "id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI…" }]
}
Ваша система в SyBox
Отображение ваших данных рядом с чатом#
Когда оператор открывает чат, SyBox вызывает ВАШ эндпоинт и показывает полученные данные прямо рядом с чатом.
Добавьте свой эндпоинт в разделе Настройки › Интеграции › ERP API. Каждый вызов — это POST с вашим ключом в Authorization и запросом в одном поле data, содержащем JSON-строку, — так любая из сторон может позже добавить поля, ничего не сломав другой. Отвечайте в течение 15 секунд.
POST YOUR_ERP_URL
Authorization: Bearer YOUR_ERP_KEY
X-Sybox-Action: lookup
X-Sybox-Lang: en
X-Sybox-Theme: light
X-Sybox-Agent: 6f1c…
X-Sybox-Number: 104857600000000
Content-Type: application/json
{ "data": "{\"phone\":\"15551234567\",\"email\":\"customer@example.com\",\"ref\":\"\"}" }{
"renderAs": "table",
"content": [
{ "Order": "#10432", "Status": "Shipped", "Total": "$1,250.00", "sybox_ref": "10432" },
{ "Order": "#10388", "Status": "Delivered", "Total": "$310.00", "sybox_ref": "10388" }
]
}
Каждый вызов также сообщает, кто спрашивает: X-Sybox-Lang (язык пользователя — отвечайте на нём), X-Sybox-Theme (light или dark, для ответа html), X-Sybox-Agent (id пользователя) и X-Sybox-Number (номер, на котором идёт разговор).
Мы передаем телефон и e-mail — ответьте структурой { renderAs, content }.
renderAs#
Укажите renderAs, чтобы сообщить SyBox, как отобразить ваш контент:
| renderAs | Описание |
|---|
table | Строки и столбцы. content — массив плоских объектов (или { rows: [ … ] }); их ключи становятся заголовками столбцов. |
cards · kpi | Небольшие плитки, например баланс или число заказов. content — массив { label, value } (или { cards: [ … ] }). |
feed · list · thread | Лента карточек. content — массив { title, text, date, tags, media: [{ type, url, name }] }. |
message | Одна запись в виде одной карточки, похожей на сообщение. |
html | Ваш собственный HTML в виде строки в content. Только для разметки, которую вы формируете сами. |
sections | Несколько блоков в одном ответе: content — это { sections: [ { title, renderAs, content } ] }. |
json | Вариант по умолчанию, если renderAs не указан: SyBox показывает ваш content как отформатированный JSON. |
Добавьте sybox_ref к любой строке таблицы, чтобы сделать ее кликабельной.
Поиск товаров (X-Sybox-Action: products)#
Тот же эндпоинт может отвечать и на поиск товаров. Пользователь вводит то, что ищет клиент, SyBox отправляет это как «q», а ваш ответ отображается в виде карточек товаров рядом с чатом — с отправкой в одно касание, которая вставляет ссылку на товар в поле ввода сообщения.
POST YOUR_ERP_URL
Authorization: Bearer YOUR_ERP_KEY
X-Sybox-Action: products
# the decoded "data":
{ "q": "wireless keyboard", "phone": "15551234567", "email": "customer@example.com", "ref": "" }{
"content": [
{
"name": "Wireless Keyboard K380",
"price": 449,
"oldPrice": 520,
"currency": "USD",
"available": true,
"stock": 12,
"image": "https://cdn.example.com/k380.jpg",
"link": "https://shop.example.com/p/k380",
"note": "Ships in 24h"
}
]
}
| Поле | Описание |
|---|
nameобязательно | Единственное обязательное поле. Товар с одним лишь названием тоже даёт аккуратную карточку. |
price · oldPrice · currency | Числа, а не отформатированные строки. Присылайте oldPrice только при наличии реальной прежней цены — скидка считается из пары. |
available · stock | available — true/false; stock — необязательное число рядом. Не присылайте их вовсе, если не ведёте остатки: отсутствующее поле ничего не рисует и никогда не читается как «нет в наличии». |
image | Прямая https-ссылка на изображение. Если картинка не загрузилась, остаётся нейтральный фон. |
link | Ссылка на страницу товара. Без неё карточка только для чтения — «Отправить» появляется, лишь когда есть что отправить. |
note | Одна короткая свободная строка под ценой — срок доставки, вариант, условие. |
Ничего сверх уже сделанного: тот же URL, тот же ключ, тот же конверт { "data": "…" }. Отличайте поиск товара от запроса клиента по заголовку X-Sybox-Action. Верните content обычным массивом (или { items: [ … ] }).
События
Вебхуки: события на вашем сервере#
Добавьте свой эндпоинт в разделе Настройки › Интеграции › Вебхуки, выберите события и номера, и SyBox будет отправлять на него POST с каждым событием в момент, когда оно происходит. Событие WhatsApp приходит в собственном формате вебхуков Meta, поэтому обработчик, написанный для Meta, работает без изменений.
| Событие | Описание |
|---|
messageВходящее сообщение | Сообщение клиента. |
statusСтатус сообщения | Отправлено, доставлено, прочитано или не доставлено — для отправленного вами сообщения. |
reactionРеакция | Клиент отреагировал на сообщение. |
doneДиалог отмечен как завершённый | Диалог был отмечен как завершённый: пользователем, автоматизацией или автоматически после молчания клиента. |
labelМетка добавлена или удалена | К диалогу была добавлена метка или удалена из него. |
contactКонтакт создан или обновлён | Был создан контакт или изменились его имя, эл. почта либо поля контакта. |
dealСделка закрыта | В диалоге была записана закрытая сделка. |
caseОбращение CRM открыто или закрыто | Обращение CRM было открыто или закрыто (решено, не решено или отменено). |
Запрос · SyBox → вам POST YOUR_WEBHOOK_URL
Content-Type: application/json
X-Hub-Signature-256: sha256=5d1f0c…
x-sybox-signature: sha256=5d1f0c…
x-sybox-event: message
x-sybox-timestamp: 1761400000
{
"object": "whatsapp_business_account",
"entry": [{
"id": "WABA_ID",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": { "display_phone_number": "15550001111", "phone_number_id": "PHONE_NUMBER_ID" },
"contacts": [{ "profile": { "name": "Sara" }, "wa_id": "15551234567" }],
"messages": [{
"from": "15551234567",
"id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgASGBQ…",
"timestamp": "1761400000",
"type": "text",
"text": { "body": "Is my order shipped?" }
}]
}
}]
}]
}
Медиафайлы. Собственная ссылка Meta открывается только с токеном Meta, поэтому SyBox заменяет её подписанной ссылкой на свою копию файла, действительной 24 часа. Голосовое сообщение также может содержать свой текст в sybox.transcription, если вебхук его запрашивает.
Другие каналы — Messenger, Instagram, Telegram и онлайн-чат — приходят в собственном формате SyBox, и только для события сообщения:
{
"object": "sybox",
"event": "message",
"channel": "telegram",
"line": "8123456789",
"workspace": "yourworkspace",
"contact": { "key": "tg:6423115118", "name": "Sara" },
"message": {
"id": "812345",
"timestamp": 1761400000,
"type": "image",
"text": "",
"caption": "The broken part",
"media": { "url": "https://api.sybox.io/…", "mime": "image/jpeg" }
}
}
События диалогов приходят в собственном формате SyBox для любого канала. Поле event точно указывает, что произошло:
| Событие | <code>event</code> |
|---|
| Диалог отмечен как завершённый | conversation.done |
| Метка добавлена или удалена | label.added · label.removed |
| Контакт создан или обновлён | contact.created · contact.updated |
| Сделка закрыта | deal.closed |
| Обращение CRM открыто или закрыто | case.opened · case.closed |
| Тестовое событие | test |
{
"object": "sybox",
"event": "conversation.done",
"channel": "whatsapp",
"line": "201001234567",
"workspace": "yourworkspace",
"timestamp": 1761400000,
"contact": {
"key": "201112223344",
"name": "Sara",
"phone": "201112223344",
"email": "",
"channel": "whatsapp",
"labels": [{ "id": 3, "name": "VIP" }],
"fields": [{ "id": 2, "key": "city", "name": "City", "value": "Cairo" }]
},
"data": { "by": "Omar", "note": "Refund sent" }
}{
"object": "sybox",
"event": "case.closed",
"workspace": "yourworkspace",
"timestamp": 1761400000,
"case": {
"id": 8124,
"guid": "5f0c1e9a…",
"subject": "Screen replacement not delivered",
"type": 1,
"status": "solved",
"department": "Support"
},
"customer": { "name": "Sara", "phone": "201112223344" }
}
Ключ подписи#
| Заголовок | Описание |
|---|
X-Hub-Signature-256 | sha256= и шестнадцатеричный HMAC-SHA256 исходного тела, вычисленный с секретом подписи вебхука, — тот же заголовок и тот же метод, что у Meta. |
x-sybox-signature | То же значение, чтобы эндпоинт, который также получает вызовы самой Meta, мог отличить наши. |
x-sybox-event | Что произошло: message, status, reaction, одно из событий диалога ниже (conversation.done, label.added…) или test. |
x-sybox-timestamp | Время отправки в секундах Unix. |
x-sybox-signature-rsa | Вторая подпись: rsa-sha256= и RSA-SHA256 (PKCS#1 v1.5) в base64 от x-sybox-timestamp, точки и исходного тела. |
x-sybox-key-id | Каким из наших ключей она создана. Открытый ключ находится по адресу https://api.sybox.io/.well-known/sybox-webhook-key. |
Проверяйте подпись по исходным байтам до их разбора и сравнивайте за постоянное время:
const crypto = require('crypto');
// express.raw: the signature is over the bytes as sent, so read them before any JSON parser.
app.post('/sybox/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sent = Buffer.from(req.get('X-Hub-Signature-256') || '');
const want = Buffer.from('sha256=' + crypto
.createHmac('sha256', process.env.SYBOX_WEBHOOK_SECRET)
.update(req.body)
.digest('hex'));
if (sent.length !== want.length || !crypto.timingSafeEqual(sent, want)) return res.sendStatus(401);
res.sendStatus(200); // answer first, then do the work
const event = JSON.parse(req.body);
const kind = req.get('x-sybox-event'); // message | status | reaction
});$raw = file_get_contents('php://input');
$want = 'sha256=' . hash_hmac('sha256', $raw, getenv('SYBOX_WEBHOOK_SECRET'));
$sent = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
if (!hash_equals($want, $sent)) {
http_response_code(401);
exit;
}
http_response_code(200);
$event = json_decode($raw, true);
$kind = $_SERVER['HTTP_X_SYBOX_EVENT'] ?? ''; // message | status | reactionimport hashlib, hmac, os
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/sybox/webhook")
def sybox_webhook():
raw = request.get_data()
want = "sha256=" + hmac.new(os.environ["SYBOX_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(want, request.headers.get("X-Hub-Signature-256", "")):
abort(401)
event = request.get_json()
kind = request.headers.get("x-sybox-event") # message | status | reaction
return "", 200using System.Security.Cryptography;
using System.Text;
app.MapPost("/sybox/webhook", async (HttpRequest req) =>
{
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
byte[] raw = ms.ToArray();
string secret = Environment.GetEnvironmentVariable("SYBOX_WEBHOOK_SECRET")!;
byte[] mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), raw);
byte[] want = Encoding.ASCII.GetBytes("sha256=" + Convert.ToHexString(mac).ToLowerInvariant());
byte[] sent = Encoding.ASCII.GetBytes(req.Headers["X-Hub-Signature-256"].ToString());
if (!CryptographicOperations.FixedTimeEquals(want, sent)) return Results.Unauthorized();
string kind = req.Headers["x-sybox-event"]; // message | status | reaction
return Results.Ok();
});
Не хотите хранить общий секрет на своём сервере? Тогда проверяйте x-sybox-signature-rsa нашим открытым ключом. В любом случае отклоняйте метку времени старше пяти минут.
Ответьте любым кодом 2xx в течение 15 секунд. При любом другом исходе — ошибке, тайм-ауте, перенаправлении — попытка повторяется через 1, 2, 4, 8, 16 и 32 минуты, после чего событие отбрасывается. Событие может прийти больше одного раза: используйте id сообщения, чтобы пропускать повторы. Адрес должен быть публичным и использовать https.
Вебхук, который завершается ошибкой 20 раз подряд на протяжении более часа, автоматически отключается, а администратор рабочего пространства получает уведомление. Ожидающие события хранятся 24 часа: включите его снова, чтобы отправить их.
Для проверки нажмите «Отправить тестовое событие» на карточке вебхука. В окне «Доставки» на той же карточке показаны последние 50 доставок с ответом вашего сервера, и любую из них можно отправить снова.
Онлайн-чат
Онлайн-чат#
Добавьте чат на свой сайт: посетители пишут там, а ваша команда отвечает в SyBox. Добавьте сайт в разделе Настройки › Сообщения › Каналы, на карточке «Онлайн-чат»; в открывшемся окне «Код для вашего сайта» вы получите код и ключи, описанные ниже. Три секретных ключа показываются только один раз.
| Ключ | Начинается с | Для чего |
|---|
| Открытый ключ | lcpk_ | Размещается на ваших страницах. Его можно публиковать: он только указывает сайт, а чат отображается только на адресах из вашего списка «Адреса сайтов». |
| Ключ подписи для авторизованных пользователей | lcid_ | Только на вашем сервере. Подписывает каждого авторизованного пользователя, чтобы никто не мог писать от чужого имени. |
| Ключ сервера (для собственного экрана чата) | lcsk_ | Только на вашем сервере. Нужен только сайту, который создаёт собственное окно чата вместо готового чата. |
| Ключ подписи ответов | lcwh_ | Только на вашем сервере. Подтверждает, что ответ, отправленный на ваш сервер, пришёл от SyBox. |
Готовому чату нужен только код. Ключ подписи нужен сайту с учётными записями; ключ сервера и ключ подписи ответов — только сайту с собственным окном чата.
Если выключить сайт, чат сразу скрывается. Вызовы вашего сервера по-прежнему принимаются и сохраняются.
Добавьте чат на свой сайт#
Вставьте код один раз на каждую страницу, прямо перед закрывающим тегом body. Чат отображается только на адресах из списка «Адреса сайтов», а его цвет, положение, заголовок и приветствие настраиваются в SyBox, а не в коде.
<script async src="https://api.sybox.io/lc/w.js" data-key="lcpk_YOUR_PUBLIC_KEY"></script>
WordPress: скачайте наш плагин в том же окне. Shopify: в приложении SyBox для Shopify включите «Онлайн-чат в этом магазине», затем SyBox Live Chat в разделе App embeds — авторизованный покупатель распознаётся без каких-либо дополнительных действий.
Авторизованные пользователи: ключ подписи#
Когда пользователь вошёл на ваш сайт, ваш сервер сообщает чату, кто он, с помощью короткого токена — JWT, HS256, — подписанного ключом подписи. Тогда у пользователя один разговор под его именем на всех устройствах, и никто не сможет открыть его, введя чужой id.
- После входа ваш сервер формирует токен из перечисленных ниже claims.
- Страница передаёт его чату как
identity — до кода чата. - Если вход произошёл без перезагрузки страницы, вызовите вместо этого
SyBoxChat('identify', token).
| Поле | Описание |
|---|
subобязательноstring | id вашего пользователя, до 120 символов. Он не может начинаться с v_. |
expобязательноnumber | Срок действия в секундах Unix — не более чем на сутки вперёд. Десяти минут достаточно: токен читается только при открытии чата. |
namestring | Имя, которое показывается в разговоре. |
emailstring | Адрес электронной почты пользователя. |
phonestring | Номер телефона пользователя. |
picturestring | Адрес https фотографии пользователя, до 500 символов. SyBox хранит свою копию и загружает её заново при следующем входе, если адрес изменился. |
function b64url(string $s): string { return rtrim(strtr(base64_encode($s), '+/', '-_'), '='); }
$header = b64url(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = b64url(json_encode([
'sub' => (string) $user->id,
'name' => $user->name,
'email' => $user->email,
'exp' => time() + 600, // ten minutes
]));
$signature = b64url(hash_hmac('sha256', "$header.$payload", getenv('SYBOX_LC_SIGNING_KEY'), true));
$token = "$header.$payload.$signature";const crypto = require('crypto');
const b64url = (s) => Buffer.from(s).toString('base64url');
const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
const payload = b64url(JSON.stringify({
sub: String(user.id),
name: user.name,
email: user.email,
exp: Math.floor(Date.now() / 1000) + 600 // ten minutes
}));
const signature = crypto.createHmac('sha256', process.env.SYBOX_LC_SIGNING_KEY)
.update(header + '.' + payload).digest('base64url');
const token = header + '.' + payload + '.' + signature;import base64, hashlib, hmac, json, os, time
def b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
header = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
payload = b64url(json.dumps({
"sub": str(user.id),
"name": user.name,
"email": user.email,
"exp": int(time.time()) + 600, # ten minutes
}).encode())
key = os.environ["SYBOX_LC_SIGNING_KEY"].encode()
signature = b64url(hmac.new(key, f"{header}.{payload}".encode(), hashlib.sha256).digest())
token = f"{header}.{payload}.{signature}"using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
static string B64Url(byte[] b) => Convert.ToBase64String(b).TrimEnd('=').Replace('+', '-').Replace('/', '_');
string header = B64Url(JsonSerializer.SerializeToUtf8Bytes(new { alg = "HS256", typ = "JWT" }));
string payload = B64Url(JsonSerializer.SerializeToUtf8Bytes(new {
sub = user.Id.ToString(),
name = user.Name,
email = user.Email,
exp = DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds()
}));
byte[] key = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SYBOX_LC_SIGNING_KEY"));
string signature = B64Url(HMACSHA256.HashData(key, Encoding.ASCII.GetBytes(header + "." + payload)));
string token = header + "." + payload + "." + signature;
<script>
window.SyBoxChatConfig = { identity: "TOKEN_FROM_YOUR_SERVER", locale: "en" };
</script>
<script async src="https://api.sybox.io/lc/w.js" data-key="lcpk_YOUR_PUBLIC_KEY"></script>
Создавайте токен только на своём сервере. Ключ подписи на веб-странице позволит любому писать от имени любого из ваших пользователей. И никогда не размещайте на странице просто id пользователя вместо токена: так любой сможет выбрать, чей разговор читать.
Если включено «Только авторизованные пользователи моего сайта», посетитель без токена не видит чат.
Посетителя, который писал в чат до входа, один раз спросят в том же браузере, продолжить ли этот разговор в его учётной записи. Вам для этого ничего делать не нужно.
Управление чатом со своей страницы#
Код чата добавляет на вашу страницу одну функцию, SyBoxChat:
SyBoxChat('open'); // open the chat
SyBoxChat('close'); // close it
SyBoxChat('identify', token); // after a sign-in without a page reload
SyBoxChat('locale', 'fr'); // the chat's language
SyBoxChat('on', 'message', function (m) {
// a reply arrived
});
SyBoxChat('on', 'open', function () { /* the chat opened */ });
SyBoxChat('on', 'close', function () { /* the chat closed */ });
// Before the chat's code, every setting is optional:
window.SyBoxChatConfig = {
identity: 'TOKEN_FROM_YOUR_SERVER', // a signed-in user (see above)
name: 'Sara', email: 'sara@example.com', // a visitor without an account: fills the form for them
locale: 'ar',
serviceWorker: 'auto' // 'auto' | '/your-sw.js' | 'site' | 'off'
};
Уведомления после закрытия сайта#
Пока открыта страница вашего сайта, ответ сам показывается как уведомление. Чтобы достучаться до посетителя после закрытия сайта, браузеры требуют service worker на вашем собственном домене: отдавайте эту одну строку как /sybox-lc-sw.js в корне сайта, с типом application/javascript.
/sybox-lc-sw.js try { importScripts('https://api.sybox.io/lc/sw.js'); } catch (e) {}
Выберите режим в SyBox («Уведомления, когда сайт закрыт») или на странице через serviceWorker: "auto" (файл выше, по умолчанию), собственный путь, "site" (поместите ту же строку в начало своего service worker) или "off".
Чат никогда не заменяет service worker, который уже есть на вашем сайте, а в файле нет обработчика fetch, поэтому он никогда не видит запросы вашего сайта. Плагин WordPress отдаёт этот файл за вас. Safari на iPhone не показывает уведомления для обычного сайта.
Собственное окно чата: ключ сервера#
Сайт или приложение с собственным окном чата обращается к SyBox со своего сервера вместо готового чата. Каждый вызов передаёт Authorization: Bearer lcsk_…. Ваш сервер указывает пользователя по вашему собственному id, и ему можно доверять, потому что у него есть ключ.
| Вызов | Описание |
|---|
POST /v1/livechat/messages | Ваш пользователь пишет сообщение. client_id делает повторный запрос безопасным: сообщение с тем же id сохраняется один раз ("duplicate": 1). |
POST /v1/livechat/upload | Файл в виде multipart form data: file, user_id, client_id. До 16 MB: изображения, голосовые сообщения, видео, PDF, текст и документы Office. |
GET /v1/livechat/messages?user_id=&since=&before=&limit= | Разговор: сообщения после since и agents_read_id — самое новое из сообщений вашего пользователя, прочитанное агентом. |
POST /v1/livechat/read | Ваш пользователь прочитал ответы до up_to. |
POST /v1/livechat/edit | Ваш пользователь редактирует своё сообщение в течение 15 минут. |
POST /v1/livechat/delete | Ваш пользователь удаляет своё сообщение для всех в течение 24 часов. |
POST /v1/livechat/react | Реакция на любое сообщение; пустой emoji её убирает. |
POST /v1/livechat/typing | Ваш пользователь печатает: отправляйте on: 1 каждые несколько секунд и on: 0, когда он перестаёт. |
curl -X POST https://api.sybox.io/v1/livechat/messages \
-H "Authorization: Bearer $SYBOX_LC_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{
"user": { "id": "u42", "name": "Sara Ali", "email": "sara@example.com", "locale": "ar" },
"message": { "client_id": "a1b2c3", "text": "Hi, is my order on its way?", "reply_to": "" }
}'{ "ok": 1, "id": 981, "guid": "7c9e…", "duplicate": 0 }{ "user_id": "u42", "up_to": 990 } // read
{ "user_id": "u42", "id": 981, "text": "…" } // edit
{ "user_id": "u42", "id": 981 } // delete
{ "user_id": "u42", "id": 990, "emoji": "👍" } // react
{ "user_id": "u42", "on": 1 } // typing
Сообщение в истории и в ответах:
{
"id": 990,
"guid": "b81d…",
"dir": "out", // "in" = your user wrote it, "out" = your team
"type": "image", // text | image | audio | video | file
"text": "Here is the tracking page",
"media": { "url": "https://api.sybox.io/…", "mime": "image/png", "name": "tracking.png", "w": 1080, "h": 720 },
// a voice note or a video also has "secs"
"status": 3, // on your user's messages: 1 received, 3 read by an agent
"reply_to": "7c9e…",
"client": "a1b2c3", // on your user's messages: the client_id you sent
"react_us": "", // your team's reaction on it
"react_me": "👍", // your user's reaction on it
"at": "2026-10-25T09:41:07Z",
"edited": 0
}
История пользователя, который ещё ни разу не писал, возвращает 404 unknown_user: покажите пустой разговор.
Ссылки на медиафайлы подписаны и действуют один час; за новой ссылкой обратитесь повторно. SyBox никогда не загружает файл по присланному вами адресу: загрузите его сами. До 120 вызовов в минуту на один сайт.
Ответы на вашем сервере: ключ подписи ответов#
Укажите «Адрес вашего сервера для ответов» в настройках сайта. Когда ваша команда отвечает, редактирует, удаляет или читает сообщения, SyBox отправляет туда POST с событием, подписанный ключом подписи ответов.
| Заголовок | Описание |
|---|
x-sybox-signature | t=TIMESTAMP,v1=SIGNATURE — TIMESTAMP в секундах Unix, SIGNATURE — HMAC-SHA256 в hex от TIMESTAMP.BODY (исходного тела запроса), вычисленный с ключом подписи ответов. |
x-sybox-event | message.created, message.updated, message.deleted или messages.read. |
x-sybox-delivery | Одинаков при каждом повторе одного события: используйте его, чтобы пропускать повторы. |
Тело содержит event, line_id, user_id и message — или message_id при удалении и read_id для messages.read: сообщения вашего пользователя прочитаны до этого id.
const crypto = require('crypto');
app.post('/sybox/livechat', express.raw({ type: 'application/json' }), (req, res) => {
const parts = Object.fromEntries((req.get('x-sybox-signature') || '').split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(401); // older than five minutes
const want = Buffer.from(crypto.createHmac('sha256', process.env.SYBOX_LC_REPLY_KEY)
.update(t + '.').update(req.body).digest('hex'));
const sent = Buffer.from(parts.v1 || '');
if (sent.length !== want.length || !crypto.timingSafeEqual(sent, want)) return res.sendStatus(401);
res.sendStatus(200);
const delivery = req.get('x-sybox-delivery'); // seen it already? ignore
const evt = JSON.parse(req.body); // { event, line_id, user_id, message | message_id | read_id }
});$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_SYBOX_SIGNATURE'] ?? ''), $sig);
$t = (int) ($sig['t'] ?? 0);
$want = hash_hmac('sha256', $t . '.' . $raw, getenv('SYBOX_LC_REPLY_KEY'));
if (abs(time() - $t) > 300 || !hash_equals($want, $sig['v1'] ?? '')) {
http_response_code(401);
exit;
}
http_response_code(200);
$delivery = $_SERVER['HTTP_X_SYBOX_DELIVERY'] ?? ''; // seen it already? ignore
$evt = json_decode($raw, true);import hashlib, hmac, os, time
from flask import abort, request
@app.post("/sybox/livechat")
def sybox_livechat():
raw = request.get_data()
sig = dict(p.split("=", 1) for p in request.headers.get("x-sybox-signature", "").split(",") if "=" in p)
t = int(sig["t"]) if sig.get("t", "").isdigit() else 0 # a malformed header is a 401, not a crash
want = hmac.new(os.environ["SYBOX_LC_REPLY_KEY"].encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
if abs(time.time() - t) > 300 or not hmac.compare_digest(want, sig.get("v1", "")):
abort(401)
delivery = request.headers.get("x-sybox-delivery") # seen it already? ignore
evt = request.get_json()
return "", 200using System.Security.Cryptography;
using System.Text;
app.MapPost("/sybox/livechat", async (HttpRequest req) =>
{
using var ms = new MemoryStream();
await req.Body.CopyToAsync(ms);
byte[] raw = ms.ToArray();
var sig = new Dictionary<string, string>();
foreach (var part in req.Headers["x-sybox-signature"].ToString().Split(','))
{
var kv = part.Split('=', 2);
if (kv.Length == 2) sig[kv[0].Trim()] = kv[1].Trim(); // a malformed header is a 401, not a crash
}
if (!long.TryParse(sig.GetValueOrDefault("t"), out long t) ||
Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return Results.Unauthorized();
byte[] signedBytes = Encoding.ASCII.GetBytes(t + ".").Concat(raw).ToArray();
string replyKey = Environment.GetEnvironmentVariable("SYBOX_LC_REPLY_KEY")!;
byte[] mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(replyKey), signedBytes);
byte[] want = Encoding.ASCII.GetBytes(Convert.ToHexString(mac).ToLowerInvariant());
byte[] sent = Encoding.ASCII.GetBytes(sig.GetValueOrDefault("v1", ""));
if (!CryptographicOperations.FixedTimeEquals(want, sent)) return Results.Unauthorized();
return Results.Ok();
});
Проверяйте подпись по исходному телу, отклоняйте t старше пяти минут и быстро отвечайте кодом 2xx. При любом другом исходе попытка повторяется через 10 с, 30 с, 2 мин, 10 мин и 30 мин, после чего событие отбрасывается. SyBox обращается только к публичным адресам и никогда не следует перенаправлениям.
Коды причин#
Каждый ответ онлайн-чата — это JSON: {"ok": 1, …} или {"ok": 0, "error": "CODE"} с кодом, а не с фразой:
| Код | Статус | Описание |
|---|
unauthorized | 401 | Ключ сервера неверен или заменён. |
invalid_json invalid_payload invalid_user_id | 400 | Запрос имеет не тот формат, который ожидается. |
empty_message no_file empty_file | 400 | Нет ни текста, ни файла. |
too_long | 400 | Отредактированный текст длиннее 4 096 символов. |
not_an_emoji | 400 | emoji не является эмодзи. |
not_found | 404 | Такого сообщения нет, или оно не принадлежит этому пользователю. |
too_late | 409 | Время для редактирования или удаления истекло. |
file_type_not_allowed | 400 | Файлы такого типа не принимаются. |
file_too_large | 413 | Файл больше 16 MB. |
blocked | 403 | Ваша команда заблокировала этого пользователя. |
unknown_user | 404 | Для этого id пользователя пока нет разговора. |
rate_limited | 429 | Слишком много вызовов: снизьте частоту и попробуйте ещё раз. |
maintenance | 503 | SyBox обновляется: попробуйте ещё раз через Retry-After секунд. |
server_error | 500 | Произошёл сбой на нашей стороне. |
Замена ключа#
Потеряли ключ или подозреваете утечку? На карточке сайта нажмите «Новый ключ подписи», «Новый ключ сервера» или «Новый ключ подписи ответов». После создания нового ключа сервера старый продолжает работать ещё сутки, чтобы вы могли переключиться без перерыва; остальные ключи перестают работать сразу.