API WhatsApp
2. Invio Messaggi WhatsApp & Omnicanale#
Invia messaggi di testo, immagini, documenti e modelli approvati Meta tramite le nostre rotte REST.
POST/v26.0/{phone-number-id}/messagesAuthorization: Bearer <DEVICE_KEY>
curl -X POST https://api.smartsybox.com/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.smartsybox.com/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.smartsybox.com/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.smartsybox.com/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.smartsybox.com/v26.0/PHONE_NUMBER_ID/messages",
new StringContent(json, Encoding.UTF8, "application/json"));
Console.WriteLine(await res.Content.ReadAsStringAsync());
Esempio di risposta#
La risposta include l'ID univoco del messaggio e lo stato di consegna in tempo reale.
200 OK · application/json {
"messaging_product": "whatsapp",
"contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
"messages": [{ "id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI…" }]
}
Il tuo sistema in SyBox
4. Integrazione Pannello ERP Integrato nel Chat#
Mostra informazioni di fatturazione, saldi e ordini del cliente direttamente nella barra laterale della conversazione.
Aggiungi il tuo endpoint in Impostazioni › Integrazioni › API ERP. Ogni chiamata è una POST con la tua chiave in Authorization e la richiesta in un solo campo, data, che contiene una stringa JSON — così ciascuna delle due parti può aggiungere campi in seguito senza rompere l'altra. Rispondi entro 15 secondi.
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" }
]
}
Ogni chiamata dice anche chi sta chiedendo: X-Sybox-Lang (la lingua dell'utente — rispondi in quella lingua), X-Sybox-Theme (light o dark, per una risposta html), X-Sybox-Agent (l'id dell'utente) e X-Sybox-Number (la linea su cui si trova la conversazione).
Configura i webhook ERP nelle impostazioni dello spazio di lavoro.
renderAs#
Rendering di schede personalizzate ERP per gli operatori dell'assistenza.
| renderAs | Descrizione |
|---|
table | Righe e colonne. content è un array di oggetti semplici (o { rows: [ … ] }); le loro chiavi diventano le intestazioni delle colonne. |
cards · kpi | Piccoli riquadri, come un saldo o il numero di ordini. content è un array di { label, value } (o { cards: [ … ] }). |
feed · list · thread | Una sequenza cronologica di schede. content è un array di { title, text, date, tags, media: [{ type, url, name }] }. |
message | Un solo record presentato come un'unica scheda in stile messaggio. |
html | Il tuo HTML, come stringa in content. Solo per markup generato da te. |
sections | Più blocchi in una sola risposta: content è { sections: [ { title, renderAs, content } ] }. |
json | L'impostazione predefinita quando renderAs manca: SyBox mostra il tuo content come JSON formattato. |
Riferimenti ai campi dati dinamici importati.
5. Catalogo Prodotti & Schede Articoli ERP#
Lo stesso endpoint può anche rispondere alle ricerche di prodotti. L'utente digita ciò che il cliente sta chiedendo, SyBox lo invia come "q", e la tua risposta viene mostrata come schede prodotto accanto alla chat — con un invio con un tocco che inserisce il link del prodotto nell'editor di testo.
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 | Descrizione |
|---|
nameobbligatorio | Nome Prodotto |
price · oldPrice · currency | Prezzo Unitario |
available · stock | Disponibilità Magazzino |
image | URL Immagine Articolo |
link | Link Scheda Prodotto |
note | Note e specifiche tecniche |
Supporta prezzi con valute multiple e controllo delle giacenze di magazzino.
Eventi
Webhook: gli eventi sul tuo server#
Aggiungi il tuo endpoint in Impostazioni › Integrazioni › Webhook, scegli gli eventi e i numeri, e SyBox invia ogni evento con una POST nel momento in cui accade. Un evento WhatsApp arriva nel formato webhook di Meta, così un handler scritto per Meta funziona senza modifiche.
| Evento | Descrizione |
|---|
messageMessaggio in arrivo | Un messaggio di un cliente. |
statusStato del messaggio | Inviato, consegnato, letto o non riuscito — per un messaggio che hai inviato. |
reactionReazione | Un cliente ha reagito a un messaggio. |
Richiesta · SyBox → tu 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?" }
}]
}
}]
}]
}
Media. Il link di Meta richiede un token Meta per essere aperto, quindi SyBox lo sostituisce con un link firmato alla propria copia del file, valido per 24 ore. Una nota vocale può riportare anche il testo trascritto in sybox.transcription, se il webhook lo richiede.
Altri canali — Messenger, Instagram, Telegram e Live Chat — arrivano nel formato di SyBox, solo per l'evento del messaggio:
{
"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.smartsybox.com/…", "mime": "image/jpeg" }
}
}
Chiave di firma#
| Intestazione | Descrizione |
|---|
X-Hub-Signature-256 | sha256= seguito dall'HMAC-SHA256 esadecimale del corpo grezzo, calcolato con il segreto di firma del webhook — la stessa intestazione e lo stesso metodo di Meta. |
x-sybox-signature | Lo stesso valore, così un endpoint che riceve anche le chiamate di Meta può distinguere le nostre. |
x-sybox-event | message, status o reaction. |
x-sybox-timestamp | Quando è stato inviato, in secondi Unix. |
x-sybox-signature-rsa | Una seconda firma: rsa-sha256= seguito dalla RSA-SHA256 (PKCS#1 v1.5) in base64 di x-sybox-timestamp, un punto e il corpo grezzo. |
x-sybox-key-id | Quale delle nostre chiavi l'ha creata. La chiave pubblica si trova su https://api.smartsybox.com/.well-known/sybox-webhook-key. |
Verifica la firma sui byte grezzi, prima di analizzarli, e confrontala in tempo costante:
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();
});
Preferisci non tenere un segreto condiviso sul tuo server? Verifica invece x-sybox-signature-rsa con la nostra chiave pubblica. In entrambi i casi, rifiuta un timestamp più vecchio di cinque minuti.
Rispondi con un qualsiasi 2xx entro 15 secondi. Qualsiasi altra risposta — un errore, un timeout, un reindirizzamento — viene ritentata dopo 1, 2, 4, 8, 16 e 32 minuti, poi scartata. Un evento può arrivare più di una volta: usa l'id del messaggio per ignorare un duplicato. L'indirizzo deve essere pubblico e in https.
Per fare una prova, invia un messaggio al tuo numero e guarda Ultimo invio sulla scheda del webhook: mostra la risposta del tuo server.
Live Chat
Live Chat#
Aggiungi una chat al tuo sito web: i visitatori scrivono lì, il tuo team risponde in SyBox. Aggiungi il sito web in Impostazioni › Messaggistica › Canali, nella scheda Live Chat; la finestra che si apre, Codice per il tuo sito, ti fornisce il codice e le chiavi qui sotto. Le tre chiavi segrete vengono mostrate una sola volta.
| Chiave | Inizia con | A cosa serve |
|---|
| Chiave pubblica | lcpk_ | Va nelle tue pagine. Si può pubblicare senza rischi: identifica solo il sito web, e la chat compare solo sui tuoi Indirizzi del sito web. |
| Chiave di firma per gli utenti connessi | lcid_ | Solo sul tuo server. Firma ogni utente connesso, così nessuno può scrivere al posto di un altro. |
| Chiave del server (per la tua schermata di chat) | lcsk_ | Solo sul tuo server. Solo per un sito che crea la propria schermata di chat invece di usare la chat pronta. |
| Chiave di firma delle risposte | lcwh_ | Solo sul tuo server. Dimostra che una risposta inviata al tuo server proviene da SyBox. |
La chat pronta richiede solo il codice. La chiave di firma serve a un sito con account utente; la chiave del server e la chiave di firma delle risposte solo a un sito con la propria schermata di chat.
Disattivare un sito web nasconde subito la chat. Le chiamate del tuo server vengono comunque accettate e salvate.
Aggiungi la chat al tuo sito#
Incolla il codice una volta in ogni pagina, subito prima del tag di chiusura body. La chat compare solo sugli indirizzi in Indirizzi del sito web, e colore, posizione, titolo e messaggio di benvenuto si impostano in SyBox, non nel codice.
<script async src="https://api.smartsybox.com/lc/w.js" data-key="lcpk_YOUR_PUBLIC_KEY"></script>
WordPress: scarica il nostro plugin dalla stessa finestra. Shopify: nell'app Shopify di SyBox attiva Live Chat su questo negozio, poi SyBox Live Chat in Incorporamenti app — un cliente connesso viene riconosciuto senza fare altro.
Utenti connessi: la chiave di firma#
Quando un utente ha effettuato l'accesso al tuo sito, il tuo server comunica alla chat chi è con un breve token — un JWT, HS256 — firmato con la chiave di firma. L'utente ha così un'unica conversazione a suo nome, su ogni dispositivo, e nessuno può aprirla digitando l'id di qualcun altro.
- Dopo l'accesso, il tuo server crea il token con i claim qui sotto.
- La pagina lo passa alla chat come
identity, prima del codice della chat. - Dopo un accesso senza ricaricare la pagina, chiama invece
SyBoxChat('identify', token).
| Campo | Descrizione |
|---|
subobbligatoriostring | L'id del tuo utente, fino a 120 caratteri. Non può iniziare con v_. |
expobbligatorionumber | Scadenza, in secondi Unix — non oltre un giorno da adesso. Dieci minuti vanno bene: il token viene letto solo quando la chat si apre. |
namestring | Il nome mostrato dalla conversazione. |
emailstring | L'indirizzo email dell'utente. |
phonestring | Il numero di telefono dell'utente. |
picturestring | Un indirizzo https della foto dell'utente, fino a 500 caratteri. SyBox ne conserva una propria copia e la scarica di nuovo al successivo accesso se l'indirizzo 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.smartsybox.com/lc/w.js" data-key="lcpk_YOUR_PUBLIC_KEY"></script>
Crea il token solo sul tuo server. Una chiave di firma in una pagina web permette a chiunque di scrivere come uno qualsiasi dei tuoi utenti. E non mettere mai nella pagina un semplice id utente al posto del token: chiunque potrebbe scegliere quale conversazione leggere.
Con Solo gli utenti connessi al mio sito attivo, un visitatore senza token non vede la chat.
A un visitatore che ha scritto in chat prima di accedere viene chiesto una sola volta, nello stesso browser, se vuole continuare quella conversazione con il suo account. Non devi fare nulla.
Controlla la chat dalla tua pagina#
Il codice della chat aggiunge una funzione alla tua pagina, 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'
};
Notifiche dopo la chiusura del sito#
Mentre una pagina del tuo sito è aperta, una risposta compare da sola come notifica. Per raggiungere un visitatore dopo che ha chiuso il tuo sito, i browser richiedono un service worker sul tuo dominio: servi questa sola riga come /sybox-lc-sw.js nella radice del sito, come application/javascript.
try { importScripts('https://api.smartsybox.com/lc/sw.js'); } catch (e) {}
Scegli la modalità in SyBox (Notifiche a sito chiuso) o nella pagina con serviceWorker: "auto" (il file qui sopra, l'impostazione predefinita), un tuo percorso, "site" (metti la stessa riga all'inizio del tuo service worker) o "off".
La chat non sostituisce mai un service worker già presente sul tuo sito, e il file non ha un gestore fetch, quindi non vede mai le richieste del tuo sito. Il plugin WordPress serve il file per te. Safari su iPhone non mostra notifiche per un normale sito web.
La tua schermata di chat: la chiave del server#
Un sito o un'app con la propria schermata di chat comunica con SyBox dal suo server invece di usare la chat pronta. Ogni chiamata porta Authorization: Bearer lcsk_…. Il tuo server identifica l'utente con il tuo id, e SyBox se ne fida perché il server possiede la chiave.
| Chiamata | Descrizione |
|---|
POST /v1/livechat/messages | Il tuo utente scrive. client_id rende sicuro un nuovo tentativo: lo stesso id viene salvato una sola volta ("duplicate": 1). |
POST /v1/livechat/upload | Un file, come dati di un modulo multipart: file, user_id, client_id. Fino a 16 MB: immagini, audio, video, PDF, testo e documenti Office. |
GET /v1/livechat/messages?user_id=&since=&before=&limit= | La conversazione: i messaggi dopo since e agents_read_id, il più recente dei messaggi del tuo utente letto da un agente. |
POST /v1/livechat/read | Il tuo utente ha letto le risposte fino a up_to. |
POST /v1/livechat/edit | Il tuo utente modifica un proprio messaggio, entro 15 minuti. |
POST /v1/livechat/delete | Il tuo utente elimina un proprio messaggio per tutti, entro 24 ore. |
POST /v1/livechat/react | Una reazione a qualsiasi messaggio; un emoji vuoto la rimuove. |
POST /v1/livechat/typing | Il tuo utente sta scrivendo: invia on: 1 ogni pochi secondi e on: 0 quando smette. |
curl -X POST https://api.smartsybox.com/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 messaggio, nella cronologia e nelle risposte:
{
"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.smartsybox.com/…", "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
}
La cronologia di un utente che non ha mai scritto risponde 404 unknown_user: mostra una conversazione vuota.
I link ai media sono firmati e funzionano per un'ora; richiedine uno nuovo quando scade. SyBox non scarica mai un file da un indirizzo che invii: caricalo. Fino a 120 chiamate al minuto per sito web.
Risposte sul tuo server: la chiave di firma delle risposte#
Inserisci Indirizzo del tuo server per le risposte nel sito web. Quando il tuo team risponde, modifica, elimina o legge, SyBox invia lì l'evento con una POST, firmato con la chiave di firma delle risposte.
| Intestazione | Descrizione |
|---|
x-sybox-signature | t=TIMESTAMP,v1=SIGNATURE — TIMESTAMP in secondi Unix, SIGNATURE l'HMAC-SHA256 esadecimale di TIMESTAMP.BODY (il body grezzo), calcolato con la chiave di firma delle risposte. |
x-sybox-event | message.created, message.updated, message.deleted o messages.read. |
x-sybox-delivery | Lo stesso a ogni nuovo tentativo di un evento: usalo per ignorare un duplicato. |
Il corpo contiene event, line_id, user_id e message — oppure message_id per un'eliminazione e read_id per messages.read: i messaggi del tuo utente sono stati letti fino a quell'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();
});
Verifica la firma sul corpo grezzo, rifiuta un t più vecchio di cinque minuti e rispondi subito con 2xx. Qualsiasi altra risposta viene ritentata dopo 10 s, 30 s, 2 min, 10 min e 30 min, poi scartata. SyBox chiama solo indirizzi pubblici e non segue mai un reindirizzamento.
Codici di motivo#
Ogni risposta della Live Chat è JSON — {"ok": 1, …}, oppure {"ok": 0, "error": "CODE"} con un codice invece di una frase:
| Codice | Stato | Descrizione |
|---|
unauthorized | 401 | La chiave del server è errata o è stata sostituita. |
invalid_json invalid_payload invalid_user_id | 400 | La richiesta non ha la struttura prevista. |
empty_message no_file empty_file | 400 | Nessun testo e nessun file. |
too_long | 400 | Il testo modificato supera i 4.096 caratteri. |
not_an_emoji | 400 | emoji non è un'emoji. |
not_found | 404 | Il messaggio non esiste, oppure non appartiene a questo utente. |
too_late | 409 | Il tempo per modificare o eliminare è scaduto. |
file_type_not_allowed | 400 | Questo tipo di file non è accettato. |
file_too_large | 413 | Il file supera i 16 MB. |
blocked | 403 | Il tuo team ha bloccato questo utente. |
unknown_user | 404 | Ancora nessuna conversazione per questo id utente. |
rate_limited | 429 | Troppe chiamate: rallenta e riprova. |
maintenance | 503 | SyBox si sta aggiornando: riprova dopo Retry-After secondi. |
server_error | 500 | Qualcosa non ha funzionato da parte nostra. |
Sostituire una chiave#
Hai perso una chiave o pensi che sia trapelata? Nella scheda del sito web, fai clic su Nuova chiave di firma, Nuova chiave del server o Nuova chiave di firma delle risposte. Una nuova chiave del server lascia funzionare quella vecchia per un giorno, così puoi passare alla nuova senza interruzioni; le altre chiavi smettono subito di funzionare.