Developer Documentation

Connect your own software to your SyBox inbox

Send WhatsApp messages from your own number, show your customers' data beside each chat, file cases, receive every event on your own server, and put a live chat on your website. The send route speaks the WhatsApp Cloud API format, so code written for Meta needs only a new base URL and a SyBox key.

Base URLhttps://api.sybox.io

Getting started

Quickstart

Three things and your first message is on its way: the base URL, a device key, and the messages endpoint.

POST/v26.0/{phone-number-id}/messagesAuthorization: Bearer <DEVICE_KEY>

The version segment is accepted in any v<n> form (v26.0, v1.0, …) — keep whatever your current code uses. The {phone-number-id} is your WhatsApp number's ID, exactly as with Meta.

The {phone-number-id} is shown after id on the number's card in Settings › Messaging › Channels. A sending key is bound to one number and sends only from it.

cURL
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" }
  }'

Each key's card in Settings › Integrations › API keys shows when it was last used — the quickest way to see whether your calls arrive.

Getting started

Authentication and keys

Every call carries a key in the Authorization header as a Bearer token. Each key does one job: a key made to send cannot file a case, and a key made to file cases cannot send. Keep every key on your server — the only exception is the Live Chat public key, which is made to sit in your pages.

HTTP
Authorization: Bearer YOUR_KEY
Content-Type: application/json
KeyWhat it is for
Send through WhatsAppPOST /v{n}/{phone-number-id}/messages
File a casePOST /v1/case
Start an automationPOST /v1/automation
Read and update contactsPOST /v1/contacts/…
Receive from a programPOST /v1/inbound
Public key · lcpk_<script … data-key="lcpk_…">
Signing key for signed-in users · lcid_Signed-in users: the signing key
Server key (for your own chat screen) · lcsk_/v1/livechat/*
Reply signature key · lcwh_x-sybox-signature
Signing secretX-Hub-Signature-256

Getting started

Getting a device key

Keys are created inside your workspace and carry the routing themselves — no subdomain, no account id in the request.

  1. Open your workspace → Settings → API keys and create a device key.
  2. Each key is bound to one number and can be revoked at any time.
  3. Your real WhatsApp token never leaves our server — the key stands in for it.
  4. The gateway is off by default; enabling API access for the workspace switches it on.

WhatsApp API

Send your first message

A plain text message. The body is the standard Cloud API payload — we pass it through to Meta untouched.

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" }
  }'

The Response

You get back exactly what Meta returns, with the same HTTP status — so any existing Cloud API client library parses it unchanged.

200 OK · application/json
{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
  "messages": [{ "id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI…" }]
}

WhatsApp API

Any message type works

Because the body is forwarded untouched, every Cloud API message type is supported — templates, images, documents, interactive messages. Here's a template send.

{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "template",
  "template": {
    "name": "order_shipped",
    "language": { "code": "en_US" },
    "components": [{
      "type": "body",
      "parameters": [{ "type": "text", "text": "#10432" }]
    }]
  }
}

Free text reaches a customer only within 24 hours of their last message. Outside that window send an approved template: SyBox refuses free text there with code 131047 before it reaches Meta.

WhatsApp API

Key rules and limits

Each sending key carries its own rules, set when you make it: free text, templates or both; media or not; which templates; which recipient countries; the hours it may send; and a daily limit. A send that breaks one is refused by SyBox before it reaches Meta.

StatusWhen
404An empty answer: the API gateway is switched off, or the key belongs to no workspace. Check the switch at the top of Settings › Integrations › API keys.
401The key is missing or not known.
403The key is not a sending key, or sends from a number it is not bound to.
403The message breaks one of the key's rules: free text, templates, media, the template's name, the country or the hours.
403Meta has paused the template, or limited it for its quality rating.
400The 24-hour window is closed (code 131047): send a template instead.
400The body is not valid JSON, or the key's WhatsApp number is no longer connected.
409Meta has stopped this number from sending for now.
429The key's daily limit, or the number's WhatsApp messaging limit, is reached.
502Meta could not be reached. Try again.

The daily count starts again at midnight UTC, and a send that fails is not counted.

WhatsApp API

Errors

Errors come from two places, and both use Meta's error shape, so an existing Cloud API client reads them unchanged. An error from Meta comes back exactly as Meta sent it, with Meta's status code. An error from one of SyBox's own rules has the type SyBoxGatewayError, and its message says which rule refused the call.

{
  "error": {
    "message": "(#131030) Recipient phone number not in allowed list",
    "type": "OAuthException",
    "code": 131030
  }
}

Your system in SyBox

Show your data beside the chat

When an agent opens a conversation, SyBox calls YOUR endpoint and shows what you return right beside the chat — the customer's orders, balance, tickets. We send the customer's phone, or their email on an email conversation.

Add your endpoint in Settings › Integrations › ERP APIs. Every call is a POST with your key in Authorization and the request in one field, data, holding a JSON string — so either side can add fields later without breaking the other. Answer within 15 seconds.

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\":\"\"}" }

Each call also says who is asking: X-Sybox-Lang (the user's language — answer in it), X-Sybox-Theme (light or dark, for an html reply), X-Sybox-Agent (the user's id) and X-Sybox-Number (the line the conversation is on).

We send both phone and email — either may be empty. Match on whichever identifies the customer in your system, and reply with { renderAs, content }.

renderAs

Set renderAs to tell SyBox how to display your content — one of:

renderAsDescription
tableRows and columns. content is an array of flat objects (or { rows: [ … ] }); their keys become the column headers.
cards · kpiSmall tiles, such as a balance or an order count. content is an array of { label, value } (or { cards: [ … ] }).
feed · list · threadA timeline of cards. content is an array of { title, text, date, tags, media: [{ type, url, name }] }.
messageOne record laid out as a single message-style card.
htmlYour own HTML, as a string in content. Only for markup you generate yourself.
sectionsSeveral blocks in one reply: content is { sections: [ { title, renderAs, content } ] }.
jsonThe default when renderAs is left out: SyBox shows your content as formatted JSON.

Add a sybox_ref to any table row or card and it becomes clickable — SyBox calls you again with that ref so you can return the single record's detail.

Product search (X-Sybox-Action: products)

The same endpoint can also answer product searches. The user types what the customer is asking for, SyBox sends it as "q", and your reply is drawn as product cards next to the chat — with a one-tap Send that puts the product link in the composer.

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": "" }
FieldDescription
namerequiredThe only required field. A product with nothing but a name still renders as a tidy card.
price · oldPrice · currencyNumbers, not formatted strings. Send oldPrice only when there is a genuine previous price — the discount badge is calculated from the pair.
available · stockavailable is true/false; stock is an optional count shown beside it. Omit them entirely if you do not track stock — an absent field draws nothing, and is never read as out of stock.
imageA direct https image URL. Anything that fails to load falls back to a neutral placeholder.
linkThe product page URL. Without it the card is read-only — Send to chat appears only when there is something to send.
noteOne short free line under the price — delivery time, a variant, a condition.

Only the endpoint you already built: same URL, same key, same { "data": "…" } envelope. Look at X-Sybox-Action to tell a product search from a customer lookup. Reply with content as a plain array (or { items: [ … ] }).

Your system in SyBox

File a case from your own system

This is the route an ERP or accounting system uses. Your program does not send a formatted message about a complaint — it asks SyBox to open the case. One record, one place, one number your staff can quote back to the customer.

POST/v1/caseAuthorization: Bearer <CASE_KEY>
curl -X POST https://api.sybox.io/v1/case \
  -H "Authorization: Bearer $SYBOX_CASE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_unique_id": "CUST-10432",
    "header": "Screen replacement not delivered",
    "description": "Paid on 12 Aug, promised in 3 days, still nothing.",
    "department_id": 5,
    "ext_ref": "TICKET-99817"
  }'

The fields

FieldDescription
customer_unique_idYour own id for the customer — the same key the customer sync uses. Send phone instead when you have no id.
phoneThe customer's phone number, when you have no id.
header · descriptionAt least one is required. The header is the line users see in the list.
department_idrequiredintegerRequired. The department the case is filed in: the number shown beside it in Settings › Departments (for example #5).
ext_refYour ticket number. Send it. A retry with the same ext_ref returns the case that already exists instead of filing a twin — making the call safe to repeat after a timeout.
sectioninteger1 to 4: which case tab it goes in. Default 1.
deadlinestringA due date, YYYY-MM-DD.
notifybooleanDefault true: a note with the case number appears in the customer's conversation in SyBox. Nothing is sent to the customer. false leaves it out.
sourcestringThe program name shown on that note. Default ERP.

The answer carries the case's number, the one your staff quote back to the customer. A retry with the same ext_ref answers "duplicate": true with the case that already exists, without customer_id.

The customer must already be in SyBox. When the id is not found, SyBox runs one customer sync with your system and looks again; if it is still not found, the answer is 404 — never a case against an invented customer.

Use a key of kind 'case'. A relay key (the one that sends messages) is refused here with 403, on purpose: a key that can message your customers should not also be able to open records in your CRM.

Your system in SyBox

Push a message in from another program

For anything that is not WhatsApp: a note from your point of sale, an alert from a device, a line from a legacy system. It lands in the right conversation, keyed by phone number, marked with the name of the program that sent it.

POST/v1/inboundAuthorization: Bearer <INBOUND_KEY>

Keys of this kind are no longer made: an existing Receive from a program key keeps working. For a new integration, file a case — SyBox writes the note into the conversation for you.

cURL
curl -X POST https://api.sybox.io/v1/inbound \
  -H "Authorization: Bearer $SYBOX_INBOUND_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "15551234567",
    "text": "Order 4821 has left the warehouse.",
    "source": "Warehouse"
  }'
FieldDescription
phonerequiredstringThe customer's phone number, with the country code.
textrequiredstringThe message.
sourcestringThe program's name, shown on the message.

The answer is {"ok": 1, "id": …}, the id of the note in the conversation.

Inbound only, by design. This route writes into the conversation; it does not send anything to the customer. Use the messages route above for that.

Your system in SyBox

Contacts: read and update

With a key of the type Read and update contacts, a program can find a contact, read it, and change its name, email, contact fields and labels. Every call is a POST with a JSON body.

The key acts as the person who made it: it finds only the contacts that person may see, and only if that person may open the contacts list and see numbers. To change anything, that person must be allowed to edit contacts. A key whose maker is switched off stops working.

POST/v1/contacts/searchAuthorization: Bearer <CONTACTS_KEY>

Finds contacts whose name, phone or email contains q. limit is 20 by default, 50 at most.

POST/v1/contacts/get

One contact, by phone or by key (the same contact.key a webhook sends).

POST/v1/contacts/update

Changes only what you send: name, email, and fields (contact fields by id or by name, with their values; an empty value clears one).

POST/v1/contacts/labels

Adds and removes labels by id or by name: add and remove are lists.

curl -X POST https://api.sybox.io/v1/contacts/update \
  -H "Authorization: Bearer $SYBOX_CONTACTS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "201112223344",
    "name": "Sara Ali",
    "fields": { "city": "Alexandria" }
  }'

# Find:   POST /v1/contacts/search  {"q": "sara", "limit": 20}
# Read:   POST /v1/contacts/get     {"phone": "201112223344"}
# Labels: POST /v1/contacts/labels  {"phone": "201112223344", "add": ["VIP"], "remove": [7]}

Each call answers with the contact (search: a list of them): key, name, phone, email, channel, labels and fields.

Events

Webhooks: events on your server

Add your endpoint in Settings › Integrations › Webhooks, choose the events and the numbers, and SyBox POSTs each event to it as it happens. A WhatsApp event arrives in Meta's own webhook format, so a handler written for Meta works unchanged.

EventDescription
messageIncoming messageA customer's message.
statusMessage statusSent, delivered, read or failed — for a message you sent.
reactionReactionA customer reacted to a message.
doneConversation marked doneA conversation was marked done: by a person, by an automation, or on its own after the customer's silence.
labelLabel added or removedA label was added to a conversation, or removed from it.
contactContact created or updatedA contact was created, or its name, email or contact fields changed.
dealDeal closedA closed deal was recorded in a conversation.
caseCRM case opened or closedA CRM case was opened, or closed (solved, not solved or canceled).
Request · SyBox → you
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. Meta's own link needs a Meta token to open, so SyBox replaces it with a signed link to its own copy of the file, valid for 24 hours. A voice note can also carry its words in sybox.transcription, when the webhook asks for them.

Other channels — Messenger, Instagram, Telegram and Live Chat — arrive in SyBox's own format, for the message event only:

JSON
{
  "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" }
  }
}

Conversation events arrive in SyBox's own format, for every channel. event says exactly what happened:

Event<code>event</code>
Conversation marked doneconversation.done
Label added or removedlabel.added · label.removed
Contact created or updatedcontact.created · contact.updated
Deal closeddeal.closed
CRM case opened or closedcase.opened · case.closed
Test eventtest
{
  "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" }
}

Signing secret

HeaderDescription
X-Hub-Signature-256sha256= and the hex HMAC-SHA256 of the raw body, keyed with the webhook's signing secret — the same header and method as Meta's.
x-sybox-signatureThe same value, so an endpoint that also receives Meta's own calls can tell ours apart.
x-sybox-eventWhat happened: message, status, reaction, one of the conversation events below (conversation.done, label.added…), or test.
x-sybox-timestampWhen it was sent, in Unix seconds.
x-sybox-signature-rsaA second signature: rsa-sha256= and the base64 RSA-SHA256 (PKCS#1 v1.5) of x-sybox-timestamp, a dot, and the raw body.
x-sybox-key-idWhich of our keys made it. The public key is at https://api.sybox.io/.well-known/sybox-webhook-key.

Check the signature on the raw bytes, before you parse them, and compare in constant time:

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
});

Rather not keep a shared secret on your server? Check x-sybox-signature-rsa with our public key instead. Refuse a timestamp more than five minutes old either way.

Answer with any 2xx within 15 seconds. Anything else — an error, a timeout, a redirect — is tried again after 1, 2, 4, 8, 16 and 32 minutes, then dropped. An event can arrive more than once: use the message's id to ignore a repeat. The address must be public and https.

A webhook that fails 20 times in a row, for more than an hour, is switched off on its own and the workspace's admin is told. Its waiting events are kept for 24 hours: switch it back on to send them.

To test, press Send test event on the webhook's card. Deliveries, on the same card, lists the last 50 deliveries with your server's answer, and can send one again.

Live Chat

Live Chat

Put a chat on your website: visitors write there, your team answers in SyBox. Add the website in Settings › Messaging › Channels, on the Live Chat card; the window that opens, Code for your site, gives you the code and the keys below. The three secret keys are shown only once.

KeyStarts withWhat it is for
Public keylcpk_Goes in your pages. Safe to publish: it only names the website, and the chat shows only on your Website addresses.
Signing key for signed-in userslcid_Your server only. Signs each signed-in user, so nobody can write as someone else.
Server key (for your own chat screen)lcsk_Your server only. Only for a site that builds its own chat screen instead of the ready chat.
Reply signature keylcwh_Your server only. Proves that a reply posted to your server came from SyBox.

The ready chat needs only the code. The signing key is for a site with accounts; the server key and the reply signature key only for a site with its own chat screen.

Switching a website off hides the chat at once. Your server's calls are still accepted and stored.

Add the chat to your site

Paste the code once into every page, just before the closing body tag. The chat shows only on the addresses in Website addresses, and its colour, position, title and welcome are set in SyBox, not in the code.

HTML
<script async src="https://api.sybox.io/lc/w.js" data-key="lcpk_YOUR_PUBLIC_KEY"></script>

WordPress: download our plugin from the same window. Shopify: in SyBox's Shopify app, turn on Live Chat on this store, then SyBox Live Chat under App embeds — a signed-in customer is recognised with nothing more to do.

Signed-in users: the signing key

When a user is signed in to your site, your server tells the chat who they are with a short token — a JWT, HS256 — signed with the signing key. The user then has one conversation under their name, on every device, and nobody can open it by typing someone else's id.

  1. After sign-in, your server builds the token from the claims below.
  2. The page passes it to the chat as identity, before the chat's code.
  3. After a sign-in without a page reload, call SyBoxChat('identify', token) instead.
FieldDescription
subrequiredstringYour user's id, up to 120 characters. It may not start with v_.
exprequirednumberExpiry, in Unix seconds — no more than one day ahead. Ten minutes is right: the token is read only when the chat opens.
namestringThe name the conversation shows.
emailstringThe user's email address.
phonestringThe user's phone number.
picturestringAn https address of the user's photo, up to 500 characters. SyBox keeps its own copy, and fetches it again at the next sign-in when the address changes.
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";
HTML
<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>

Make the token on your server only. A signing key in a web page lets anyone write as any of your users. And never put a bare user id in the page in place of the token: that would let anyone choose whose conversation to read.

With Only signed-in users of my site switched on, a visitor without a token does not see the chat.

A visitor who chatted before signing in is asked once, in the same browser, whether to continue that conversation with their account. You do nothing for this.

Control the chat from your page

The chat's code adds one function to your page, SyBoxChat:

JavaScript
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 after your site is closed

While a page of your site is open, a reply shows as a notification by itself. To reach a visitor after your site is closed, browsers require a service worker on your own domain: serve this one line as /sybox-lc-sw.js at your site's root, as application/javascript.

/sybox-lc-sw.js
try { importScripts('https://api.sybox.io/lc/sw.js'); } catch (e) {}

Choose the mode in SyBox (Notifications when the website is closed) or in the page with serviceWorker: "auto" (the file above, the default), a path of your own, "site" (put the same line at the top of your own service worker) or "off".

The chat never replaces a service worker your site already has, and the file has no fetch handler, so it never sees a request of your site. The WordPress plugin serves the file for you. iPhone Safari shows no notifications for a plain website.

Your own chat screen: the server key

A site or app with its own chat screen talks to SyBox from its server instead of using the ready chat. Every call carries Authorization: Bearer lcsk_…. Your server names the user by your own id, and is trusted to, because it holds the key.

CallDescription
POST /v1/livechat/messagesYour user writes. client_id makes a retry safe: the same id is stored once ("duplicate": 1).
POST /v1/livechat/uploadA file, as multipart form data: file, user_id, client_id. Up to 16 MB: images, voice, video, PDF, text and Office documents.
GET /v1/livechat/messages?user_id=&since=&before=&limit=The conversation: the messages after since, and agents_read_id, the newest of your user's messages an agent has read.
POST /v1/livechat/readYour user has read the replies up to up_to.
POST /v1/livechat/editYour user edits their own message, within 15 minutes.
POST /v1/livechat/deleteYour user deletes their own message for everyone, within 24 hours.
POST /v1/livechat/reactA reaction on any message; an empty emoji takes it off.
POST /v1/livechat/typingYour user is typing: send on: 1 every few seconds and on: 0 when they stop.
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": "" }
  }'

A message, in the history and in replies:

JSON
{
  "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
}

The history of a user who has never written answers 404 unknown_user: show an empty conversation.

Media links are signed and work for one hour; ask again for a fresh one. SyBox never fetches a file from an address you send: upload it. Up to 120 calls a minute per website.

Replies on your server: the reply signature key

Enter Your server's address for replies on the website. When your team replies, edits, deletes or reads, SyBox POSTs the event there, signed with the reply signature key.

HeaderDescription
x-sybox-signaturet=TIMESTAMP,v1=SIGNATURE — TIMESTAMP in Unix seconds, SIGNATURE the hex HMAC-SHA256 of TIMESTAMP.BODY (the raw body), keyed with the reply signature key.
x-sybox-eventmessage.created, message.updated, message.deleted or messages.read.
x-sybox-deliveryThe same on every retry of one event: use it to ignore a repeat.

The body holds event, line_id, user_id and message — or message_id for a deletion, and read_id for messages.read: your user's messages are read up to that 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 }
});

Check the signature on the raw body, refuse a t more than five minutes old, and answer 2xx quickly. Anything else is tried again after 10 s, 30 s, 2 min, 10 min and 30 min, then dropped. SyBox calls public addresses only and never follows a redirect.

Reason codes

Every Live Chat answer is JSON — {"ok": 1, …}, or {"ok": 0, "error": "CODE"} with a code rather than a sentence:

CodeStatusDescription
unauthorized401The server key is wrong or replaced.
invalid_json invalid_payload invalid_user_id400The request is not in the expected shape.
empty_message no_file empty_file400No text and no file.
too_long400The edited text is over 4,096 characters.
not_an_emoji400emoji is not an emoji.
not_found404No such message, or not one of this user's.
too_late409The time to edit or delete has passed.
file_type_not_allowed400This kind of file is not accepted.
file_too_large413The file is over 16 MB.
blocked403Your team has blocked this user.
unknown_user404No conversation for this user id yet.
rate_limited429Too many calls: slow down and try again.
maintenance503SyBox is updating: try again after Retry-After seconds.
server_error500Something failed on our side.

Replacing a key

Lost a key, or think it leaked? On the website's card, click New signing key, New server key or New reply signature key. A new server key leaves the old one working for one day, so you can switch over without a gap; the other keys stop at once.