Channels: Web & Mobile.

Alongside WhatsApp, add a chat bubble to your website or connect your mobile app directly to Cxperium — same bot, same inbox.

Channel Types and Creation

A Cxperium account can have more than one channel. All channels connect to the same bot, the same contact list and the same inbox; agents see the conversation on one screen regardless of where it came from.

TypeWhat forHow it connects
WhatsAppYour Meta-approved business numberOur team handles the application; the approved number appears in the Channels list.
WebThe chat bubble on your websiteYou paste the install code provided by the dashboard into your site. No coding needed.
Mobile (custom channel)Your mobile app or your own clientOver REST + WebSocket; the app connects directly or through your own server.

To create a new channel:

  1. Open the Channels page

    Go to Channels in the left menu and click Add at the top right.

  2. Pick the type

    Choose the Web card for chat on your website, or Mobile for a mobile app or your own client.

  3. Give it a name and a bot address

    Enter a name for the channel and the bot address that incoming messages on this channel are forwarded to. The default language is optional. Click Add Channel.

  4. Collect the connection details

    The last step shows the channel key, the secret and (for the Web type) a ready-made install code. You can reach these later from the Detail window in the Channels list.

ValueExampleWhere it is used
Channel key (channelKey)ch_c727ccd3ef5112a1Identifies your channel in addresses and in the install code. Not secret.
Secret64-character hexIncluded in the install code for the web channel. For the mobile (custom) channel it is used only at registration (app) or to sign requests (server).
Bot addresshttps://bot.example.com/api/cxperiumThe chatbot endpoint every message from this channel is forwarded to. You can change it by editing the channel.
If the secret leaks: edit the channel in the Channels list and click Regenerate Secret, then update the install code on your site or the value in your app. The old secret is invalid from that moment.

Web Channel: Widget Setup

The web channel adds a chat bubble to the corner of your site. The visitor clicks the bubble, optionally types a name and starts talking to the bot or your agent. Setup is a single snippet of code.

site-install-code.html
<!-- Cxperium chat -->
<script>
  window.cxperiumSettings = {
    channelKey: "ch_c727ccd3ef5112a1",
    secret: "f57b4e…",
    brand: "Acme Support",
    color: "#7c3aed",
    launcherText: "Chat with us",
    texts: { greetingMessage: "Hi! How can we help you?" }
  };
  // Optional: introduce the user signed in on your site —
  // the welcome form is skipped and the chat starts right away:
  // window.cxperiumSettings.user = { name: "Jane Doe", username: "jane.d" };
  (function (d) {
    var s = d.createElement("script");
    s.src = "https://cdn.cxperium.com/widget/v1/cxperium-widget.js";
    s.async = true;
    d.head.appendChild(s);
  })(document);
</script>
  1. Copy the code

    Click Copy in the last step of channel creation or in the Channels → Detail window. If you customised the appearance, copy the code from the Widget Designer; that code contains your design.

  2. Paste it into your site

    Paste it right before the </body> tag on every page where the chat should appear. There must be only one Cxperium snippet per page; remove any old one.

  3. Reload the page

    The bubble should appear in the chosen corner. If not, reload without cache (Ctrl + F5).

The widget stores the visitor's identity and token in the browser; when the visitor returns, they can continue the previous conversation or open a new one. If you introduce a signed-in user through the user setting, the welcome form is never shown.

Security: the install code contains your channel key and secret. The secret on the page can only open a new chat record; it cannot read existing chats. Still, regenerate it if you believe it has leaked.

The same widget in a mobile app: you can also use the custom channel inside a WebView. The WebView Customizer screen of a mobile channel in the Channels list configures the look of the page you embed in your app, so you can add chat without writing the SDK integration below.

Widget Appearance (Widget Designer)

You change the bubble's colour, position, texts and brand from the dashboard. The designer opens only for channels of the Web type.

You reach the designer from two places: the Customize appearance button in the last step when you create the channel, or later from the channel's Detail window in the Channels list. The screen has three parts: settings on the left, a live preview on the right (Chat / Welcome form views) and the install code under the preview. There is no save button; every change is reflected in both the preview and the code within about half a second.

The design is not stored on the server. Your design lives inside the install code you paste into your site. The designer only keeps a draft in this browser; opening it from another computer shows the defaults. After every change, copy the code again and replace the one on your site.

Brand

SettingCode keyWhat it does
Brand namebrandThe name shown in the window title and above bot bubbles.
Avatar imageavatarUrlThe round logo in the header. If empty, the brand's initial is shown. Square, at least 96×96 px, full address (https://…).
Launcher iconiconUrlThe icon of the opening bubble in the corner. If empty, the standard chat icon.
Launcher textlauncherTextA short call-to-action next to the bubble (max. 24 characters). If empty, only the icon is shown.
Footer textfooterTextThe small line at the bottom of the window. Leave empty to hide the line.

Colours and font

SettingCode keyWhat it does
Primary colourcolorHeader, visitor bubbles, buttons and the launcher bubble.
GradientcolorDarkThe dark end of the header gradient. Derived from the primary colour if empty.
BackgroundbackgroundBackground of the chat area.
Text / MutedtextColor, mutedColorText colour in bot bubbles, and secondary text such as timestamps.
FontfontA CSS font name (e.g. Inter, sans-serif). If empty, the visitor's system font. The font must be loaded on your site.

Layout and size

SettingCode keyWhat it does
Positionpositionright (bottom right) or left (bottom left).
Side / bottom offsetsideOffset, bottomOffsetDistance of the bubble from the side and the bottom of the page (0–200 px). Increase the bottom offset if your site has a fixed bottom bar.
Panel width / heightwidth, heightSize of the opened window (280–600 / 400–900 px). On phones the window fills the screen.
Corner radiusradius0 for sharp corners, 40 for very round.

Texts and behaviour

SettingCode keyWhat it does
Ask the visitor's nameaskNameWhen on, the welcome form asks for a name and requires it. When off, the visitor starts as "Visitor".
Ask for a usernameaskUsernameSame rule for a username. Off by default; most sites pass it from their own session via the user setting.
Chat greeting messagetexts.greetingMessageThe first message shown as a bot bubble when the chat starts. Not shown if empty.
Welcome title / texttexts.welcomeTitle, texts.welcomeMessageTitle and description above the welcome form.
Name field label / placeholdertexts.nameLabel, texts.namePlaceholderLabel of the name box and the grey example text inside it.
Start buttontexts.startButtonText of the button that submits the form.
File / image sendingallowMediaAdds an attachment button to the message box. Max. 2 MB; images and PDF.
Message box placeholdertexts.inputPlaceholderThe grey text in the message input.
Language: the widget's default screen texts are in Turkish. For an English or other-language site, fill in the Texts section of the designer (the texts object in the code); keys you leave empty keep the Turkish default.

The install code contains only the settings that differ from the defaults, so it stays short. Reset to defaults in the top bar clears all settings and the draft.

Mobile App (Custom Channel)

The mobile channel (custom channel) connects your iOS, Android, React Native or Flutter app directly to Cxperium. The user writes a message in the app, the bot or an agent replies, and the reply lands in the app instantly. No backend required.

What forAddress
Registration, history, delivery acknowledgement (REST)https://provider.cxperium.com
Live messaging (WebSocket, socket.io)https://ws.cxperium.com — namespace /custom

Two setup paths

The decision comes down to one question: will your own server sit between your app and Cxperium?

A) Mobile app onlyB) You have your own server
Where the secret livesEmbedded in the app; used only at registrationOn your server as an environment variable
Who generates the user identityCxperium; returned in the registration responseYou do (e.g. u_42)
What authenticates requestsThe user token obtained at registration (Authorization: Bearer)An HMAC signature computed with the secret (X-Cxperium-Signature)
Is the user verified to be that personNo; sufficient for anonymous supportYes; your server verifies the user

If you have no backend, follow path A. The steps below describe path A; for path B only the authentication header changes (see "If you have a server: signature").

Step 1: Register (once, on first launch)

register.js
const res = await fetch("https://provider.cxperium.com/custom/ch_xxxxxxxx/register", {
  method: "POST",
  headers: { "apiKey": SECRET, "Content-Type": "application/json" },
  body: JSON.stringify({ user: { name: "Jane" } }),
}).then((r) => r.json());

// res = { status: 200, channelKey: "ch_xxxxxxxx",
//         contact: { id: "c_5d5cdeb1-…" }, token: "8abfc196…" }
save("contactId", res.contact.id);   // Keychain / EncryptedSharedPreferences
save("token", res.token);             // never expires, obtained once

If the app was reinstalled and you still have the stored contactId, include it in the registration body as user.id; the same token is returned and the conversation continues where it left off. The user id cannot contain the & character.

Step 2: Connect and fetch history (on every launch)

connect.js
import { io } from "socket.io-client";

const socket = io("https://ws.cxperium.com/custom", {
  transports: ["websocket"],
  auth: { channelKey: "ch_xxxxxxxx", user: contactId, token },
});

socket.on("connect", async () => {
  // ORDER MATTERS: connect first, THEN fetch history — the reverse loses messages.
  // If you store messages locally, after = last seen seq; otherwise 0.
  let after = stored("lastSeq") || 0;
  for (;;) {
    const h = await fetch(
      `https://provider.cxperium.com/custom/ch_xxxxxxxx/messages?user=${contactId}&after=${after}&limit=50`,
      { headers: { Authorization: `Bearer ${token}` } },
    ).then((r) => r.json());
    for (const m of h.messages) render(m);
    if (!h.hasMore) break;
    after = h.cursor;   // advance from the page's cursor, not from your own counter
  }
});

History returns both directions: direction: "in" is the user's own message (draw on the right), "out" is a bot or agent reply (draw on the left). seq is a per-user increasing sequence number.

Step 3: Send, receive and acknowledge

chat.js
// Send — id is the replay guard: on a network error resend with the SAME id
socket.emit("message.send",
  { user: { name: "Jane" }, message: { id: uniqueId(), type: "text", text: "hello" } },
  (ack) => { /* ack = { status: 202, messageId: "…" } */ });

// Receive — m = { id, seq, ts, type, text, choices, media, link, form }
socket.on("message.created", (m) => {
  if (seen.has(m.id)) return;   // the same message can arrive twice
  seen.add(m.id);
  render(m);
  save("lastSeq", m.seq);
  socket.emit("message.ack", { seq: m.seq }, () => {});   // everything up to seq counts as delivered
});

If the WebSocket is down, the same bodies can be sent over REST: POST /custom/<KEY>/messages and POST /custom/<KEY>/ack with the Authorization: Bearer <token> header. socket.io reconnects on its own; when connect fires again, run the history fetch from Step 2 once more and any messages missed in between arrive.

  • Persist contactId and token — they must survive the app being closed.
  • WebSocket first, then history — the reverse order misses messages that arrive in between.
  • Deduplicate messages by id — the system guarantees at-least-once delivery, not exactly-once.
  • When paging, advance after from the response cursor — not from a counter you keep yourself.
  • Don't panic on disconnects — fetch history after reconnecting; no message is lost.

If you have a server: signature (path B)

The app talks to your server, and your server talks to Cxperium. The secret never reaches the app. Every request carries an X-Cxperium-Signature: t=<unix seconds>,v1=<hex> header where v1 = HMAC-SHA256(secret, "<t>.<content>"). The content is the raw JSON body for POST requests, and the string "<channelKey>:<userId>" for the WebSocket connection and the history query. A signature is valid for 300 seconds; generate a fresh one for every request.

sign.js
const crypto = require("crypto");
function sign(content) {
  const t = Math.floor(Date.now() / 1000);
  const v1 = crypto.createHmac("sha256", CHANNEL_SECRET).update(`${t}.${content}`).digest("hex");
  return `t=${t},v1=${v1}`;
}

// For WebSocket, auth must be a FUNCTION, not a static object — the signature is refreshed on reconnect
const socket = io("https://ws.cxperium.com/custom", {
  transports: ["websocket"],
  auth: (cb) => cb({ channelKey: CHANNEL_KEY, user: userId, signature: sign(`${CHANNEL_KEY}:${userId}`) }),
});
Do not re-stringify the POST body; if the key order changes, the signature will not match. Sign exactly the text you send.

Message Types and Forms

The same message body is used in the WebSocket message.created event, in the history query and in the delivery webhook. If a type you don't recognise arrives, show the text field; new types are added over time and text is always populated.

What you can send from the user to the bot

TypeBodyNote
text{ "type": "text", "text": "hello" }Plain text.
choice{ "type": "choice", "choice": { "id": "opt_yes", "title": "Yes" } }Clicking a button offered by the bot.
media{ "type": "media", "media": { "kind": "image", "url": "https://…", "mime": "image/jpeg", "caption": "…" } }Image or file; the URL must be publicly reachable.
form{ "type": "form", "form": { "token": "…", "name": "signup_form", "version": 3, "response": { … } } }The answer to a form the bot sent; token is echoed from the incoming form.

The location type is not supported yet; sending it returns 202 but it is not processed.

What can arrive from the bot

TypeFieldsHow to render
texttextPlain bubble.
choicestext, choices[] (id, title)Buttons under the text; when clicked, send a choice reply.
mediamedia.kind, url, mime, caption, filenameImage, video or file card.
linktext, link.url, link.labelA link button; clicking sends nothing to the chat, it opens the address. The address is also the last line of text; if you render the button, drop that line.
formtext, form.screens, form.token, form.screen, form.dataSee below.

Forms (Flow)

The bot can send forms to the custom channel with the same code it uses for WhatsApp Flows. The form definition arrives inside the message as form.screens; the screens are in Meta Flow JSON format (TextInput, Dropdown, RadioButtonsGroup, CheckboxGroup, DatePicker, Footer). Start with form.screen; form.data is the data of the first screen. If you cannot render forms, at least show the text field.

  • Answer: if the Footer action of the last screen is complete, send the filled fields as a normal message of type form; token is required.
  • Requesting a screen from the server: if the Footer action is data_exchange, call POST /custom/<KEY>/flows/exchange; the response is { "version": "3.0", "screen": "…", "data": { … } }. If screen is SUCCESS, close the form and send the data.extension_message_response.params object as the answer.
  • Ready-made implementation: the web widget implements this whole section; a WebView integration needs no extra work.

Troubleshooting

The most common situations and their fixes.

Web widget

SituationExplanation / fix
I reopened the designer and my settings are goneThe draft is kept only in the same browser. The code on your site is unchanged; that is the real record. Re-enter the values from the code into the designer.
The change does not show on the siteMake sure you pasted the new code and removed the old one; reload without cache (Ctrl + F5).
The widget looks default blueThe code may have been copied from the channel detail; that code always gives the default look. The customised code is copied from the designer.
There is no "Customize appearance" buttonThe button is shown only for Web channels. Mobile channels use the WebView Customizer.
Avatar / icon not showingThe address must be complete (https://…) and publicly accessible.
Console says [cxperium] channelKey ve secret zorunlu ("channelKey and secret are required")channelKey or secret in the install code is empty. Copy the code again from the channel detail.

Custom channel (mobile)

ErrorCauseFix
401 Invalid channel secretThe apiKey header is missing or wrong at registrationCompare with the secret in the dashboard.
401 Invalid token / WS INVALID_TOKENThe token is used with another identityEach user must use their own token; re-register if needed.
401 Invalid signature / WS INVALID_SIGNATUREThe body was re-stringified or the signature is older than 300 sSign the raw text; regenerate on every request.
WS UNAUTHORIZEDChannel missing, inactive or deletedCheck the channel key and the channel's status in the dashboard.
WS INVALID_AUTHNeither token nor signature givenFix the connection's auth field.
WS AUTH_UNAVAILABLETemporary server issueWait and retry the same way.
WS connects at first, then keeps rejectingOn path B, auth was given as a static objectauth must be a function; the signature is refreshed on every connect.
Messages occasionally go missingNo history fetch (gap-fill)Collect with after=<last seq> on connect.
The same message appears twiceNo deduplication by idDeduplicate messages by id.
400 FLOW_KEY_MISSINGNo bot form key defined on the channelEnter the form key from Channels → Edit.
502 BOT_UNREACHABLE / BOT_ERRORThe bot did not reply within 10 s or returned an errorShow "try again"; check the bot address in the dashboard.
The bot never repliesChannel inactive or bot downContact our support team with the channel name.

Couldn't find what you're looking for?

Let our technical team guide you through your integration — send us your question and we'll get back to you the same day.

Dedicated technical support · Free integration consulting