Embed the live-chat widget
Check whether live chat is on, start a conversation as a visitor, send and read messages with the conversation's token, and end the chat, with no account.
Updated
JourneyAPI: content and public APIsstep 3 of 3
On this page
Live chat lets a visitor on a website talk to the team without an account. The widget on the Solnyxus site uses the four routes described here, and you can build the same thing on your own page. You can learn whether chat is on, open a conversation with a first message, read the replies, send more, and end the chat.
These routes need no sign-in. A conversation is protected by its own token instead: the server gives it to you once, when you start the chat, and you send it with every later call. Every route is limited per address, and it answers in plain sentences that are safe to show to a visitor.
Check that chat is on
/api/v1/public/chat/config- Auth
- none
Call this first and show nothing unless it says chat is enabled. When it is off, the answer is exactly {"enabled": false}.
| Field | Type | Meaning |
|---|---|---|
enabled | boolean | true when chat is switched on and working. |
online | boolean | true when someone is available now. When false, a visitor must leave an email address to start a chat. |
greeting | string | Text to show above the first message. |
offlineMessage | string | Text to show when nobody is online. |
collectEmail | boolean | Whether to ask for an email address. |
retentionDays | integer | How many days conversations are kept. Tell the visitor. |
curl -s "https://solnyxus.com/api/v1/public/chat/config"
const res = await fetch("https://solnyxus.com/api/v1/public/chat/config");
const config = await res.json();
if (!config.enabled) console.log("chat is off: draw nothing");
else console.log(config.online ? "online" : "offline", config.greeting);
import requests
config = requests.get("https://solnyxus.com/api/v1/public/chat/config", timeout=30).json()
if not config["enabled"]:
print("chat is off: draw nothing")
else:
print("online" if config["online"] else "offline", config["greeting"])
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, err := http.Get("https://solnyxus.com/api/v1/public/chat/config")
if err != nil {
panic(err)
}
defer res.Body.Close()
var c struct {
Enabled, Online bool
Greeting string
}
json.NewDecoder(res.Body).Decode(&c)
fmt.Println(c.Enabled, c.Online, c.Greeting)
}
{
"enabled": true,
"online": true,
"greeting": "Hi! Ask us anything. A person will answer here.",
"offlineMessage": "We are not online right now. Leave your email and a message and we will get back to you.",
"collectEmail": true,
"retentionDays": 90
}
Start a conversation
/api/v1/public/chat/start- Auth
- none
- Rate limit
- 5 new conversations per address per hour
Opens a conversation with the visitor's first message and answers 201. Keep the token from the answer: it is shown once and cannot be fetched again.
Parameters (JSON body)
| Name | Type | Required | Description |
|---|---|---|---|
message | string | Required | The first message: 2,000 characters at most and 20 line breaks. Control and invisible characters are removed. |
name | string | No | 80 characters at most. |
email | string | No | A valid address. Required when online is false, so that a reply can reach the visitor. |
pageUrl | string | No | The page the visitor is on. Only its scheme, host and path are kept, as text, and the server never fetches it. |
Do not send a field called website; it is reserved, and a request that fills it is ignored.
curl -s -X POST "https://solnyxus.com/api/v1/public/chat/start" \
-H "Content-Type: application/json" \
-d '{"name":"Sam","email":"sam@example.com","message":"Hello, can you help me reset my sign-in?","pageUrl":"https://example.com/pricing"}'
const res = await fetch("https://solnyxus.com/api/v1/public/chat/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Sam",
email: "sam@example.com",
message: "Hello, can you help me reset my sign-in?",
pageUrl: "https://example.com/pricing",
}),
});
const chat = await res.json();
if (res.status !== 201) throw new Error(chat.error);
console.log(chat.conversationId, chat.online);
// Keep chat.token in the visitor's browser only.
import requests
res = requests.post(
"https://solnyxus.com/api/v1/public/chat/start",
json={
"name": "Sam",
"email": "sam@example.com",
"message": "Hello, can you help me reset my sign-in?",
"pageUrl": "https://example.com/pricing",
},
timeout=30,
)
chat = res.json()
if res.status_code != 201:
raise SystemExit(chat["error"])
print(chat["conversationId"], chat["online"])
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
func main() {
body := `{"name":"Sam","email":"sam@example.com","message":"Hello, can you help me reset my sign-in?","pageUrl":"https://example.com/pricing"}`
res, err := http.Post("https://solnyxus.com/api/v1/public/chat/start", "application/json", strings.NewReader(body))
if err != nil {
panic(err)
}
defer res.Body.Close()
var chat struct {
ConversationID string
Online bool
Error string
}
json.NewDecoder(res.Body).Decode(&chat)
if res.StatusCode != http.StatusCreated {
panic(chat.Error)
}
fmt.Println(chat.ConversationID, chat.Online)
}
The answer holds the new conversation's id, its token, the message you sent as the server stored it, whether someone is online, and the status. A new conversation is waiting until a person replies.
{
"conversationId": "3f9c0038-0000-4000-8000-000000000038",
"token": "paste-the-token-you-receive",
"messages": [
{
"id": "3f9c0039-0000-4000-8000-000000000039",
"sender": "visitor",
"body": "Hello, can you help me reset my sign-in?",
"createdAt": "2026-10-08T09:30:00Z"
}
],
"online": true,
"status": "waiting"
}
| Status | When |
|---|---|
400 | The message is empty, too long or has too many line breaks; the name is over 80 characters; the email is not valid; or nobody is online and no email was given. The error text is written for the visitor. |
413 | The request body is over 16 KB. |
429 | Too many requests or too many new chats. Wait for the Retry-After header; the body carries retryAfterSeconds. |
503 | Chat is switched off: Live chat is not available right now. |
The conversation token
The token is the visitor's only credential. It is 32 random bytes, and the server keeps only a hash of it. Send it with every call after start, in either header:
X-Chat-Token: <token>Authorization: Bearer <token>, which is the one to use from a page on another origin, because browsers only let those pages send headers the API's CORS rules name.
Keep it in the visitor's own browser, ask for it nowhere else, and never put it in a URL or a log. A wrong token, an unknown conversation id and a malformed id all give the same 404 with That chat was not found., so the API never confirms that a conversation exists. Repeated wrong guesses from one address are slowed down with 429.
Read messages
/api/v1/public/chat/{id}/messages- Auth
- conversation token
- Rate limit
- 20 reads, then one every 2 seconds, per conversation
Returns the conversation's messages, oldest first, up to 200. Pass since, the createdAt of the last message you hold, to receive only what is newer. The server starts a couple of seconds before since so that nothing in flight is missed, which means you can receive a message twice: merge by id. A since that is not an RFC 3339 time is 400.
curl -s "https://solnyxus.com/api/v1/public/chat/$CONVERSATION_ID/messages?since=2026-10-08T09:30:00Z" \
-H "X-Chat-Token: $TOKEN"
const id = "paste-the-conversation-id-here";
const token = "paste-the-token-here";
const since = encodeURIComponent("2026-10-08T09:30:00Z");
const res = await fetch(`https://solnyxus.com/api/v1/public/chat/${id}/messages?since=${since}`, {
headers: { "X-Chat-Token": token },
});
const data = await res.json();
for (const m of data.messages) console.log(m.sender, m.name ?? "", m.body);
if (data.closed) console.log("the chat has ended");
import os
import requests
res = requests.get(
f"https://solnyxus.com/api/v1/public/chat/{os.environ['CONVERSATION_ID']}/messages",
params={"since": "2026-10-08T09:30:00Z"},
headers={"X-Chat-Token": os.environ["TOKEN"]},
timeout=30,
)
data = res.json()
for m in data["messages"]:
print(m["sender"], m.get("name", ""), m["body"])
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
)
func main() {
u := "https://solnyxus.com/api/v1/public/chat/" + os.Getenv("CONVERSATION_ID") +
"/messages?since=" + url.QueryEscape("2026-10-08T09:30:00Z")
req, _ := http.NewRequest("GET", u, nil)
req.Header.Set("X-Chat-Token", os.Getenv("TOKEN"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data struct {
Messages []struct{ Sender, Name, Body string }
Closed bool
}
json.NewDecoder(res.Body).Decode(&data)
for _, m := range data.Messages {
fmt.Println(m.Sender, m.Name, m.Body)
}
}
A message has id, sender (visitor, staff or system), body, createdAt and, for staff, the first name only in name. A staff message that contains a video-call link gets meetUrl for a Join button. A visitor sees only what was written for them: the team's private notes never appear in this list. The widget polls about every 4 seconds while open and every 20 seconds when folded away; do the same or slower.
{
"messages": [
{
"id": "3f9c003a-0000-4000-8000-00000000003a",
"sender": "staff",
"name": "Alex",
"body": "Hi Sam, happy to help. Which sign-in are you trying to reset?",
"createdAt": "2026-10-08T09:31:10Z"
}
],
"status": "open",
"closed": false
}
Send a message
/api/v1/public/chat/{id}/messages- Auth
- conversation token
- Rate limit
- 20 messages per minute per conversation
Adds the visitor's next message and answers 201 with {message, status}. The body is {"body": "..."}, cleaned and limited like the first message: 2,000 characters and 20 line breaks. A conversation takes 300 visitor messages in all.
curl -s -X POST "https://solnyxus.com/api/v1/public/chat/$CONVERSATION_ID/messages" \
-H "X-Chat-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"body":"It is the portal sign-in."}'
const id = "paste-the-conversation-id-here";
const token = "paste-the-token-here";
const res = await fetch(`https://solnyxus.com/api/v1/public/chat/${id}/messages`, {
method: "POST",
headers: { "X-Chat-Token": token, "Content-Type": "application/json" },
body: JSON.stringify({ body: "It is the portal sign-in." }),
});
const out = await res.json();
console.log(res.status, out.message?.id ?? out.error);
import os
import requests
res = requests.post(
f"https://solnyxus.com/api/v1/public/chat/{os.environ['CONVERSATION_ID']}/messages",
json={"body": "It is the portal sign-in."},
headers={"X-Chat-Token": os.environ["TOKEN"]},
timeout=30,
)
out = res.json()
print(res.status_code, out.get("message", {}).get("id", out.get("error")))
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
u := "https://solnyxus.com/api/v1/public/chat/" + os.Getenv("CONVERSATION_ID") + "/messages"
req, _ := http.NewRequest("POST", u, strings.NewReader(`{"body":"It is the portal sign-in."}`))
req.Header.Set("X-Chat-Token", os.Getenv("TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}
| Status | When |
|---|---|
400 | The message is empty, too long, or has too many line breaks. |
404 | The id or the token is wrong. |
409 | The chat has ended. The answer carries closed: true; start a new chat to carry on. |
429 | Too many messages, or the 300-message limit. Wait for Retry-After. |
503 | Chat has been switched off. |
End the chat
/api/v1/public/chat/{id}/close- Auth
- conversation token
Lets the visitor end their conversation. It answers {"closed": true, "status": "closed"}, and calling it again is harmless.
curl -s -X POST "https://solnyxus.com/api/v1/public/chat/$CONVERSATION_ID/close" \
-H "X-Chat-Token: $TOKEN"
const id = "paste-the-conversation-id-here";
const token = "paste-the-token-here";
const res = await fetch(`https://solnyxus.com/api/v1/public/chat/${id}/close`, {
method: "POST",
headers: { "X-Chat-Token": token },
});
console.log(res.status, await res.json());
import os
import requests
res = requests.post(
f"https://solnyxus.com/api/v1/public/chat/{os.environ['CONVERSATION_ID']}/close",
headers={"X-Chat-Token": os.environ["TOKEN"]},
timeout=30,
)
print(res.status_code, res.json())
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, _ := http.NewRequest("POST", "https://solnyxus.com/api/v1/public/chat/"+os.Getenv("CONVERSATION_ID")+"/close", nil)
req.Header.Set("X-Chat-Token", os.Getenv("TOKEN"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}
Where next
- Every call is limited per address; when you are told to wait, wait the number of seconds in the
Retry-Afterheader. - Point visitors at answers before they ask, with Read the public knowledge base.