API de WhatsApp
Envíe su primer mensaje#
Un mensaje de texto plano. El cuerpo es el payload estándar de 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());
La Respuesta#
Recibe exactamente lo que devuelve Meta, con el mismo estado HTTP.
200 OK · application/json {
"messaging_product": "whatsapp",
"contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
"messages": [{ "id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI…" }]
}
Tu sistema en SyBox
Muestre sus datos junto al chat#
Cuando un agente abre una conversación, SyBox llama a SU endpoint y muestra lo que devuelva junto al chat.
Añade tu endpoint en Configuración › Integraciones › APIs de ERP. Cada llamada es un POST con tu clave en Authorization y la solicitud en un solo campo, data, que contiene una cadena JSON; así cualquiera de los dos lados puede añadir campos más adelante sin romper el otro. Responde en menos de 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 llamada indica también quién pregunta: X-Sybox-Lang (el idioma del usuario; responde en él), X-Sybox-Theme (light o dark, para una respuesta html), X-Sybox-Agent (el id del usuario) y X-Sybox-Number (la línea en la que está la conversación).
Enviamos teléfono y correo — responda con { renderAs, content }.
renderAs#
Establezca renderAs para indicar a SyBox cómo mostrar su contenido:
| renderAs | Descripción |
|---|
table | Filas y columnas. content es un array de objetos planos (o { rows: [ … ] }); sus claves pasan a ser los encabezados de las columnas. |
cards · kpi | Pequeños recuadros, como un saldo o un número de pedidos. content es un array de { label, value } (o { cards: [ … ] }). |
feed · list · thread | Una línea de tiempo de tarjetas. content es un array de { title, text, date, tags, media: [{ type, url, name }] }. |
message | Un solo registro presentado como una tarjeta con aspecto de mensaje. |
html | Tu propio HTML, como cadena en content. Solo para el marcado que generas tú mismo. |
sections | Varios bloques en una sola respuesta: content es { sections: [ { title, renderAs, content } ] }. |
json | El valor predeterminado si omites renderAs: SyBox muestra tu content como JSON con formato. |
Añada sybox_ref a cualquier fila de tabla para hacerla interactiva.
Búsqueda de productos (X-Sybox-Action: products)#
El mismo punto de conexión también puede responder a búsquedas de productos. El usuario escribe lo que pide el cliente, SyBox lo envía como "q", y tu respuesta se muestra como tarjetas de producto junto al chat, con un botón de envío en un solo toque que coloca el enlace del producto en el editor.
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 | Descripción |
|---|
nameobligatorio | El único campo obligatorio. Un producto con solo un nombre sigue dando una ficha limpia. |
price · oldPrice · currency | Números, no cadenas con formato. Envíe oldPrice solo cuando exista un precio anterior real — el descuento se calcula a partir de ambos. |
available · stock | available es true/false; stock es un recuento opcional que se muestra al lado. Omítalos si no controla existencias — un campo ausente no dibuja nada y nunca se lee como «agotado». |
image | Una URL de imagen https directa. Lo que no cargue deja un fondo neutro. |
link | La URL de la página del producto. Sin ella la ficha es de solo lectura — Enviar aparece solo cuando hay algo que enviar. |
note | Una línea libre corta bajo el precio — plazo de entrega, variante, condición. |
Nada más que el endpoint que ya construyó: misma URL, misma clave, mismo sobre { "data": "…" }. Use X-Sybox-Action para distinguir una búsqueda de producto de una consulta de cliente. Responda con content como un array (o { items: [ … ] }).
Eventos
Webhooks: eventos en tu servidor#
Añade tu endpoint en Configuración › Integraciones › Webhooks, elige los eventos y los números, y SyBox le envía cada evento por POST en cuanto ocurre. Un evento de WhatsApp llega en el propio formato de webhook de Meta, así que un controlador escrito para Meta funciona sin cambios.
| Evento | Descripción |
|---|
messageMensaje entrante | Un mensaje de un cliente. |
statusEstado del mensaje | Enviado, entregado, leído o fallido, para un mensaje que enviaste. |
reactionReacción | Un cliente reaccionó a un mensaje. |
doneConversación marcada como hecha | Una conversación se marcó como hecha: por una persona, por una automatización o por sí sola tras el silencio del cliente. |
labelEtiqueta añadida o quitada | Se añadió una etiqueta a una conversación o se quitó de ella. |
contactContacto creado o actualizado | Se creó un contacto o cambiaron su nombre, su correo electrónico o sus campos de contacto. |
dealTrato cerrado | Se registró un trato cerrado en una conversación. |
caseCaso CRM abierto o cerrado | Se abrió o se cerró un caso CRM (resuelto, no resuelto o cancelado). |
Solicitud · SyBox → tú 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?" }
}]
}
}]
}]
}
Multimedia. El enlace propio de Meta necesita un token de Meta para abrirse, así que SyBox lo sustituye por un enlace firmado a su propia copia del archivo, válido durante 24 horas. Una nota de voz también puede incluir su texto en sybox.transcription, si el webhook lo solicita.
Otros canales (Messenger, Instagram, Telegram y Chat en vivo) llegan en el formato propio de SyBox, solo para el evento de mensaje:
{
"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" }
}
}
Los eventos de conversación llegan en el formato propio de SyBox para todos los canales. event indica exactamente qué ocurrió:
| Evento | <code>event</code> |
|---|
| Conversación marcada como hecha | conversation.done |
| Etiqueta añadida o quitada | label.added · label.removed |
| Contacto creado o actualizado | contact.created · contact.updated |
| Trato cerrado | deal.closed |
| Caso CRM abierto o cerrado | case.opened · case.closed |
| Evento de prueba | 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" }
}
Clave de firma#
| Encabezado | Descripción |
|---|
X-Hub-Signature-256 | sha256= y el HMAC-SHA256 en hexadecimal del cuerpo sin procesar, con el secreto de firma del webhook como clave: el mismo encabezado y el mismo método que usa Meta. |
x-sybox-signature | El mismo valor, para que un endpoint que también recibe las llamadas de Meta pueda distinguir las nuestras. |
x-sybox-event | Qué ocurrió: message, status, reaction, uno de los eventos de conversación de abajo (conversation.done, label.added…) o test. |
x-sybox-timestamp | Cuándo se envió, en segundos Unix. |
x-sybox-signature-rsa | Una segunda firma: rsa-sha256= y el RSA-SHA256 (PKCS#1 v1.5) en base64 de x-sybox-timestamp, un punto y el cuerpo sin procesar. |
x-sybox-key-id | Cuál de nuestras claves la generó. La clave pública está en https://api.sybox.io/.well-known/sybox-webhook-key. |
Comprueba la firma sobre los bytes sin procesar, antes de analizarlos, y compara en tiempo 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();
});
¿Prefieres no guardar un secreto compartido en tu servidor? Comprueba x-sybox-signature-rsa con nuestra clave pública. En ambos casos, rechaza una marca de tiempo de más de cinco minutos de antigüedad.
Responde con cualquier 2xx en menos de 15 segundos. Cualquier otra cosa (un error, un tiempo de espera agotado, una redirección) se reintenta después de 1, 2, 4, 8, 16 y 32 minutos, y luego se descarta. Un evento puede llegar más de una vez: usa el id del mensaje para ignorar una repetición. La dirección debe ser pública y https.
Un webhook que falla 20 veces seguidas durante más de una hora se desactiva automáticamente y se avisa al administrador del espacio de trabajo. Sus eventos pendientes se conservan durante 24 horas: vuelve a activarlo para enviarlos.
Para probar, pulsa Enviar evento de prueba en la tarjeta del webhook. Entregas, en la misma tarjeta, muestra las últimas 50 entregas con la respuesta de tu servidor y permite enviar una de nuevo.
Chat en vivo
Chat en vivo#
Pon un chat en tu sitio web: los visitantes escriben allí y tu equipo responde en SyBox. Añade el sitio web en Configuración › Mensajería › Canales, en la tarjeta Chat en vivo; la ventana que se abre, Código para tu sitio, te da el código y las claves de abajo. Las tres claves secretas se muestran una sola vez.
| Clave | Empieza por | Para qué sirve |
|---|
| Clave pública | lcpk_ | Va en tus páginas. Puedes publicarla sin riesgo: solo identifica el sitio web, y el chat solo se muestra en tus Direcciones de sitios web. |
| Clave de firma para usuarios con sesión iniciada | lcid_ | Solo en tu servidor. Firma a cada usuario con sesión iniciada, para que nadie pueda escribir haciéndose pasar por otra persona. |
| Clave de servidor (para tu propia pantalla de chat) | lcsk_ | Solo en tu servidor. Solo para un sitio que crea su propia pantalla de chat en lugar de usar el chat listo para usar. |
| Clave de firma de respuestas | lcwh_ | Solo en tu servidor. Demuestra que una respuesta enviada a tu servidor vino de SyBox. |
El chat listo para usar solo necesita el código. La clave de firma es para un sitio con cuentas; la clave de servidor y la clave de firma de respuestas, solo para un sitio con su propia pantalla de chat.
Al desactivar un sitio web, el chat se oculta de inmediato. Las llamadas de tu servidor se siguen aceptando y guardando.
Añade el chat a tu sitio#
Pega el código una vez en cada página, justo antes de la etiqueta de cierre de body. El chat solo se muestra en las direcciones de Direcciones de sitios web, y su color, posición, título y bienvenida se configuran en SyBox, no en el código.
<script async src="https://api.sybox.io/lc/w.js" data-key="lcpk_YOUR_PUBLIC_KEY"></script>
WordPress: descarga nuestro plugin desde la misma ventana. Shopify: en la app de Shopify de SyBox, activa Chat en vivo en esta tienda y luego SyBox Live Chat en App embeds; un cliente con sesión iniciada se reconoce sin que tengas que hacer nada más.
Usuarios con sesión iniciada: la clave de firma#
Cuando un usuario ha iniciado sesión en tu sitio, tu servidor le dice al chat quién es con un token corto (un JWT, HS256) firmado con la clave de firma. Así el usuario tiene una sola conversación a su nombre, en todos sus dispositivos, y nadie puede abrirla escribiendo el id de otra persona.
- Después del inicio de sesión, tu servidor crea el token con los claims de abajo.
- La página se lo pasa al chat como
identity, antes del código del chat. - Tras un inicio de sesión sin recargar la página, llama a
SyBoxChat('identify', token) en su lugar.
| Campo | Descripción |
|---|
subobligatoriostring | El id de tu usuario, de hasta 120 caracteres. No puede empezar por v_. |
expobligatorionumber | Caducidad, en segundos Unix: como máximo un día después. Diez minutos es lo adecuado: el token solo se lee cuando se abre el chat. |
namestring | El nombre que muestra la conversación. |
emailstring | La dirección de correo electrónico del usuario. |
phonestring | El número de teléfono del usuario. |
picturestring | Una dirección https de la foto del usuario, de hasta 500 caracteres. SyBox guarda su propia copia y la vuelve a descargar en el siguiente inicio de sesión cuando la dirección cambia. |
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>
Crea el token solo en tu servidor. Una clave de firma en una página web permite a cualquiera escribir como cualquiera de tus usuarios. Y nunca pongas en la página un id de usuario sin más en lugar del token: eso permitiría a cualquiera elegir qué conversación leer.
Con Solo usuarios con sesión iniciada en mi sitio activado, un visitante sin token no ve el chat.
A un visitante que chateó antes de iniciar sesión se le pregunta una vez, en el mismo navegador, si quiere continuar esa conversación con su cuenta. No tienes que hacer nada para esto.
Controla el chat desde tu página#
El código del chat añade una función a tu 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'
};
Notificaciones después de cerrar tu sitio#
Mientras haya una página de tu sitio abierta, una respuesta se muestra como notificación por sí sola. Para llegar a un visitante después de que cierre tu sitio, los navegadores exigen un service worker en tu propio dominio: sirve esta única línea como /sybox-lc-sw.js en la raíz de tu sitio, como application/javascript.
try { importScripts('https://api.sybox.io/lc/sw.js'); } catch (e) {}
Elige el modo en SyBox (Notificaciones con el sitio web cerrado) o en la página con serviceWorker: "auto" (el archivo de arriba, el valor predeterminado), una ruta propia, "site" (pon la misma línea al principio de tu propio service worker) o "off".
El chat nunca reemplaza un service worker que tu sitio ya tenga, y el archivo no tiene controlador fetch, así que nunca ve ninguna solicitud de tu sitio. El plugin de WordPress sirve el archivo por ti. Safari en iPhone no muestra notificaciones de un sitio web normal.
Tu propia pantalla de chat: la clave de servidor#
Un sitio o una app con su propia pantalla de chat se comunica con SyBox desde su servidor en lugar de usar el chat listo para usar. Cada llamada lleva Authorization: Bearer lcsk_…. Tu servidor identifica al usuario con tu propio id, y SyBox confía en él porque tiene la clave.
| Llamada | Descripción |
|---|
POST /v1/livechat/messages | Tu usuario escribe. client_id hace que un reintento sea seguro: el mismo id se guarda una sola vez ("duplicate": 1). |
POST /v1/livechat/upload | Un archivo, como datos de formulario multipart: file, user_id, client_id. Hasta 16 MB: imágenes, voz, video, PDF, texto y documentos de Office. |
GET /v1/livechat/messages?user_id=&since=&before=&limit= | La conversación: los mensajes posteriores a since, y agents_read_id, el más reciente de los mensajes de tu usuario que ha leído un agente. |
POST /v1/livechat/read | Tu usuario ha leído las respuestas hasta up_to. |
POST /v1/livechat/edit | Tu usuario edita su propio mensaje, en un plazo de 15 minutos. |
POST /v1/livechat/delete | Tu usuario elimina su propio mensaje para todos, en un plazo de 24 horas. |
POST /v1/livechat/react | Una reacción en cualquier mensaje; un emoji vacío la quita. |
POST /v1/livechat/typing | Tu usuario está escribiendo: envía on: 1 cada pocos segundos y on: 0 cuando deje de escribir. |
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
Un mensaje, en el historial y en las respuestas:
{
"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
}
El historial de un usuario que nunca ha escrito responde 404 unknown_user: muestra una conversación vacía.
Los enlaces de multimedia están firmados y funcionan durante una hora; vuelve a pedirlos para obtener uno nuevo. SyBox nunca descarga un archivo desde una dirección que envíes: súbelo. Hasta 120 llamadas por minuto por sitio web.
Respuestas en tu servidor: la clave de firma de respuestas#
Introduce Dirección de tu servidor para respuestas en el sitio web. Cuando tu equipo responde, edita, elimina o lee, SyBox envía el evento por POST a esa dirección, firmado con la clave de firma de respuestas.
| Encabezado | Descripción |
|---|
x-sybox-signature | t=TIMESTAMP,v1=SIGNATURE: TIMESTAMP en segundos Unix y SIGNATURE el HMAC-SHA256 en hexadecimal de TIMESTAMP.BODY (el cuerpo sin procesar), con la clave de firma de respuestas como clave. |
x-sybox-event | message.created, message.updated, message.deleted o messages.read. |
x-sybox-delivery | El mismo en cada reintento de un evento: úsalo para ignorar una repetición. |
El cuerpo contiene event, line_id, user_id y message, o message_id en una eliminación y read_id en messages.read: los mensajes de tu usuario están leídos hasta ese 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();
});
Comprueba la firma sobre el cuerpo sin procesar, rechaza un t de más de cinco minutos de antigüedad y responde con 2xx rápidamente. Cualquier otra cosa se reintenta después de 10 s, 30 s, 2 min, 10 min y 30 min, y luego se descarta. SyBox solo llama a direcciones públicas y nunca sigue una redirección.
Códigos de motivo#
Cada respuesta del Chat en vivo es JSON: {"ok": 1, …}, o {"ok": 0, "error": "CODE"} con un código en lugar de una frase:
| Código | Estado | Descripción |
|---|
unauthorized | 401 | La clave de servidor es incorrecta o se ha reemplazado. |
invalid_json invalid_payload invalid_user_id | 400 | La solicitud no tiene el formato esperado. |
empty_message no_file empty_file | 400 | No hay texto ni archivo. |
too_long | 400 | El texto editado supera los 4096 caracteres. |
not_an_emoji | 400 | emoji no es un emoji. |
not_found | 404 | El mensaje no existe o no es de este usuario. |
too_late | 409 | Ya pasó el plazo para editar o eliminar. |
file_type_not_allowed | 400 | No se acepta este tipo de archivo. |
file_too_large | 413 | El archivo supera los 16 MB. |
blocked | 403 | Tu equipo ha bloqueado a este usuario. |
unknown_user | 404 | Todavía no hay ninguna conversación para este id de usuario. |
rate_limited | 429 | Demasiadas llamadas: ve más despacio e inténtalo de nuevo. |
maintenance | 503 | SyBox se está actualizando: inténtalo de nuevo después de Retry-After segundos. |
server_error | 500 | Algo falló de nuestro lado. |
Reemplazar una clave#
¿Perdiste una clave o crees que se filtró? En la tarjeta del sitio web, haz clic en Nueva clave de firma, Nueva clave de servidor o Nueva clave de firma de respuestas. Una nueva clave de servidor deja la anterior funcionando durante un día, para que puedas hacer el cambio sin interrupciones; las otras claves dejan de funcionar de inmediato.