Skip to main content

Node.js SDK

Integrate PocketPing into your Node.js backend for custom event handling, analytics, and automation.


Installation

# npm
npm install @pocketping/sdk-node

# yarn
yarn add @pocketping/sdk-node

# pnpm
pnpm add @pocketping/sdk-node

Quick Start

Express

const express = require('express');
const http = require('node:http');
const { PocketPing } = require('@pocketping/sdk-node');

const app = express();
app.use(express.json());

// Initialize PocketPing
const pp = new PocketPing({
welcomeMessage: 'Hi! How can we help?',
});

// Mount the PocketPing request handler at /pocketping
// It handles /pocketping/connect, /message, /messages, etc.
app.use('/pocketping', pp.middleware());

// Attach the WebSocket server for real-time streaming (/pocketping/stream)
const server = http.createServer(app);
pp.attachWebSocket(server);

server.listen(3000, () => {
console.log('Server running on port 3000');
});

The middleware() returns a standard Node (req, res, next) handler, so it also works with raw http/Connect servers and any framework that accepts Connect-style middleware.

Next.js (App Router)

middleware() is a Node request handler, so wrap it to adapt the App Router's Web Request/Response. A long-running server (not serverless) is required for the WebSocket stream.

app/api/pocketping/[...path]/route.ts
import { PocketPing } from '@pocketping/sdk-node';

const pp = new PocketPing({
welcomeMessage: 'Hi! How can we help?',
});

// Receive an event from the widget and react to it
pp.onEvent('clicked_pricing', async (event, session) => {
console.log(`Pricing interest from ${session.id}`, event.data);
});

Built-in Bridges

The SDK includes built-in bridges for Telegram, Discord, and Slack. No external libraries required - all communication uses HTTP APIs directly.

import { PocketPing, TelegramBridge, DiscordBridge, SlackBridge } from '@pocketping/sdk-node';

const pp = new PocketPing({ /* ... */ });

// Add Telegram bridge
pp.addBridge(new TelegramBridge(
process.env.TELEGRAM_BOT_TOKEN,
process.env.TELEGRAM_CHAT_ID
));

// Add Discord bridge (bot mode for bidirectional)
pp.addBridge(DiscordBridge.bot(
process.env.DISCORD_BOT_TOKEN,
process.env.DISCORD_CHANNEL_ID
));

// Add Slack bridge (webhook mode for simple notifications)
pp.addBridge(SlackBridge.webhook(
process.env.SLACK_WEBHOOK_URL
));
Multiple bridges

You can add multiple bridges simultaneously. Messages sync across all platforms.

Discord requires long-running server

Discord bot mode uses the Discord Gateway (WebSocket) to receive operator replies. This only works on long-running servers (Express, Fastify, etc.).

Does NOT work with:

  • Vercel Functions
  • AWS Lambda
  • Cloudflare Workers
  • Any serverless environment

For serverless + Discord bidirectional: Use the Bridge Server instead, or use DiscordBridge.webhook(url) (send-only).

Telegram & Slack work with serverless

Telegram and Slack use HTTP webhooks (not WebSocket), so they work fully with serverless environments like Vercel, Lambda, etc.


Configuration

const pp = new PocketPing({
// Optional: welcome message returned to new visitors on connect
welcomeMessage: 'Hi! How can we help?',

// Optional: built-in bridges (Telegram, Discord, Slack)
bridges: [],

// Optional: Custom session storage (see Custom Storage section)
storage: new RedisStorage(),

// Optional: Event handlers (see below)
onNewSession: (session) => {},
onMessage: (message, session) => {},
onEvent: (event, session) => {},
onIdentify: (session) => {},
});

Configuration Options

OptionTypeRequiredDescription
storageStorage | 'memory'NoCustom session/message storage (defaults to in-memory)
bridgesBridge[]NoBuilt-in bridges (Telegram/Discord/Slack)
welcomeMessagestringNoMessage returned to new visitors on connect
onNewSessionfunctionNoCalled when a new session starts
onMessagefunctionNoCalled on each message
onEventfunctionNoCalled on custom events from the widget
onIdentifyfunctionNoCalled when a visitor is identified
onVisitorDisconnectfunctionNoCalled when a visitor disconnects
webhookUrlstringNoURL to forward custom events (Zapier, Make, n8n, etc.)
webhookSecretstringNoSecret for HMAC-SHA256 signature verification
webhookTimeoutnumberNoWebhook request timeout in ms (default: 5000)
aiAIConfigNoAI fallback provider (see AI Fallback)
aiTakeoverDelaynumberNoSeconds offline before AI takes over (default: 300)

Webhook Forwarding

Forward custom events to external services like Zapier, Make, n8n, or your own backend for automation.

Basic Setup

const pp = new PocketPing({
// Forward all custom events to your webhook
webhookUrl: 'https://hooks.zapier.com/hooks/catch/123456/abcdef',
});

With HMAC Signature

For security, add a webhook secret to verify requests:

const pp = new PocketPing({
webhookUrl: 'https://your-backend.com/pocketping/events',
webhookSecret: process.env.WEBHOOK_SECRET, // e.g., 'whsec_xxx'
webhookTimeout: 10000, // 10 seconds (default: 5000)
});

Webhook Payload

Every event is sent as a POST request with this JSON body:

{
"event": {
"name": "clicked_pricing",
"data": { "plan": "pro", "source": "homepage" },
"timestamp": "2026-01-21T12:00:00.000Z",
"sessionId": "sess_abc123"
},
"session": {
"id": "sess_abc123",
"visitorId": "visitor_xyz",
"metadata": {
"url": "https://example.com/pricing",
"country": "France",
"browser": "Chrome",
"deviceType": "desktop"
}
},
"sentAt": "2026-01-21T12:00:00.123Z"
}

Verifying Signatures

If webhookSecret is set, requests include X-PocketPing-Signature header:

// Your webhook endpoint
app.post('/pocketping/events', (req, res) => {
const signature = req.headers['x-pocketping-signature'];
const body = JSON.stringify(req.body);

// Verify signature
const crypto = require('crypto');
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(body)
.digest('hex');

if (signature !== expected) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Process the event
const { event, session } = req.body;
console.log(`Event: ${event.name} from ${session.metadata.country}`);

res.json({ ok: true });
});

Use Cases

IntegrationExample
ZapierCreate CRM lead when form_submitted
Make (Integromat)Send Slack message when clicked_pricing
n8nUpdate Airtable when started_trial
Custom BackendLog analytics, trigger emails, sync to data warehouse

Custom Events

The most powerful feature of the SDK. Handle events from the widget and send events back.

Event Flow Overview

Receiving Events from Widget

Handle events sent by PocketPing.trigger() in the widget:

const pp = new PocketPing({
onEvent: async (event, session) => {
console.log(`Event: ${event.name}`, event.data);
console.log(`From session: ${session.id}`);

// Example: Track in analytics
analytics.track(event.name, {
...event.data,
sessionId: session.id,
visitorId: session.visitorId,
});

// Example: Trigger automation based on event
if (event.name === 'clicked_pricing') {
// Wait 5 seconds, then send an operator message
setTimeout(() => {
pp.sendOperatorMessage(
session.id,
"I see you're checking out our pricing! Want help choosing a plan?"
);
}, 5000);
}

// Example: Notify team in Slack
if (event.name === 'requested_demo') {
await notifySlack(`New demo request from ${session.identity?.email}`);
}
},
});

Event Object Structure

interface CustomEvent {
name: string; // Event name (e.g., 'clicked_pricing')
data?: Record<string, unknown>; // Event payload
timestamp: string; // ISO timestamp
sessionId?: string; // Session that triggered it
}

interface Session {
id: string;
visitorId: string;
createdAt: Date;
lastActivity: Date;
operatorOnline: boolean;
aiActive: boolean;
metadata?: {
url?: string;
country?: string;
browser?: string;
deviceType?: 'desktop' | 'mobile' | 'tablet';
// ...plus other page/client/geo fields
};
identity?: {
id: string; // set after PocketPing.identify()
email?: string;
name?: string;
[key: string]: unknown;
};
}

Sending Events to Widget

Use emitEvent() to send events the widget can react to:

// Show a discount popup
await pp.emitEvent(sessionId, 'show_discount', {
percent: 20,
code: 'SAVE20',
message: 'Special offer just for you!',
});

// Open the chat widget programmatically
await pp.emitEvent(sessionId, 'open_chat');

// Highlight a UI element
await pp.emitEvent(sessionId, 'highlight', {
selector: '#pricing-section',
message: 'Check out our new pricing!',
});

// Show a notification
await pp.emitEvent(sessionId, 'notification', {
title: 'New feature!',
body: 'We just launched dark mode.',
});

The widget listens for these with PocketPing.onEvent():

// In the browser
PocketPing.onEvent('show_discount', (data) => {
showDiscountModal(data);
});

Session & Message APIs

Working with Sessions

// Get a specific session
const session = await pp.getSession('sess_abc123');
if (session) {
console.log(`Session from ${session.metadata?.country}`);
}

// Get session messages (paginated; returns { messages, hasMore })
const { messages, hasMore } = await pp.handleGetMessages({
sessionId: 'sess_abc123',
limit: 50, // optional, max 100
// after: 'msg_id' // optional cursor for pagination
});
messages.forEach((msg) => {
console.log(`[${msg.sender}] ${msg.content}`);
});
Sessions live in your storage

The SDK does not keep an in-memory list of "all sessions". To enumerate conversations, query your Storage implementation directly (e.g. via pp.getStorage() or your own DB), or track sessions in onNewSession.

Sending Messages

Send a reply as the operator. sendOperatorMessage(sessionId, content) persists the message, broadcasts it to the widget over WebSocket, and syncs it to your bridges (Telegram/Discord/Slack).

// Send an operator message to a session
await pp.sendOperatorMessage(
'sess_abc123',
'Hello from the backend!'
);

For lower-level control you can also call handleMessage() directly with a full SendMessageRequest ({ sessionId, content, sender }).

Identifying Visitors

Enrich session data with visitor information so operators can see who they're talking to:

// After user logs in on your site
await pp.handleIdentify({
sessionId: 'sess_abc123',
identity: {
id: 'user_12345', // Required - unique user identifier
email: '[email protected]',
name: 'John Doe',
// Any custom properties
plan: 'pro',
company: 'Acme Inc',
mrr: 99,
},
});

Required field: identity.id must be a non-empty string (typically your user's database ID).

You can also pass identity during connection:

const session = await pp.handleConnect({
visitorId: 'visitor-xyz',
identity: {
id: 'user_12345',
email: '[email protected]',
name: 'John Doe',
},
});

This data appears in your messaging platform (Telegram/Discord/Slack) and enables personalized automation.


Event Handlers

onNewSession

Called when a new conversation starts:

const pp = new PocketPing({
onNewSession: async (session) => {
console.log(`New session: ${session.id}`);
console.log(`From: ${session.metadata?.country}`);
console.log(`Page: ${session.metadata?.url}`);

// Log to analytics
analytics.track('chat_started', {
sessionId: session.id,
page: session.metadata?.url,
});

// Send welcome based on page
if (session.metadata?.url?.includes('/pricing')) {
await pp.sendOperatorMessage(
session.id,
"Hi! Looking at our pricing? I'd love to help you find the right plan."
);
}
},
});

onMessage

Called on every message (visitor, operator, and AI):

const pp = new PocketPing({
onMessage: async (message, session) => {
console.log(`[${message.sender}] ${message.content}`);

// Log all messages for compliance
await logMessage({
sessionId: session.id,
content: message.content,
sender: message.sender, // 'visitor' | 'operator' | 'ai'
timestamp: message.timestamp,
});

// Keyword detection on visitor messages
if (message.sender === 'visitor') {
const lowerContent = message.content.toLowerCase();

if (lowerContent.includes('urgent') || lowerContent.includes('emergency')) {
await notifyTeam('Urgent message received!', session.id);
}

if (lowerContent.includes('cancel') || lowerContent.includes('refund')) {
await pp.sendOperatorMessage(
session.id,
"I'll connect you with our billing team right away."
);
await escalateToSupport(session.id);
}
}
},
});

Complete Example

const express = require('express');
const http = require('node:http');
const { PocketPing } = require('@pocketping/sdk-node');

const app = express();
app.use(express.json());

const pp = new PocketPing({
welcomeMessage: 'Hi! How can we help?',

// New session started
onNewSession: async (session) => {
console.log(`New chat from ${session.metadata?.country}`);

// Track in analytics
analytics.track('chat_started', {
sessionId: session.id,
country: session.metadata?.country,
page: session.metadata?.url,
});
},

// Message received
onMessage: async (message, session) => {
if (message.sender === 'visitor') {
analytics.track('message_received', {
sessionId: session.id,
wordCount: message.content.split(' ').length,
});
}
},

// Custom events from widget
onEvent: async (event, session) => {
console.log(`Event: ${event.name}`, event.data);

switch (event.name) {
case 'clicked_pricing':
// Track high-intent action
analytics.track('pricing_interest', {
plan: event.data.plan,
sessionId: session.id,
});

// If they look at enterprise, notify sales
if (event.data.plan === 'enterprise') {
await notifySales(session);
}
break;

case 'started_trial':
// Update CRM
await crm.updateLead(session.visitorId, {
status: 'trial',
trialStarted: new Date(),
});
break;

case 'form_submitted':
// Send confirmation
await pp.emitEvent(session.id, 'show_toast', {
message: 'Thanks! We received your submission.',
});
break;
}
},
});

// Mount middleware + WebSocket stream
app.use('/pocketping', pp.middleware());
const server = http.createServer(app);
pp.attachWebSocket(server);

// Your other routes
app.get('/api/support/sessions/:id', async (req, res) => {
const session = await pp.getSession(req.params.id);
res.json({ session });
});

app.post('/api/support/sessions/:id/message', async (req, res) => {
await pp.sendOperatorMessage(req.params.id, req.body.message);
res.json({ success: true });
});

server.listen(3000);

Custom Storage

By default, the SDK uses in-memory storage. For production, implement the Storage interface:

import { Storage, Session, Message } from '@pocketping/sdk-node';
import Redis from 'ioredis';

class RedisStorage implements Storage {
constructor(private redis: Redis) {}

async createSession(session: Session): Promise<void> {
await this.redis.set(
`pocketping:session:${session.id}`,
JSON.stringify(session)
);
}

async getSession(sessionId: string): Promise<Session | null> {
const data = await this.redis.get(`pocketping:session:${sessionId}`);
return data ? (JSON.parse(data) as Session) : null;
}

// The SDK passes the full, updated Session object
async updateSession(session: Session): Promise<void> {
await this.redis.set(
`pocketping:session:${session.id}`,
JSON.stringify(session)
);
}

async deleteSession(sessionId: string): Promise<void> {
await this.redis.del(`pocketping:session:${sessionId}`);
await this.redis.del(`pocketping:messages:${sessionId}`);
}

async saveMessage(message: Message): Promise<void> {
await this.redis.rpush(
`pocketping:messages:${message.sessionId}`,
JSON.stringify(message)
);
}

async getMessages(sessionId: string): Promise<Message[]> {
const messages = await this.redis.lrange(
`pocketping:messages:${sessionId}`,
0, -1
);
return messages.map((m) => JSON.parse(m));
}

async getMessage(messageId: string): Promise<Message | null> {
// Scan or maintain a secondary index; simplified here
return null;
}
}

// Use it
const pp = new PocketPing({
storage: new RedisStorage(new Redis(process.env.REDIS_URL)),
});
Required vs optional methods

createSession, getSession, updateSession, deleteSession, saveMessage, getMessages, and getMessage are required. Methods for bridge message IDs and attachments (saveAttachment, getAttachment, updateAttachment, saveBridgeMessageIds, getBridgeMessageIds) are optional — implement them to enable edit/delete-to-bridge sync and file attachments.


TypeScript Support

The SDK includes full TypeScript definitions:

import {
PocketPing,
PocketPingConfig,
Session,
Message,
CustomEvent,
Storage,
} from '@pocketping/sdk-node';

const config: PocketPingConfig = {
welcomeMessage: 'Hi! How can we help?',

onEvent: (event: CustomEvent, session: Session) => {
// Fully typed
console.log(event.name, event.data);
console.log(session.id, session.metadata?.country);
},
};

const pp = new PocketPing(config);

API Reference

Methods

MethodDescription
pp.middleware()Returns a Node/Connect/Express request handler
pp.attachWebSocket(server)Attach the WebSocket stream to an HTTP server
pp.handleConnect(request)Handle a widget connection
pp.handleMessage(request)Handle a message ({ sessionId, content, sender })
pp.handleGetMessages(request)Get session messages ({ messages, hasMore })
pp.handleEditMessage(request)Edit a visitor message (synced to bridges)
pp.handleDeleteMessage(request)Delete a visitor message (synced to bridges)
pp.handleRead(request)Update delivered/read status
pp.handleIdentify({ sessionId, identity })Identify a visitor
pp.handlePresence()Get operator presence + AI status
pp.handleUploadRequest(request)Request a presigned upload URL
pp.handleUploadComplete(attachmentId)Mark an attachment ready
pp.getSession(id)Get a specific session
pp.sendOperatorMessage(sessionId, content)Send an operator reply
pp.setOperatorOnline(online)Set operator online/offline
pp.emitEvent(sessionId, event, data)Send a custom event to one widget
pp.broadcastEvent(event, data)Send a custom event to all widgets
pp.triggerEvent(sessionId, event, data)Process an event server-side
pp.onEvent(name, handler)Subscribe to custom events ('*' for all)
pp.addBridge(bridge)Add a bridge at runtime
pp.getStorage()Access the underlying storage adapter

User-Agent Filtering

Block bots and automated requests from creating chat sessions to prevent spam and reduce noise.

Quick Setup

const pp = new PocketPing({
uaFilter: {
enabled: true,
// Automatically blocks ~50 known bot patterns (GoogleBot, curl, etc.)
useDefaultBots: true,
},
});

Configuration Options

const pp = new PocketPing({
uaFilter: {
enabled: true,
mode: 'blocklist', // 'blocklist' | 'allowlist' | 'both'
useDefaultBots: true, // Include ~50 default bot patterns
blocklist: [
'my-custom-scraper',
'bad-bot',
'/spam-\\d+/', // Regex pattern
],
allowlist: [
'my-monitoring-bot',
'/internal-.*/', // Regex: allow internal tools
],
logBlocked: true,
blockedStatusCode: 403,
blockedMessage: 'Forbidden',
},
});

Filter Modes

ModeBehavior
blocklistBlock matching user-agents, allow all others
allowlistOnly allow matching user-agents, block all others
bothAllowlist takes precedence, then blocklist is applied

Pattern Types

  • Substring: googlebot matches any UA containing "googlebot" (case-insensitive)
  • Regex: /bot-\d+/ - wrap pattern in / for regex matching

Default Bot Patterns

When useDefaultBots: true, these patterns are automatically blocked:

  • Search Engines: GoogleBot, BingBot, DuckDuckBot, YandexBot, etc.
  • SEO Tools: SEMrush, Ahrefs, Screaming Frog, etc.
  • Monitoring: Pingdom, UptimeRobot, NewRelic, Datadog, etc.
  • HTTP Libraries: curl, wget, Python-requests, axios, etc.
  • AI Crawlers: GPTBot, ChatGPT-User, Anthropic-AI, etc.

Manual Filtering

import { checkUaFilter, isBot, DEFAULT_BOT_PATTERNS } from '@pocketping/sdk-node';

// Quick bot check
if (isBot(req.headers['user-agent'])) {
return res.status(403).json({ error: 'Bots not allowed' });
}

// Full filter check
const result = checkUaFilter(req.headers['user-agent'], {
enabled: true,
mode: 'blocklist',
useDefaultBots: true,
blocklist: ['my-custom-bot'],
});

if (!result.allowed) {
console.log(`Blocked: ${result.reason}, pattern: ${result.matchedPattern}`);
}

Custom Filter Function

For dynamic filtering logic:

const pp = new PocketPing({
uaFilter: {
enabled: true,
customFilter: (userAgent, requestInfo) => {
// Allow internal IPs regardless of UA
if (requestInfo.ip?.startsWith('10.')) return true;

// Block specific pattern
if (userAgent.includes('evil-bot')) return false;

// Return null to defer to list-based filtering
return null;
},
},
});

Next Steps