API WhatsApp
Envoyez votre premier message#
Un message texte simple. Le corps est le charge utile standard de l'API Cloud — nous le transmettons tel quel.
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 réponse#
Vous recevez exactement ce que Meta renvoie, avec le même statut HTTP.
200 OK · application/json {
"messaging_product": "whatsapp",
"contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
"messages": [{ "id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI…" }]
}
Votre système dans SyBox
Affichez vos données à côté de la conversation#
À l'ouverture d'une conversation, SyBox appelle VOTRE point de terminaison et affiche votre réponse juste à côté du chat — commandes, solde, tickets du client. Nous envoyons le téléphone du client, ou son e-mail pour une conversation par e-mail.
Ajoutez votre point de terminaison dans Paramètres › Intégrations › API ERP. Chaque appel est un POST avec votre clé dans Authorization et la requête dans un seul champ, data, qui contient une chaîne JSON — ainsi, chaque côté peut ajouter des champs plus tard sans casser l'autre. Répondez en moins de 15 secondes.
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" }
]
}
Chaque appel indique aussi qui fait la demande : X-Sybox-Lang (la langue de l'utilisateur — répondez dans cette langue), X-Sybox-Theme (light ou dark, pour une réponse html), X-Sybox-Agent (l'identifiant de l'utilisateur) et X-Sybox-Number (la ligne sur laquelle se trouve la conversation).
Nous envoyons le téléphone et l'e-mail — l'un peut être vide ; faites la correspondance avec celui qui identifie le client dans votre système, et répondez avec { renderAs, content }.
renderAs#
Définissez renderAs pour indiquer à SyBox comment afficher vos données — l'une de :
| renderAs | Description |
|---|
table | Des lignes et des colonnes. content est un tableau d'objets plats (ou { rows: [ … ] }) ; leurs clés deviennent les en-têtes des colonnes. |
cards · kpi | De petites tuiles, comme un solde ou un nombre de commandes. content est un tableau de { label, value } (ou { cards: [ … ] }). |
feed · list · thread | Une chronologie de cartes. content est un tableau de { title, text, date, tags, media: [{ type, url, name }] }. |
message | Un enregistrement présenté sous la forme d'une seule carte, à la manière d'un message. |
html | Votre propre HTML, sous forme de chaîne dans content. Uniquement pour un balisage que vous générez vous-même. |
sections | Plusieurs blocs dans une seule réponse : content est { sections: [ { title, renderAs, content } ] }. |
json | La valeur par défaut lorsque renderAs est omis : SyBox affiche votre content sous forme de JSON mis en forme. |
Ajoutez un sybox_ref à une ligne de tableau ou une carte pour la rendre cliquable — SyBox vous rappelle avec ce ref afin que vous renvoyiez le détail de cet enregistrement.
Recherche de produits (X-Sybox-Action: products)#
Le même point de terminaison peut également répondre aux recherches de produits. L'utilisateur saisit ce que demande le client, SyBox l'envoie en tant que « q », et votre réponse s'affiche sous forme de fiches produits à côté du chat — avec un bouton d'envoi en un clic qui insère le lien du produit dans la zone de saisie.
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"
}
]
}
| Champ | Description |
|---|
nameobligatoire | Le seul champ obligatoire. Un produit réduit à son nom donne quand même une fiche propre. |
price · oldPrice · currency | Des nombres, pas des chaînes formatées. N'envoyez oldPrice que s'il existe un vrai prix précédent — la remise est calculée à partir des deux. |
available · stock | available vaut true/false ; stock est un compteur facultatif affiché à côté. Omettez-les si vous ne suivez pas le stock — un champ absent n'affiche rien et n'est jamais lu comme « rupture ». |
image | Une URL d'image https directe. Une image qui ne charge pas laisse place à un fond neutre. |
link | L'URL de la fiche produit. Sans elle, la carte est en lecture seule — Envoyer n'apparaît que s'il y a quelque chose à envoyer. |
note | Une courte ligne libre sous le prix — délai de livraison, variante, condition. |
Rien de plus que l'endpoint déjà en place : même URL, même clé, même enveloppe { "data": "…" }. Lisez X-Sybox-Action pour distinguer une recherche produit d'une recherche client. Répondez avec content sous forme de tableau (ou { items: [ … ] }).
Événements
Webhooks : les événements sur votre serveur#
Ajoutez votre point de terminaison dans Paramètres › Intégrations › Webhooks, choisissez les événements et les numéros, et SyBox y envoie chaque événement en POST au moment où il se produit. Un événement WhatsApp arrive au format webhook de Meta : un gestionnaire écrit pour Meta fonctionne sans modification.
| Événement | Description |
|---|
messageMessage entrant | Un message d'un client. |
statusStatut du message | Envoyé, distribué, lu ou échoué — pour un message que vous avez envoyé. |
reactionRéaction | Un client a réagi à un message. |
doneConversation marquée comme terminée | Une conversation a été marquée comme terminée : par une personne, par une automatisation ou automatiquement après le silence du client. |
labelLibellé ajouté ou retiré | Un libellé a été ajouté à une conversation ou retiré de celle-ci. |
contactContact créé ou mis à jour | Un contact a été créé, ou son nom, son e-mail ou ses champs de contact ont changé. |
dealAffaire conclue | Une affaire conclue a été enregistrée dans une conversation. |
caseDossier CRM ouvert ou fermé | Un dossier CRM a été ouvert ou fermé (résolu, non résolu ou annulé). |
Requête · SyBox → vous 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édias. Le lien propre à Meta nécessite un jeton Meta pour s'ouvrir : SyBox le remplace donc par un lien signé vers sa propre copie du fichier, valable 24 heures. Un message vocal peut aussi contenir sa transcription dans sybox.transcription, lorsque le webhook la demande.
Les autres canaux — Messenger, Instagram, Telegram et Chat en direct — arrivent au format propre à SyBox, uniquement pour l'événement message :
{
"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" }
}
}
Les événements de conversation arrivent au format propre à SyBox, pour chaque canal. event indique exactement ce qui s'est passé :
| Événement | <code>event</code> |
|---|
| Conversation marquée comme terminée | conversation.done |
| Libellé ajouté ou retiré | label.added · label.removed |
| Contact créé ou mis à jour | contact.created · contact.updated |
| Affaire conclue | deal.closed |
| Dossier CRM ouvert ou fermé | case.opened · case.closed |
| Événement de test | 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" }
}
Clé de signature#
| En-tête | Description |
|---|
X-Hub-Signature-256 | sha256= suivi du HMAC-SHA256 en hexadécimal du corps brut, calculé avec le secret de signature du webhook — le même en-tête et la même méthode que chez Meta. |
x-sybox-signature | La même valeur, pour qu'un point de terminaison qui reçoit aussi les appels de Meta puisse distinguer les nôtres. |
x-sybox-event | Ce qui s'est passé : message, status, reaction, l'un des événements de conversation ci-dessous (conversation.done, label.added…) ou test. |
x-sybox-timestamp | La date d'envoi, en secondes Unix. |
x-sybox-signature-rsa | Une seconde signature : rsa-sha256= suivi du RSA-SHA256 (PKCS#1 v1.5) en base64 de x-sybox-timestamp, d'un point et du corps brut. |
x-sybox-key-id | Laquelle de nos clés l'a produite. La clé publique se trouve à l'adresse https://api.sybox.io/.well-known/sybox-webhook-key. |
Vérifiez la signature sur les octets bruts, avant de les analyser, et comparez en temps constant :
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();
});
Vous préférez ne pas conserver de secret partagé sur votre serveur ? Vérifiez plutôt x-sybox-signature-rsa avec notre clé publique. Dans les deux cas, refusez un horodatage de plus de cinq minutes.
Répondez avec n'importe quel code 2xx en moins de 15 secondes. Toute autre réponse — une erreur, un délai dépassé, une redirection — entraîne une nouvelle tentative après 1, 2, 4, 8, 16 et 32 minutes, puis l'événement est abandonné. Un événement peut arriver plusieurs fois : utilisez l'identifiant du message pour ignorer un doublon. L'adresse doit être publique et en https.
Un webhook qui échoue 20 fois de suite pendant plus d'une heure est désactivé automatiquement et l'administrateur de l'espace de travail est prévenu. Ses événements en attente sont conservés pendant 24 heures : réactivez-le pour les envoyer.
Pour tester, cliquez sur Envoyer un événement de test sur la carte du webhook. Livraisons, sur la même carte, affiche les 50 dernières livraisons avec la réponse de votre serveur et permet d'en renvoyer une.
Chat en direct
Chat en direct#
Ajoutez un chat à votre site web : les visiteurs y écrivent, votre équipe répond dans SyBox. Ajoutez le site dans Paramètres › Messagerie › Canaux, sur la carte Chat en direct ; la fenêtre qui s'ouvre, Code pour votre site, vous donne le code et les clés ci-dessous. Les trois clés secrètes ne sont affichées qu'une seule fois.
| Clé | Commence par | À quoi elle sert |
|---|
| Clé publique | lcpk_ | Se place dans vos pages. Elle peut être publiée sans risque : elle ne fait que désigner le site, et le chat ne s'affiche que sur vos Adresses de sites web. |
| Clé de signature pour les utilisateurs connectés | lcid_ | Sur votre serveur uniquement. Signe chaque utilisateur connecté, pour que personne ne puisse écrire au nom d'un autre. |
| Clé de serveur (pour votre propre écran de chat) | lcsk_ | Sur votre serveur uniquement. Seulement pour un site qui crée son propre écran de chat au lieu du chat prêt à l'emploi. |
| Clé de signature des réponses | lcwh_ | Sur votre serveur uniquement. Prouve qu'une réponse envoyée à votre serveur vient bien de SyBox. |
Le chat prêt à l'emploi n'a besoin que du code. La clé de signature sert à un site avec des comptes ; la clé de serveur et la clé de signature des réponses, uniquement à un site qui a son propre écran de chat.
Désactiver un site web masque immédiatement le chat. Les appels de votre serveur restent acceptés et enregistrés.
Ajouter le chat à votre site#
Collez le code une fois dans chaque page, juste avant la balise de fermeture body. Le chat ne s'affiche que sur les adresses indiquées dans Adresses de sites web, et sa couleur, sa position, son titre et son message d'accueil se règlent dans SyBox, pas dans le code.
<script async src="https://api.sybox.io/lc/w.js" data-key="lcpk_YOUR_PUBLIC_KEY"></script>
WordPress : téléchargez notre extension depuis la même fenêtre. Shopify : dans l'application Shopify de SyBox, activez Chat en direct sur cette boutique, puis SyBox Live Chat sous Intégrations d'applications — un client connecté est reconnu sans rien faire de plus.
Utilisateurs connectés : la clé de signature#
Lorsqu'un utilisateur est connecté à votre site, votre serveur indique au chat qui il est grâce à un jeton court — un JWT, HS256 — signé avec la clé de signature. L'utilisateur a alors une seule conversation à son nom, sur tous ses appareils, et personne ne peut l'ouvrir en saisissant l'identifiant de quelqu'un d'autre.
- Après la connexion, votre serveur crée le jeton à partir des claims ci-dessous.
- La page le transmet au chat sous la forme
identity, avant le code du chat. - Après une connexion sans rechargement de la page, appelez plutôt
SyBoxChat('identify', token).
| Champ | Description |
|---|
subobligatoirestring | L'identifiant de votre utilisateur, jusqu'à 120 caractères. Il ne doit pas commencer par v_. |
expobligatoirenumber | Expiration, en secondes Unix — pas plus d'un jour à l'avance. Dix minutes suffisent : le jeton n'est lu qu'à l'ouverture du chat. |
namestring | Le nom affiché dans la conversation. |
emailstring | L'adresse e-mail de l'utilisateur. |
phonestring | Le numéro de téléphone de l'utilisateur. |
picturestring | Une adresse https de la photo de l'utilisateur, jusqu'à 500 caractères. SyBox en garde sa propre copie, et la récupère à nouveau à la connexion suivante si l'adresse change. |
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>
Créez le jeton sur votre serveur uniquement. Une clé de signature dans une page web permet à n'importe qui d'écrire au nom de n'importe lequel de vos utilisateurs. Et ne mettez jamais un simple identifiant d'utilisateur dans la page à la place du jeton : n'importe qui pourrait alors choisir la conversation à lire.
Lorsque Uniquement les utilisateurs connectés à mon site est activé, un visiteur sans jeton ne voit pas le chat.
Un visiteur qui a discuté avant de se connecter se voit demander une seule fois, dans le même navigateur, s'il veut poursuivre cette conversation avec son compte. Vous n'avez rien à faire pour cela.
Contrôler le chat depuis votre page#
Le code du chat ajoute une fonction à votre page, 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'
};
Notifications après la fermeture de votre site#
Tant qu'une page de votre site est ouverte, une réponse s'affiche d'elle-même en notification. Pour joindre un visiteur après la fermeture de votre site, les navigateurs exigent un service worker sur votre propre domaine : servez cette seule ligne sous /sybox-lc-sw.js à la racine de votre site, en application/javascript.
try { importScripts('https://api.sybox.io/lc/sw.js'); } catch (e) {}
Choisissez le mode dans SyBox (Notifications quand le site est fermé) ou dans la page avec serviceWorker : "auto" (le fichier ci-dessus, par défaut), un chemin de votre choix, "site" (placez la même ligne en haut de votre propre service worker) ou "off".
Le chat ne remplace jamais un service worker que votre site possède déjà, et le fichier n'a pas de gestionnaire fetch : il ne voit donc jamais aucune requête de votre site. L'extension WordPress sert le fichier pour vous. Safari sur iPhone n'affiche aucune notification pour un simple site web.
Votre propre écran de chat : la clé de serveur#
Un site ou une application qui a son propre écran de chat communique avec SyBox depuis son serveur au lieu d'utiliser le chat prêt à l'emploi. Chaque appel transmet Authorization: Bearer lcsk_…. Votre serveur désigne l'utilisateur par votre propre identifiant, et SyBox lui fait confiance, car il détient la clé.
| Appel | Description |
|---|
POST /v1/livechat/messages | Votre utilisateur écrit. client_id rend une nouvelle tentative sans risque : un même identifiant n'est enregistré qu'une fois ("duplicate": 1). |
POST /v1/livechat/upload | Un fichier, en données de formulaire multipart : file, user_id, client_id. Jusqu'à 16 Mo : images, messages vocaux, vidéos, PDF, texte et documents Office. |
GET /v1/livechat/messages?user_id=&since=&before=&limit= | La conversation : les messages après since, et agents_read_id, le plus récent des messages de votre utilisateur lu par un agent. |
POST /v1/livechat/read | Votre utilisateur a lu les réponses jusqu'à up_to. |
POST /v1/livechat/edit | Votre utilisateur modifie son propre message, dans les 15 minutes. |
POST /v1/livechat/delete | Votre utilisateur supprime son propre message pour tout le monde, dans les 24 heures. |
POST /v1/livechat/react | Une réaction sur n'importe quel message ; un emoji vide la retire. |
POST /v1/livechat/typing | Votre utilisateur est en train d'écrire : envoyez on: 1 toutes les quelques secondes et on: 0 quand il s'arrête. |
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 message, dans l'historique et dans les réponses :
{
"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
}
L'historique d'un utilisateur qui n'a jamais écrit répond 404 unknown_user : affichez une conversation vide.
Les liens des médias sont signés et valables une heure ; redemandez-en un nouveau. SyBox ne récupère jamais un fichier depuis une adresse que vous envoyez : téléversez-le. Jusqu'à 120 appels par minute et par site web.
Les réponses sur votre serveur : la clé de signature des réponses#
Saisissez l'Adresse de votre serveur pour les réponses sur le site web. Lorsque votre équipe répond, modifie, supprime ou lit, SyBox y envoie l'événement en POST, signé avec la clé de signature des réponses.
| En-tête | Description |
|---|
x-sybox-signature | t=TIMESTAMP,v1=SIGNATURE — TIMESTAMP en secondes Unix, SIGNATURE le HMAC-SHA256 en hexadécimal de TIMESTAMP.BODY (le corps brut), calculé avec la clé de signature des réponses. |
x-sybox-event | message.created, message.updated, message.deleted ou messages.read. |
x-sybox-delivery | Identique à chaque nouvelle tentative d'un même événement : utilisez-le pour ignorer un doublon. |
Le corps contient event, line_id, user_id et message — ou message_id pour une suppression, et read_id pour messages.read : les messages de votre utilisateur sont lus jusqu'à cet identifiant.
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();
});
Vérifiez la signature sur le corps brut, refusez un t de plus de cinq minutes, et répondez rapidement par un 2xx. Toute autre réponse entraîne une nouvelle tentative après 10 s, 30 s, 2 min, 10 min et 30 min, puis l'événement est abandonné. SyBox n'appelle que des adresses publiques et ne suit jamais de redirection.
Codes de motif#
Chaque réponse du Chat en direct est en JSON — {"ok": 1, …}, ou {"ok": 0, "error": "CODE"} avec un code plutôt qu'une phrase :
| Code | Statut | Description |
|---|
unauthorized | 401 | La clé de serveur est incorrecte ou a été remplacée. |
invalid_json invalid_payload invalid_user_id | 400 | La requête n'a pas le format attendu. |
empty_message no_file empty_file | 400 | Ni texte ni fichier. |
too_long | 400 | Le texte modifié dépasse 4 096 caractères. |
not_an_emoji | 400 | emoji n'est pas un emoji. |
not_found | 404 | Ce message n'existe pas, ou il n'appartient pas à cet utilisateur. |
too_late | 409 | Le délai pour modifier ou supprimer est dépassé. |
file_type_not_allowed | 400 | Ce type de fichier n'est pas accepté. |
file_too_large | 413 | Le fichier dépasse 16 Mo. |
blocked | 403 | Votre équipe a bloqué cet utilisateur. |
unknown_user | 404 | Aucune conversation pour cet identifiant d'utilisateur pour l'instant. |
rate_limited | 429 | Trop d'appels : ralentissez et réessayez. |
maintenance | 503 | SyBox est en cours de mise à jour : réessayez après Retry-After secondes. |
server_error | 500 | Une erreur s'est produite de notre côté. |
Remplacer une clé#
Vous avez perdu une clé, ou pensez qu'elle a fuité ? Sur la fiche du site web, cliquez sur Nouvelle clé de signature, Nouvelle clé de serveur ou Nouvelle clé de signature des réponses. Une nouvelle clé de serveur laisse l'ancienne fonctionner pendant un jour, pour que vous puissiez basculer sans interruption ; les autres clés cessent de fonctionner immédiatement.