Chat SDK
Build one chatbot that answers Instagram, Facebook, Telegram, WhatsApp, X, Bluesky and Reddit conversations through the Zernio adapter for Vercel's Chat SDK.
When you finish this page a Chat SDK bot answers messages from Instagram, Facebook, Telegram, WhatsApp, X (platform value twitter), Bluesky and Reddit through one adapter and one webhook endpoint. You need an API key with read-write permission, a connected account and Node.js 20+. Zernio holds the platform app registrations and tokens, so the bot never talks to Meta, X, Reddit or Telegram directly.
@zernio/chat-sdk-adapter is the official Zernio adapter, listed on chat-sdk.dev. Every account includes the inbox; messages the bot sends are metered after the first 10,000 each month (pricing). On a legacy AppSumo plan the inbox stays off until support enables it.
Step 1: Install and configure
Install the adapter
npm install @zernio/chat-sdk-adapter chat @chat-adapter/state-memory@chat-adapter/state-memory keeps state in memory. In production use a persistent state adapter such as @chat-adapter/state-redis or @chat-adapter/state-pg (state adapters).
Set the environment variables
ZERNIO_API_KEY=$ZERNIO_API_KEY
ZERNIO_WEBHOOK_SECRET=$ZERNIO_WEBHOOK_SECRETZERNIO_WEBHOOK_SECRET is the secret you set on the webhook endpoint in Step 3; the adapter verifies every delivery's signature with it.
Step 2: Create the bot and its route
Create the bot
import { Chat } from "chat";
import { createZernioAdapter } from "@zernio/chat-sdk-adapter";
import { createMemoryState } from "@chat-adapter/state-memory";
export const bot = new Chat({
userName: "pizza-bot",
adapters: {
zernio: createZernioAdapter(),
},
state: createMemoryState(),
});
// /.*/ matches every message
bot.onNewMessage(/.*/, async (thread, message) => {
const platform = (message.raw as any).platform;
await thread.post(`Hello from ${platform}`);
});Add the route that receives events
import { bot } from "@/lib/bot";
export async function POST(request: Request) {
return bot.webhooks.zernio(request);
}Step 3: Subscribe Zernio to your route
Create the endpoint in the webhooks dashboard, or call POST /v1/webhooks/settings with name, url, events and the same secret you put in ZERNIO_WEBHOOK_SECRET. Subscribe to message.received and comment.received; add reaction.received if the bot handles reactions, which route to bot.onReaction.
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: created } = await zernio.webhooks.createWebhookSettings({
body: {
name: 'Chat SDK bot',
url: 'https://your-app.com/api/chat-webhook',
events: ['message.received', 'comment.received', 'reaction.received'],
secret: process.env.ZERNIO_WEBHOOK_SECRET
}
});
console.log(created.webhook._id);Response (200):
{
"success": true,
"webhook": {
"_id": "66d9f1a2b3c4d5e6f7a8b9c0",
"name": "Chat SDK bot",
"url": "https://your-app.com/api/chat-webhook",
"events": ["message.received", "comment.received", "reaction.received"],
"isActive": true,
"failureCount": 0
}
}Send the connected account a DM and the bot replies. The adapter reads its settings from the environment; to pass them explicitly:
const adapter = createZernioAdapter({
apiKey: process.env.ZERNIO_API_KEY,
webhookSecret: process.env.ZERNIO_WEBHOOK_SECRET,
baseUrl: "https://zernio.com/api", // default
botName: "My Bot", // default: "Zernio Bot"
});| Environment variable | Config key | Required | Description |
|---|---|---|---|
ZERNIO_API_KEY | apiKey | Yes | API key used to send messages |
ZERNIO_WEBHOOK_SECRET | webhookSecret | Recommended | HMAC-SHA256 secret for webhook verification |
ZERNIO_API_BASE_URL | baseUrl | No | Override the API base URL |
ZERNIO_BOT_NAME | botName | No | Bot display name |
Step 4: Read platform data and reactions
Every message carries the raw Zernio payload in message.raw:
bot.onNewMessage(/.*/, async (thread, message) => {
const raw = message.raw as any;
console.log(raw.platform); // "instagram" | "facebook" | "telegram" | ...
if (raw.sender.instagramProfile) {
console.log(raw.sender.instagramProfile.followerCount);
console.log(raw.sender.instagramProfile.isVerified);
}
if (raw.sender.phoneNumber) {
console.log(raw.sender.phoneNumber); // WhatsApp
}
for (const att of raw.attachments) {
console.log(att.type, att.url);
}
});Reactions (WhatsApp, Telegram, Slack, Instagram, Facebook Messenger) arrive as their own event and route to onReaction, never to onNewMessage, so a 👍 is never handled as an inbound message:
bot.onReaction(async (event) => {
// event.emoji the normalized emoji (event.rawEmoji is the platform value)
// event.added true when added, false when removed
// event.messageId the message that was reacted to
// event.thread the thread where it happened
if (event.added) {
await event.thread.post(`Thanks for the ${event.emoji}`);
}
});What the adapter supports
| Feature | Supported | Notes |
|---|---|---|
| Send messages | Yes | Text on every platform |
| Rich messages (cards) | Yes | Buttons and templates on Facebook, Instagram, Telegram, WhatsApp |
| Edit messages | Partial | Telegram only |
| Delete messages | Partial | Telegram, X (full); Bluesky, Reddit (own messages only) |
| Send reactions | Partial | WhatsApp, Telegram, Slack, Instagram, Facebook Messenger |
Receive reactions (onReaction) | Partial | WhatsApp, Telegram, Slack, Instagram, Facebook Messenger, through the reaction.received event |
| Typing indicators | Partial | Facebook Messenger, Instagram, Telegram, WhatsApp |
| AI streaming | Partial | Live post and edit on Telegram; one full reply on platforms without message editing (WhatsApp, Instagram, Facebook, X, Bluesky, Reddit) |
| File attachments | Yes | Through the media upload endpoint |
| Fetch messages | Yes | Full conversation history, with limit, cursor and direction |
| Fetch thread info | Yes | Participant details, platform, status |
| Webhook verification | Yes | HMAC-SHA256 |
| Comment webhooks | Yes | comment.received routed through handlers |
| Reaction webhooks | Yes | reaction.received routed to onReaction |
For calls the Chat SDK does not cover, the package exports a client for the Zernio API:
import { ZernioApiClient } from "@zernio/chat-sdk-adapter";
const client = new ZernioApiClient(process.env.ZERNIO_API_KEY, "https://zernio.com/api");
const { data, pagination } = await client.listConversations({
platform: "instagram",
status: "active",
limit: 20,
});
await client.sendMessage("66c3d2ae7b4f6c8d0e1f2a3b", {
accountId: "66b2e19d8c3f5a7e9d0b1c2d",
message: "Here is the menu",
attachmentUrl: "https://cdn.example.com/menu.jpg",
attachmentType: "image",
});If it fails
A 401 with the body Invalid signature from the webhook route means the signature check failed: the secret on the webhook endpoint and ZERNIO_WEBHOOK_SECRET differ, or the endpoint has no secret. Set the same value in both places and redeliver the event from the webhooks dashboard.