Skip to content

One bot, four channels: what actually differs between web chat, WhatsApp, Messenger and Telegram

"Put the same bot on WhatsApp and Telegram too" sounds like a configuration change. It is not a hard project either — but the parts that differ are not the parts people expect.

"Put the same bot on WhatsApp and Telegram too" sounds like a configuration change. It is not a hard project either — but the parts that differ are not the parts people expect.

The messaging APIs are the easy half. Each one is a POST with a token. What actually costs you time is that the four channels disagree about who the user is, how long you may speak, what a message can contain, and whether anyone is listening. Build as if those are the same and you get a bot that works beautifully in the browser and behaves strangely everywhere else.

Here is the comparison I wish I had seen before starting, and the shape of code that survives it.

The comparison that matters

Web chat WhatsApp Messenger Telegram
Who the user is Anonymous, per browser Phone number Page-scoped id Telegram user id
Identity survives Until storage is cleared Forever Per page Forever
When you may send Any time 24h window, then templates only 24h window, with tagged exceptions Any time
Costs money per message No Yes No No
Rich formatting Anything you render Limited markdown-ish Plain text + templates HTML or MarkdownV2
Typing indicator Yours to fake Supported Supported Supported
Delivery visible to you Trivially Separate status events Separate status events Send response only
Message length Your choice ~4,096 chars 2,000 chars 4,096 chars

Four rows in that table cause nearly all the work.

1. Identity: the hardest one, and it is not technical

On the web the user is anonymous. On WhatsApp you have a verified phone number. This sounds like a straight upgrade, and it is also a trap: a phone number is personal data, and the same person on two channels is two different users to you.

Decide early what a "contact" is in your system, because it determines whether you can join conversations later:

  • A conversation belongs to one channel and one channel identity. Always.
  • A contact is a person, and may link several channel identities.
  • Linking happens only when the person tells you — they give an email in web chat that matches a contact, they message from a number you already hold. Never infer it.

Do not skip the contact layer because you launched with one channel. Adding it later means backfilling every conversation you ever stored, and guessing at links you no longer have evidence for.

And store the phone number the way personal data should be stored: scoped to the tenant, out of logs, out of analytics events, deletable on request.

2. The reply window is a design constraint, not an API detail

Web chat has no concept of being "allowed" to reply. The other three do, and WhatsApp's is the strictest: a customer's message opens a 24-hour window in which you may send free-form text, and outside it you may only send a pre-approved template. Messenger has its own 24-hour rule with a set of tagged exceptions. Telegram, pleasantly, just lets you message.

Two consequences worth designing for rather than discovering:

Human handover breaks across the window. Your bot escalates at 11pm, an agent replies at 10am — on WhatsApp that reply is outside the window and will be rejected. The whole escalation flow needs a template path, or your agents will send messages that silently never arrive.

Anything proactive is a different feature per channel. An appointment reminder is free text on Telegram, a template with an approval queue on WhatsApp, and a tagged message on Messenger. Build it as one feature and you will build it three times anyway; better to know that up front.

So the send path has a gate, and the gate is per channel:

// Each adapter answers for itself. The core never reasons about windows.
const canSendFreeForm = {
  web:       () => true,
  telegram:  () => true,
  whatsapp:  c => Date.now() - c.lastInboundAt < 24 * 60 * 60 * 1000,
  messenger: c => Date.now() - c.lastInboundAt < 24 * 60 * 60 * 1000
};

Evaluate it immediately before sending, never when queueing — a reply that waited forty minutes in a queue may have been legal when it was created and illegal by the time it goes out.

3. Formatting: write once in a neutral form, render per channel

The answer your model produces is Markdown. Nobody accepts Markdown.

Telegram wants HTML or MarkdownV2 — and MarkdownV2 requires escaping a long list of characters, so an unescaped . or - in an answer returns a parse error and the message never sends. WhatsApp has its own asterisk-and-underscore convention. Messenger is plain text with structured attachments beside it. Web chat can render whatever you like.

The mistake is letting channel formatting leak into the generation prompt — "reply in Telegram HTML" — because then the answer is wrong for every other channel and you cannot reuse it.

Generate neutral, render at the edge:

// The model returns Markdown. Each adapter is the only thing that knows
// what its channel accepts.
const render = {
  web:       md => markdownToHtml(md),
  telegram:  md => markdownToTelegramHtml(md),   // and escape < > &
  whatsapp:  md => markdownToWhatsAppText(md),   // *bold*, _italic_, the rest flattened
  messenger: md => stripToPlainText(md)
};

Two details that bite: links, which web chat renders inline but plain-text channels need spelled out in full, so a bare URL is safer than [click here](...); and length, since a 3,000-character answer is fine on the web, over the limit on Messenger, and unreadable on a phone anywhere. Cap the answer in the prompt and split long ones in the adapter.

4. The adapter boundary that keeps this sane

Everything above points at the same structure. One core that knows nothing about channels, and a small adapter per channel that knows everything:

// Every adapter normalises inbound to the same shape...
// { channel, channelUserId, conversationId, text, attachments, receivedAt }
//
// ...and takes outbound in the same shape:
// { text (markdown), quickReplies?, handoff? }

async function handleInbound(msg) {
  const conversation = await findOrCreateConversation(msg);
  const answer = await generateAnswer(conversation, msg.text);
  const adapter = adapters[msg.channel];

  if (!adapter.canSendFreeForm(conversation)) {
    return adapter.sendTemplate(conversation, 'reopen', { topic: answer.topic });
  }
  return adapter.send(conversation, adapter.render(answer.text));
}

The test for whether you have the boundary right: generateAnswer contains no if (channel === ...). If it does, the channel has leaked into the brain, and every new channel will mean touching it.

Two more things that belong in the adapter and nowhere else: retries and deduplication. WhatsApp and Messenger both redeliver webhooks when you are slow to acknowledge, so every adapter needs to ack fast and drop message ids it has already seen. Telegram's long-polling has its own version of the same problem. The core should never see a duplicate.

5. What each channel is actually good at

Worth being honest about, because "be everywhere" is not a strategy:

  • Web chat is the only one where the visitor is already looking at the thing they want to ask about. The best context, the worst retention — close the tab and they are gone.
  • WhatsApp is where the customer already is, and the conversation persists for years in a thread they will scroll back through. It costs money and it has the strictest rules. Worth it when the customer base lives there.
  • Messenger matters if the business's audience arrived through a Facebook page. If it did not, it is effort for nothing.
  • Telegram is the easiest to build on by a wide margin — generous API, no fees, no windows — and the smallest audience for most businesses. It is a great first non-web channel precisely because it will teach you the adapter boundary cheaply.

If you are adding your first extra channel, add Telegram to learn the shape, then WhatsApp because that is where the customers are.

The short version

The APIs are not the work. The work is identity, the reply window, formatting, and keeping all three out of the part that writes the answer. Get the adapter boundary right and the fourth channel takes an afternoon. Get it wrong and the second one takes a week and the third never ships.


I build ChatNory, which runs one chatbot — trained on a business's own content — across a website, WhatsApp, Messenger and Telegram. The adapter boundary above is the second version; the first had channel checks in the answer generator and it was exactly as pleasant as it sounds.