API do WhatsApp
Envie sua primeira mensagem#
Uma mensagem em texto simples. O corpo é o payload padrão da 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());
A Resposta#
Você recebe de volta exatamente o que a Meta retorna, com o mesmo status HTTP.
200 OK · application/json {
"messaging_product": "whatsapp",
"contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
"messages": [{ "id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI…" }]
}
Seu sistema no SyBox
Mostre seus dados ao lado do chat#
Quando um agente abre uma conversa, o SyBox chama SEU endpoint e exibe o que você retornar ao lado do chat.
Adicione o seu endpoint em Configurações › Integrações › APIs de ERP. Toda chamada é um POST com a sua chave em Authorization e a requisição em um único campo, data, que contém uma string JSON — assim qualquer um dos lados pode adicionar campos depois sem quebrar o outro. Responda em até 15 segundos.
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" }
]
}
Cada chamada também informa quem está perguntando: X-Sybox-Lang (o idioma do usuário — responda nele), X-Sybox-Theme (light ou dark, para uma resposta html), X-Sybox-Agent (o id do usuário) e X-Sybox-Number (a linha em que a conversa está).
Enviamos telefone e e-mail — responda com { renderAs, content }.
renderAs#
Defina renderAs para informar ao SyBox como exibir seu conteúdo:
| renderAs | Descrição |
|---|
table | Linhas e colunas. content é um array de objetos simples (ou { rows: [ … ] }); as chaves deles viram os cabeçalhos das colunas. |
cards · kpi | Blocos pequenos, como um saldo ou uma contagem de pedidos. content é um array de { label, value } (ou { cards: [ … ] }). |
feed · list · thread | Uma linha do tempo de cartões. content é um array de { title, text, date, tags, media: [{ type, url, name }] }. |
message | Um registro exibido como um único cartão no estilo de mensagem. |
html | O seu próprio HTML, como uma string em content. Apenas para marcação que você mesmo gera. |
sections | Vários blocos em uma só resposta: content é { sections: [ { title, renderAs, content } ] }. |
json | O padrão quando renderAs é omitido: o SyBox mostra o seu content como JSON formatado. |
Adicione sybox_ref a qualquer linha de tabela para torná-la clicável.
Busca de produtos (X-Sybox-Action: products)#
O mesmo endpoint também pode responder a buscas de produtos. O usuário digita o que o cliente está procurando, o SyBox envia como "q", e sua resposta é exibida como cartões de produto ao lado da conversa — com um botão Enviar de um toque que insere o link do produto no campo de mensagem.
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"
}
]
}
| Campo | Descrição |
|---|
nameobrigatório | O único campo obrigatório. Um produto só com nome ainda vira um cartão limpo. |
price · oldPrice · currency | Números, não textos formatados. Envie oldPrice apenas quando houver um preço anterior real — o desconto é calculado a partir dos dois. |
available · stock | available é true/false; stock é uma contagem opcional exibida ao lado. Omita ambos se você não controla estoque — um campo ausente não desenha nada e nunca é lido como esgotado. |
image | Uma URL de imagem https direta. Imagens que falham dão lugar a um fundo neutro. |
link | A URL da página do produto. Sem ela o cartão é somente leitura — Enviar só aparece quando há algo a enviar. |
note | Uma linha livre curta sob o preço — prazo de entrega, variante, condição. |
Nada além do endpoint que você já construiu: mesma URL, mesma chave, mesmo envelope { "data": "…" }. Use X-Sybox-Action para distinguir uma busca de produto de uma consulta de cliente. Responda com content como um array (ou { items: [ … ] }).
Eventos
Webhooks: eventos no seu servidor#
Adicione o seu endpoint em Configurações › Integrações › Webhooks, escolha os eventos e os números, e o SyBox envia cada evento para ele via POST assim que acontece. Um evento do WhatsApp chega no próprio formato de webhook da Meta, então um handler escrito para a Meta funciona sem mudanças.
| Evento | Descrição |
|---|
messageMensagem recebida | Uma mensagem de um cliente. |
statusEstado da mensagem | Enviada, entregue, lida ou com falha — para uma mensagem que você enviou. |
reactionReação | Um cliente reagiu a uma mensagem. |
doneConversa marcada como concluída | Uma conversa foi marcada como concluída: por uma pessoa, por uma automação ou automaticamente após o silêncio do cliente. |
labelEtiqueta adicionada ou removida | Uma etiqueta foi adicionada a uma conversa ou removida dela. |
contactContato criado ou atualizado | Um contato foi criado ou seu nome, e-mail ou campos de contato foram alterados. |
dealNegócio fechado | Um negócio fechado foi registrado em uma conversa. |
caseCaso de CRM aberto ou fechado | Um caso de CRM foi aberto ou fechado (resolvido, não resolvido ou cancelado). |
Requisição · SyBox → você 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?" }
}]
}
}]
}]
}
Mídia. O link da própria Meta precisa de um token da Meta para abrir, então o SyBox o substitui por um link assinado para a sua própria cópia do arquivo, válido por 24 horas. Uma mensagem de voz também pode trazer as palavras em sybox.transcription, quando o webhook as solicita.
Outros canais — Messenger, Instagram, Telegram e Chat ao vivo — chegam no formato próprio do SyBox, apenas para o evento de mensagem:
{
"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" }
}
}
Os eventos de conversa chegam no formato próprio do SyBox, para todos os canais. event informa exatamente o que aconteceu:
| Evento | <code>event</code> |
|---|
| Conversa marcada como concluída | conversation.done |
| Etiqueta adicionada ou removida | label.added · label.removed |
| Contato criado ou atualizado | contact.created · contact.updated |
| Negócio fechado | deal.closed |
| Caso de CRM aberto ou fechado | case.opened · case.closed |
| Evento de teste | 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" }
}
Chave de assinatura#
| Cabeçalho | Descrição |
|---|
X-Hub-Signature-256 | sha256= e o HMAC-SHA256 em hexadecimal do corpo bruto, com o segredo de assinatura do webhook como chave — o mesmo cabeçalho e o mesmo método da Meta. |
x-sybox-signature | O mesmo valor, para que um endpoint que também recebe as chamadas da própria Meta consiga distinguir as nossas. |
x-sybox-event | O que aconteceu: message, status, reaction, um dos eventos de conversa abaixo (conversation.done, label.added…) ou test. |
x-sybox-timestamp | Quando foi enviado, em segundos Unix. |
x-sybox-signature-rsa | Uma segunda assinatura: rsa-sha256= e o RSA-SHA256 (PKCS#1 v1.5) em base64 de x-sybox-timestamp, um ponto e o corpo bruto. |
x-sybox-key-id | Qual das nossas chaves a gerou. A chave pública está em https://api.sybox.io/.well-known/sybox-webhook-key. |
Verifique a assinatura nos bytes brutos, antes de interpretá-los, e compare em tempo constante:
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();
});
Prefere não guardar um segredo compartilhado no seu servidor? Verifique x-sybox-signature-rsa com a nossa chave pública. Em qualquer caso, recuse um timestamp com mais de cinco minutos.
Responda com qualquer 2xx em até 15 segundos. Qualquer outra coisa — um erro, um tempo esgotado, um redirecionamento — é tentada novamente após 1, 2, 4, 8, 16 e 32 minutos e depois descartada. Um evento pode chegar mais de uma vez: use o id da mensagem para ignorar uma repetição. O endereço precisa ser público e https.
Um webhook que falha 20 vezes seguidas, por mais de uma hora, é desativado automaticamente e o administrador do espaço de trabalho é avisado. Os eventos pendentes são mantidos por 24 horas: ative-o novamente para enviá-los.
Para testar, pressione Enviar evento de teste no cartão do webhook. Entregas, no mesmo cartão, lista as últimas 50 entregas com a resposta do seu servidor e permite enviar uma novamente.
Chat ao vivo
Chat ao vivo#
Coloque um chat no seu site: os visitantes escrevem lá, e a sua equipe responde no SyBox. Adicione o site em Configurações › Mensagens › Canais, no cartão Chat ao vivo; a janela que se abre, Código para o seu site, mostra o código e as chaves abaixo. As três chaves secretas são exibidas apenas uma vez.
| Chave | Começa com | Para que serve |
|---|
| Chave pública | lcpk_ | Vai nas suas páginas. Pode ser publicada com segurança: ela apenas identifica o site, e o chat só aparece nos seus Endereços do site. |
| Chave de assinatura para usuários conectados | lcid_ | Somente no seu servidor. Assina cada usuário conectado, para que ninguém possa escrever como outra pessoa. |
| Chave do servidor (para sua própria tela de chat) | lcsk_ | Somente no seu servidor. Apenas para um site que cria a própria tela de chat em vez de usar o chat pronto. |
| Chave de assinatura de respostas | lcwh_ | Somente no seu servidor. Comprova que uma resposta enviada ao seu servidor veio do SyBox. |
O chat pronto precisa apenas do código. A chave de assinatura é para um site com contas de usuário; a chave do servidor e a chave de assinatura de respostas, apenas para um site com a própria tela de chat.
Desativar um site oculta o chat na hora. As chamadas do seu servidor continuam sendo aceitas e armazenadas.
Adicione o chat ao seu site#
Cole o código uma vez em todas as páginas, logo antes da tag de fechamento do body. O chat só aparece nos endereços em Endereços do site, e a cor, a posição, o título e a mensagem de boas-vindas são definidos no SyBox, não no código.
<script async src="https://api.sybox.io/lc/w.js" data-key="lcpk_YOUR_PUBLIC_KEY"></script>
WordPress: baixe o nosso plugin na mesma janela. Shopify: no app Shopify do SyBox, ative Chat ao vivo nesta loja e depois SyBox Live Chat em Incorporações de app (App embeds) — um cliente conectado é reconhecido sem que você precise fazer mais nada.
Usuários conectados: a chave de assinatura#
Quando um usuário está conectado ao seu site, o seu servidor informa ao chat quem ele é com um token curto — um JWT, HS256 — assinado com a chave de assinatura. Assim o usuário tem uma única conversa em seu nome, em todos os dispositivos, e ninguém consegue abri-la digitando o id de outra pessoa.
- Depois do login, o seu servidor monta o token com as claims abaixo.
- A página o passa para o chat como
identity, antes do código do chat. - Depois de um login sem recarregar a página, chame
SyBoxChat('identify', token).
| Campo | Descrição |
|---|
subobrigatóriostring | O id do seu usuário, com até 120 caracteres. Ele não pode começar com v_. |
expobrigatórionumber | Validade, em segundos Unix — no máximo um dia à frente. Dez minutos é o ideal: o token só é lido quando o chat abre. |
namestring | O nome exibido na conversa. |
emailstring | O endereço de e-mail do usuário. |
phonestring | O número de telefone do usuário. |
picturestring | Um endereço https da foto do usuário, com até 500 caracteres. O SyBox guarda a própria cópia e a busca novamente no próximo login quando o endereço muda. |
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>
Gere o token somente no seu servidor. Uma chave de assinatura em uma página da web permite que qualquer pessoa escreva como qualquer um dos seus usuários. E nunca coloque um id de usuário puro na página no lugar do token: isso permitiria que qualquer pessoa escolhesse de quem é a conversa que vai ler.
Com Apenas usuários conectados ao meu site ativado, um visitante sem token não vê o chat.
Um visitante que conversou antes de fazer login recebe uma única pergunta, no mesmo navegador, se quer continuar aquela conversa com a conta dele. Você não precisa fazer nada para isso.
Controle o chat a partir da sua página#
O código do chat adiciona uma função à sua página, 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'
};
Notificações depois que o seu site é fechado#
Enquanto uma página do seu site está aberta, uma resposta aparece como notificação automaticamente. Para alcançar um visitante depois que o seu site é fechado, os navegadores exigem um service worker no seu próprio domínio: sirva esta única linha como /sybox-lc-sw.js na raiz do seu site, como application/javascript.
try { importScripts('https://api.sybox.io/lc/sw.js'); } catch (e) {}
Escolha o modo no SyBox (Notificações com o site fechado) ou na página com serviceWorker: "auto" (o arquivo acima, o padrão), um caminho próprio, "site" (coloque a mesma linha no início do seu próprio service worker) ou "off".
O chat nunca substitui um service worker que o seu site já tenha, e o arquivo não tem handler de fetch, então nunca vê uma requisição do seu site. O plugin do WordPress serve o arquivo para você. O Safari do iPhone não mostra notificações de um site comum.
A sua própria tela de chat: a chave do servidor#
Um site ou app com a própria tela de chat se comunica com o SyBox a partir do seu servidor, em vez de usar o chat pronto. Toda chamada leva Authorization: Bearer lcsk_…. O seu servidor identifica o usuário pelo seu próprio id, e é confiável para isso, porque guarda a chave.
| Chamada | Descrição |
|---|
POST /v1/livechat/messages | O seu usuário escreve. client_id torna uma nova tentativa segura: o mesmo id é guardado uma única vez ("duplicate": 1). |
POST /v1/livechat/upload | Um arquivo, como dados de formulário multipart: file, user_id, client_id. Até 16 MB: imagens, voz, vídeo, PDF, texto e documentos do Office. |
GET /v1/livechat/messages?user_id=&since=&before=&limit= | A conversa: as mensagens depois de since, e agents_read_id, a mais recente das mensagens do seu usuário que um agente leu. |
POST /v1/livechat/read | O seu usuário leu as respostas até up_to. |
POST /v1/livechat/edit | O seu usuário edita a própria mensagem, em até 15 minutos. |
POST /v1/livechat/delete | O seu usuário apaga a própria mensagem para todos, em até 24 horas. |
POST /v1/livechat/react | Uma reação em qualquer mensagem; um emoji vazio a remove. |
POST /v1/livechat/typing | O seu usuário está digitando: envie on: 1 a cada poucos segundos e on: 0 quando ele parar. |
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
Uma mensagem, no histórico e nas respostas:
{
"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
}
O histórico de um usuário que nunca escreveu responde 404 unknown_user: mostre uma conversa vazia.
Os links de mídia são assinados e funcionam por uma hora; peça novamente para obter um novo. O SyBox nunca busca um arquivo a partir de um endereço que você envia: faça o upload dele. Até 120 chamadas por minuto por site.
Respostas no seu servidor: a chave de assinatura de respostas#
Informe o Endereço do seu servidor para respostas no site. Quando a sua equipe responde, edita, apaga ou lê, o SyBox envia o evento para lá via POST, assinado com a chave de assinatura de respostas.
| Cabeçalho | Descrição |
|---|
x-sybox-signature | t=TIMESTAMP,v1=SIGNATURE — TIMESTAMP em segundos Unix, SIGNATURE o HMAC-SHA256 em hexadecimal de TIMESTAMP.BODY (o body bruto), com a chave de assinatura de respostas como chave. |
x-sybox-event | message.created, message.updated, message.deleted ou messages.read. |
x-sybox-delivery | O mesmo em todas as novas tentativas de um evento: use-o para ignorar uma repetição. |
O corpo contém event, line_id, user_id e message — ou message_id para uma exclusão, e read_id para messages.read: as mensagens do seu usuário foram lidas até esse 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();
});
Verifique a assinatura no corpo bruto, recuse um t com mais de cinco minutos e responda com 2xx rapidamente. Qualquer outra coisa é tentada novamente após 10 s, 30 s, 2 min, 10 min e 30 min e depois descartada. O SyBox chama apenas endereços públicos e nunca segue um redirecionamento.
Códigos de motivo#
Toda resposta do Chat ao vivo é JSON — {"ok": 1, …}, ou {"ok": 0, "error": "CODE"} com um código em vez de uma frase:
| Código | Status | Descrição |
|---|
unauthorized | 401 | A chave do servidor está errada ou foi substituída. |
invalid_json invalid_payload invalid_user_id | 400 | A requisição não está no formato esperado. |
empty_message no_file empty_file | 400 | Nenhum texto e nenhum arquivo. |
too_long | 400 | O texto editado passa de 4.096 caracteres. |
not_an_emoji | 400 | emoji não é um emoji. |
not_found | 404 | Essa mensagem não existe, ou não é deste usuário. |
too_late | 409 | O prazo para editar ou excluir já passou. |
file_type_not_allowed | 400 | Este tipo de arquivo não é aceito. |
file_too_large | 413 | O arquivo tem mais de 16 MB. |
blocked | 403 | A sua equipe bloqueou este usuário. |
unknown_user | 404 | Ainda não há conversa para este id de usuário. |
rate_limited | 429 | Chamadas demais: diminua o ritmo e tente novamente. |
maintenance | 503 | O SyBox está sendo atualizado: tente novamente após Retry-After segundos. |
server_error | 500 | Algo falhou do nosso lado. |
Substituir uma chave#
Perdeu uma chave ou acha que ela vazou? No cartão do site, clique em Nova chave de assinatura, Nova chave do servidor ou Nova chave de assinatura de respostas. Uma nova chave do servidor mantém a antiga funcionando por um dia, para que você faça a troca sem interrupção; as outras chaves param na hora.