Unified messaging integrations for Node.js applications
Features • Packages • Quick Start • Channels • Documentation
- Multi-provider support - Unipile (LinkedIn / WhatsApp / Email / …), Uazapi (WhatsApp BR), Meta Cloud API (official WhatsApp), Twilio (SMS / MMS / WhatsApp), Zernio (social publishing + inbox + ads across 15 channels)
- Normalized events - Consistent event format across all messaging providers
- Webhook handling - Built-in parsing, validation, and queue management
- Channel agnostic - LinkedIn, WhatsApp, Instagram, Telegram, SMS, Email
- Production ready - Battle-tested in high-volume B2B applications
- Zero-config install - Two runtime dependencies (
axios,form-data); everything else is an optional peer
| Package | Version | Description |
|---|---|---|
| @brainhours/relay-core | 1.10.0 | Core messaging integrations (Unipile + Uazapi + Meta Cloud API + Wati + Twilio + Zernio) |
npm install @brainhours/relay-coreNo registry setup or authentication needed — the package is published publicly on npmjs.com.
# .env
UNIPILE_DSN=api1.unipile.com:13111
UNIPILE_ACCESS_TOKEN=your_token_hereconst { UnipileProvider } = require('@brainhours/relay-core');
require('dotenv').config();
const provider = new UnipileProvider({
dsn: process.env.UNIPILE_DSN,
accessToken: process.env.UNIPILE_ACCESS_TOKEN
});
// Send a message
await provider.messaging.sendMessage({
account_id: 'account_id',
chat_id: 'chat_id',
text: 'Hello from Relay!'
});const { parseWebhook, EventTypes } = require('@brainhours/relay-core');
app.post('/webhooks/unipile', (req, res) => {
const event = parseWebhook('unipile', req.body);
if (event.type === EventTypes.MESSAGE_RECEIVED) {
console.log('New message:', event.content);
}
res.status(200).send('OK');
});| Channel | Provider | Status |
|---|---|---|
| Unipile | Stable | |
| Unipile | Stable | |
| WhatsApp (BR API) | Uazapi | Stable (v1.8.0+) |
| WhatsApp (Meta official) | Cloud API | Stable (v1.10.0+) |
| Unipile | Stable | |
| Telegram | Unipile | Stable |
| Messenger | Unipile | Stable |
| Unipile | Stable | |
| SMS / MMS | Twilio | Stable (v1.18.0+) |
| WhatsApp (Twilio) | Twilio | Stable (v1.18.0+) |
| Social publishing (15 channels) | Zernio | Stable (v1.21.0+) |
| Social inbox / DMs (IG / FB / WhatsApp / Telegram / X / Reddit / Bluesky) | Zernio | Stable (v1.21.0+) |
| WhatsApp (Meta official, Embedded Signup) | Zernio | Stable (v1.21.0+) |
| Comments / reviews / ads | Zernio | Stable (v1.21.0+) |
const express = require('express');
const {
MetaCloudApiProvider,
parseCloudApiWebhook,
validateCloudApiSignature,
MessagingEventEmitter,
EventTypes
} = require('@brainhours/relay-core');
const meta = new MetaCloudApiProvider({
apiVersion: 'v22.0',
appSecret: process.env.META_APP_SECRET // for HMAC validation
});
const emitter = new MessagingEventEmitter();
emitter.on(EventTypes.MESSAGE_RECEIVED, (e) => {
if (e.provider !== 'cloud-api') return;
console.log(e.senderName, ':', e.content);
});
emitter.on(EventTypes.TEMPLATE_STATUS_CHANGED, (e) => {
console.log('Template', e.metadata.templateName, '->', e.metadata.newStatus);
});
const app = express();
// Capture raw body for HMAC validation:
app.use('/webhooks/meta', express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
app.post('/webhooks/meta', (req, res) => {
const ok = validateCloudApiSignature(
req.rawBody, req.headers['x-hub-signature-256'], process.env.META_APP_SECRET
);
if (!ok) return res.sendStatus(401);
// parseCloudApiWebhook returns ARRAY (Cloud API batches up to 100 events per POST)
for (const event of parseCloudApiWebhook(req.body)) {
emitter.emit(event);
}
res.sendStatus(200);
});
// Send a template (per-call credentials -> multi-tenant ready)
const creds = await db.loadCredsForTenant(tenantId);
await meta.messaging.sendTemplate({
accessToken: creds.accessToken,
phoneNumberId: creds.phoneNumberId,
to: '5511999999999',
templateName: 'hello_world',
language: 'en_US',
components: []
});See examples/cloud-api for the full setup.
const { UazapiProvider, parseWebhook } = require('@brainhours/relay-core');
// Multi-server cluster with heterogeneous capacities (or pass a single
// { baseUrl, adminToken } for a single server)
const uazapi = new UazapiProvider({
servers: [
{ id: 'plano-pequeno', baseUrl: 'https://srv1.uazapi.com', adminToken: '...', capacity: 2 },
{ id: 'plano-grande', baseUrl: 'https://srv2.uazapi.com', adminToken: '...', capacity: 10 }
],
selectionStrategy: 'weighted-round-robin',
getServerLoad: async (id) => db.instances.count({ where: { server_id: id } })
});
// Provision a new instance: pool picks a server respecting capacity
const created = await uazapi.instance.create({ name: 'tenant-acme' });
// Returns { id, token, serverId, serverUrl, ... } - persist these in your DB
// Webhooks
await uazapi.webhooks.set({
token: created.token, serverId: created.serverId,
url: 'https://app.com/webhooks/uazapi',
events: ['messages', 'messages_update', 'connection']
});
app.post('/webhooks/uazapi', (req, res) => {
const event = parseWebhook('uazapi', req.body);
// event.provider === 'uazapi', event.providerType === 'WHATSAPP'
// event.type ∈ MESSAGE_RECEIVED|MESSAGE_SENT|MESSAGE_READ|... (refined per data)
res.json({ ok: true });
});const express = require('express');
const {
TwilioProvider,
parseTwilioWebhook,
validateTwilioSignature,
emptyMessagingResponse,
MessagingEventEmitter,
EventTypes
} = require('@brainhours/relay-core');
const twilio = new TwilioProvider({
accountSid: process.env.TWILIO_ACCOUNT_SID,
authToken: process.env.TWILIO_AUTH_TOKEN // also signs inbound webhooks
});
const emitter = new MessagingEventEmitter();
emitter.on(EventTypes.MESSAGE_RECEIVED, (e) => {
if (e.provider !== 'twilio') return;
console.log(`${e.metadata.channel} ${e.senderName || e.senderId}: ${e.content}`);
});
const app = express();
// Twilio webhooks are application/x-www-form-urlencoded (NOT JSON)
app.post('/webhooks/twilio',
express.urlencoded({ extended: false }),
(req, res) => {
const url = `https://${req.headers.host}${req.originalUrl}`;
const ok = validateTwilioSignature(
url, req.body, req.headers['x-twilio-signature'], process.env.TWILIO_AUTH_TOKEN
);
if (!ok) return res.sendStatus(403);
emitter.emit(parseTwilioWebhook(req.body));
res.type('text/xml').send(emptyMessagingResponse());
});
// Send an SMS
await twilio.messaging.sendSms({
to: '+5511999999999',
from: '+12025550123',
body: 'Hello from Relay!'
});
// Send a WhatsApp message (whatsapp: prefix added for you)
await twilio.messaging.sendWhatsApp({
to: '+5511999999999',
from: '+14155238886',
body: 'Olá!'
});See examples/twilio for the full setup.
Zernio is a single-key API covering 15 channels (Twitter/X, Instagram,
Facebook, LinkedIn, TikTok, YouTube, Pinterest, Reddit, Bluesky, Threads,
Google Business, Telegram, Snapchat, WhatsApp, Discord). Unlike the messaging
providers, the tenant boundary is the profileId / accountId you pass per
call — not the credential.
const express = require('express');
const {
ZernioProvider,
parseZernioWebhook,
validateZernioSignature,
MessagingEventEmitter,
EventTypes
} = require('@brainhours/relay-core');
const zernio = new ZernioProvider({
apiKey: process.env.ZERNIO_API_KEY,
webhookSecret: process.env.ZERNIO_WEBHOOK_SECRET
});
// Publish to LinkedIn + Instagram at once
await zernio.posts.create({
content: 'New drop 🚀',
publishNow: true,
platforms: [
{ platform: 'linkedin', accountId: liAccountId },
{ platform: 'instagram', accountId: igAccountId }
]
});
// Connect WhatsApp with Meta Embedded Signup (no manual BM token):
const { authUrl } = await zernio.connect.getConnectUrl({
platform: 'whatsapp',
profileId,
redirectUrl: 'https://app.example.com/callback'
});
// → redirect the client to authUrl; Meta hosts the WABA + number picker.
// Social inbox: reply to a DM
await zernio.messaging.send({
conversationId,
accountId: igAccountId,
message: 'Thanks for reaching out!'
});
// Webhook intake (JSON; capture the raw body for signature verification)
const emitter = new MessagingEventEmitter();
app.use('/webhooks/zernio', express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
app.post('/webhooks/zernio', (req, res) => {
if (!validateZernioSignature(req.rawBody, req.headers['x-zernio-signature'], process.env.ZERNIO_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = parseZernioWebhook(req.body);
if (event && event.type === EventTypes.MESSAGE_RECEIVED) emitter.emit(event);
res.sendStatus(200);
});Managers on the provider instance: posts, media, accounts, connect,
messaging, comments, reviews, whatsapp, analytics, crm, ads,
engagement, webhooks.
- OAuth hosted authentication
- Direct credential connection
- Account status monitoring
- Multi-account support
- Send/receive messages
- File attachments
- Message reactions
- Read receipts
- Chat history
- Profile search (1st, 2nd, 3rd degree)
- Connection requests (invites)
- Full profile data (experiences, education, skills)
- Search parameters autocomplete (locations, industries, job titles, companies)
- Real-time event notifications
- Normalized event format
- Programmatic webhook management
- Account filtering
See the packages/core README for complete API documentation.
- Installation
- Environment Variables
- Account Management
- Messaging
- LinkedIn Search
- Search Parameters
- Webhooks
- Events
- Error Handling
Check out the examples directory:
- Express Webhook Handler - Basic webhook processing
See CHANGELOG.md for version history.
v1.2.0 (2024-12-30)
- Added
searchParamsmanager for LinkedIn autocomplete (locations, industries, job titles, companies)
v1.1.0 (2024-12-29)
- Added
webhooksmanager for programmatic webhook management
v1.0.0 (2024-12-29)
- Initial release with Unipile provider
- Go to Unipile Dashboard
- Create an account or sign in
- Navigate to Settings > API
- Copy your DSN and Access Token
- Node.js >= 18.0.0
- npm or yarn
- Unipile account (for Unipile provider)
MIT - Guilherme Goulart