Published and unpublished packages
@botmesh/tiktok is implemented and tested but private: it is not on npm and
its API may change. Release status, dist-tags and install commands for every
package are in Packages and versions.
Choosing a channel
WhatsApp has several options:@botmesh/baileys: links the bot as a WhatsApp Web device (QR or pairing code) and opens its own socket. No public server or webhook, groups and polls work, no per-message cost. It is an unofficial client: automating an account can break WhatsApp’s terms and get the number banned. Keep the rate limiter on.@botmesh/meta: the official WhatsApp Cloud API. Native buttons, lists, templates and WhatsApp Flows forms, no ban risk. Needs a public HTTPS webhook, has no groups, and free-form messages only work inside the 24-hour customer-service window.@botmesh/evolution/@botmesh/wppconnect: you already run an Evolution API or WPPConnect Server. The bot talks to that gateway over REST and receives its webhooks. Both gateways drive WhatsApp Web, so the same caveats as Baileys apply (unofficial, no reliable native buttons). The shared pieces (REST client, media, LID/PN reconciliation) live in@botmesh/wa-gateway-kit, a public dependency of both; you install the channel, not the kit (unless you write a transport for another gateway).@botmesh/twilio/@botmesh/gupshup: you send WhatsApp through that provider’s account. Templates are supported; Twilio also covers SMS/MMS with the same class.@botmesh/gohighlevel: the bot answers HighLevel CRM contacts on the channel they used (SMS, WhatsApp, Email, Facebook, Instagram, Live chat…).
@botmesh/telegram (long polling by default, webhook
optional), @botmesh/discord (gateway socket), @botmesh/messenger and
@botmesh/instagram (Meta webhooks), @botmesh/email (each email thread is a
chat) and @botmesh/tiktok (comments on your own videos and DMs).
Socket, polling or webhook
- Socket / polling (the provider opens the connection): Baileys, Discord, Telegram in polling mode, Email (IMAP IDLE). Nothing to expose to the internet.
- Webhook (
capabilities.pushIngress: true): Cloud API, Messenger, Instagram, Telegram in webhook mode, Evolution, WPPConnect, Twilio, HighLevel, Gupshup, TikTok. Your HTTP server receives the POST and hands the raw body plus the headers toprovider.ingest(), which verifies the signature or token and returns the HTTP status to answer.@botmesh/serverdoes that wiring (raw body, size limits, timeouts, concurrency cap, health endpoints) fornode:http, Express, Fastify and Hono.
@botmesh/server (pnpm add @botmesh/server): one
route per provider, and the GET handshake and the signed POSTs are
handled for you:
ingest() returns:
GET handshake, Telegram has none, and the providers whose
authentication covers the URL take it as well (WPPConnect’s async
ingest(body, headers, url), ingestRequest({ url, body, headers }) on
Twilio and Gupshup).
ingest() does not wait for your handlers, so the platform gets its answer
right away.
The constructor of every webhook provider (Telegram only in webhook mode)
throws when its webhook credential (app secret, auth token or shared
token) is missing, unless you pass allowUnsignedWebhooks: true, which is
meant only for isolated local development. HighLevel is the exception: its
signing public key is bundled.
Capability matrix
Values come from each provider’scapabilities declaration. A feature
marked “no” is degraded, not broken: buttons and lists become a numbered
text menu, ctx.edit sends a new message, ctx.react resolves
{ skipped: true }, ctx.typing is a no-op (see
Messages & media).
Content. What each channel can send natively.
Ingress and presence. How events arrive and which presence hints work.
- Send reactions only describes sending. Several providers still
receive reactions as
reactionevents (Messenger, Instagram, Gupshup, TikTok…). ctx.stream:draft= native draft previews then one final message (Telegram private chats; edits elsewhere),edit= one message edited as the text grows,chunks= no live preview, sentence-sized messages with typing (streaming: 'none'). See Streaming replies.- Forms:
nativesendsflow().form()as a WhatsApp Flow;field by fieldasks each field with text messages. textFormat(not a column): the text markup the channel renders, so Markdown producers such as@botmesh/aican convert it.'whatsapp'(*bold*,_italic_,~strike~): Baileys, Cloud API, Evolution, WPPConnect, Gupshup.'plain'(no markup): Telegram (sent withoutparse_mode), Messenger, Instagram, TikTok.'markdown': Discord. Not declared: Email. Providers that serve several channels decide per chat with the optionaltextFormatFor(chatId), which wins overcapabilities.textFormatand reaches handlers asctx.textFormat: Twilio returns'whatsapp'forwhatsapp:chats and'plain'for SMS; HighLevel follows the contact’s reply channel ('whatsapp'for WhatsApp,'plain'for SMS, Email, FB, IG, Live chat, GMB and custom).- Templates is the
templatecontent kind: Cloud API templates, Twilio Content Templates (ContentSid), HighLeveltemplateId, Gupshup templates. Other providers reject it withUnsupportedContentError. - Every provider except Baileys accepts the
nativecontent kind (ctx.sendNative('<provider id>', payload)) as an escape hatch to the channel’s own API, with a provider-specific payload. Baileys rejects it withUnsupportedContentError. Discord, Messenger, Instagram, Evolution, WPPConnect, Twilio, Gupshup, HighLevel, Email and TikTok also reject a payload addressed to another provider id instead of posting it.
Cross-channel contract
The same rules hold on every channel, so a bot behaves the same wherever it runs. Provider authors check them with the helpers of@botmesh/core/testing
(see Testing).
- Timestamps are epoch milliseconds.
ctx.timestamp,InboundMessage.timestamp,InboundInteraction.timestampandDeliveryStatus.timestampare epoch ms on every channel. Transports that report seconds (WhatsApp, Telegram, the Cloud API) are multiplied by 1000; one that reports no time (Twilio) uses the receive time, never0. Telegram sends no press time for a button, so a callback query carries the receive time. - Long text is split. Each channel declares its text limit and the core splits a
longer
textsend into several messages (see Messages;splitLongText: falseturns it off). - Unsupported operations are absent. An operation the transport cannot perform is
not implemented: the optional method (
typing,markRead,editMessage,deleteMessage) is missing and its capability flag isfalse, soctx.typing(),ctx.markRead()andctx.delete()do nothing. A method that exists performs the operation or rejects; it is never a silent stand-in. - Presence hints are best-effort while disconnected.
typing()andmarkRead()resolve without contacting the transport while the provider’s last connection state is not'ready'(beforestart()connected, while reconnecting, afterstop()): no error, no queue, no later retry. The bot applies the same rule before calling them, logs the skipped hint at debug and counts it inmetrics().presenceSkipped(botmesh_presence_skipped_totalintoPrometheus()). Once connected, a transport failure still rejects.assertPresenceWhileDisconnected()checks a provider. - Too many options reject. A transport that shows fewer buttons, rows or poll
answers than you sent rejects with
UnsupportedContentError(Cloud API, Messenger, Instagram, Discord), unless it has a documented degradation (numbered text on Baileys, Evolution, WPPConnect, Gupshup, TikTok). - A local media path that cannot be read rejects with
UnsupportedContentError(the fs error ascause), never a rawENOENT. - Waits are bounded. Every REST call has a timeout (
requestTimeoutMson the Cloud API, Telegram, Discord, Messenger and Instagram), and aRateLimitedErrornever asks for more than 24 h. - An edit keeps the original id. Where the transport reports text edits (Baileys,
Telegram
edited_message, DiscordMESSAGE_UPDATE), the edit arrives as a message with the new text and the originalref.id; each of those packages exportsisEditedMessage(ctx)and aneditedMessages: 'ignore'option. The core dedupe lets an edit through because its content changed, and drops a redelivery of the same edit. - Poll votes have one shape. Baileys and Telegram emit each vote as a
'poll'event (bot.on('poll', (vote) => …)):vote.refis the poll message,vote.senderIdthe voter andvote.optionsthe current tally of every option with itsvoters. - One naming scheme for low-level helpers. Each provider’s error mapper is
to<Channel>Error(toTelegramError,toDiscordError,toTwilioError…; the Meta channels sharetoGraphErrorfrom@botmesh/graph-core). Telegram, TikTok and the Cloud API keep their oldertoProviderError(and Email itstoEmailProviderError) as deprecated aliases; the channels first published in the beta have only the new names.ingest()returns the HTTP status to answer with (anumber); WPPConnect returns it as aPromise<number>(it resolves unknown LIDs through the gateway before emitting, never waiting for your handlers) and email’singest()takes a raw message, not a webhook, and returns anEmailIngestResult.
Providers
Each channel has its own page with setup, options and limits.@botmesh/baileys
WhatsApp Web through Baileys: QR or pairing-code login, own socket, groups and polls. See Baileys (WhatsApp Web).
@botmesh/meta (WhatsApp Cloud API)
The official WhatsApp Cloud API: native buttons, lists, templates and WhatsApp Flows forms over webhooks. See WhatsApp Cloud API.
@botmesh/telegram
Telegram Bot API, by long polling or webhook. See Telegram.
@botmesh/messenger
Facebook Messenger through the Graph Send API and Meta webhooks. See Messenger.
@botmesh/instagram
Instagram DMs of a professional account (Messenger API for Instagram). See Instagram.
@botmesh/discord
Discord gateway (WebSocket) for events plus REST for sending. See Discord.
@botmesh/evolution
Evolution API v2, a self-hosted WhatsApp gateway: REST out, instance webhook in. See Evolution API.
@botmesh/wppconnect
WPPConnect Server, a self-hosted WhatsApp Web gateway: REST with a Bearer token, webhooks with a URL token. See WPPConnect.
@botmesh/twilio
Twilio Programmable Messaging: WhatsApp and SMS/MMS with one class. See Twilio.
@botmesh/gohighlevel
HighLevel (LeadConnector) Conversations API: answers CRM contacts on the channel they used. See HighLevel (GoHighLevel).
@botmesh/gupshup
Gupshup WhatsApp API, with callbacks authenticated by a shared token. See Gupshup.
@botmesh/email
Email over IMAP IDLE and SMTP: each email thread is a chat. See Email.
@botmesh/tiktok (not on npm)
TikTok API for Business: comments on your own videos and direct messages. See TikTok.
Upgrading from a 2.0 alpha
New bots can skip this section. If you ran a bot on an early 2.0 alpha, the cross-channel contract changed some behaviour; the per-package details are in Upgrade notes: channels & plugins and Migrating to the stable API.What changed on every channel
What changed on every channel
ctx.timestampis epoch milliseconds everywhere. Baileys, the Cloud API andMockProviderused to report epoch seconds. Drop any* 1000(new Date(ctx.timestamp)is now right). The same goes forInboundMessage,InboundInteractionandDeliveryStatustimestamps in custom hooks. A third-party provider that still emits seconds keeps working: the core converts a value below1e12forctx.timestampand the message log, but update it to emit ms (assertInboundContractflags it).- Telegram button presses carry the receive time, not the date of the message that holds the keyboard.
- No-op methods were removed:
CloudAPIProvider#typing()and#deleteMessage(),TelegramProvider#markRead()andDiscordProvider#markRead(). They did nothing; usectx.typing()/ctx.delete()/ctx.markRead()(no-ops there) orprovider.typing?.(…). - Long text is split into several messages instead of being rejected with
PayloadTooLargeError.new Bot({ splitLongText: false })restores the old behaviour.before('send')hooks still run once per message (on the whole text, before the split);after('send')hooks run once per part. - Too many options reject on Discord (over 25 buttons or menu options, over 10 poll
answers), Messenger (over 3 buttons, over 10 list rows) and Instagram (over 13 quick
replies) with
UnsupportedContentError; they used to drop the extra options silently. - Discord defers unanswered interactions after 2 s.
autoAcknowledgeMs: falserestores the old behaviour. A latectx.acknowledge('text')then goes out as a followup; with noappIdand no application id received from Discord it rejects. MockProviderreportscapabilities.typingandreadReceiptsasfalseunless atyping()/markRead()method exists (they had no effect before either), and emits epoch-ms timestamps.RateLimitedError.retryAfterMsis capped at 24 h.- Telegram polls are sent non-anonymous so their votes reach the bot as
'poll'events.new TelegramProvider({ anonymousPolls: true })keeps anonymous polls. - Discord edits are delivered as messages with the original
ref.id; a handler that should not see them checksisEditedMessage(ctx)or setseditedMessages: 'ignore'. - Renamed helpers (the old names are deprecated aliases until v3): Telegram
pollTimeout(seconds) →pollTimeoutMs;toProviderError→toTelegramError,toTikTokError;toEmailProviderError→toEmailError;@botmesh/meta’stoProviderError→toGraphErrorfrom@botmesh/graph-core. Discord, Messenger and Instagram dropped their alphatoProviderErrorbefore their first npm release: usetoDiscordError,toMessengerErrorandtoInstagramError.
Next steps
- Messages & media: what each helper does on each transport.
- Platform sandbox: run a bot on the real providers against local simulators, without accounts.
- Webhook server: mount webhook providers with
@botmesh/server. - 24-hour window and pricing: Cloud API session window and usage tracking.
- Forms: native WhatsApp Flows on the Cloud API, field by field elsewhere.