TypeScript SDK for Facebook Messenger Platform API with Conversations support
npm install @warriorteam/messenger-sdkA modern TypeScript SDK for the Facebook Messenger Platform API, designed with simplicity and type safety in mind.
- 🚀 Zero runtime dependencies - Built with native fetch
- 📝 Full TypeScript support - Complete type definitions with discriminated unions
- 🔄 Dual module support - ESM and CommonJS
- ✅ Comprehensive validation - Built-in message and template validation
- 🛡️ Error handling - Detailed error types and messages
- 📚 Complete API coverage - Send API, Templates, Attachments, Moderation, Profile, Conversations
- 💬 Conversations API - Retrieve conversation history and messages for Messenger & Instagram
- 🔐 Webhook utilities - Complete webhook type system and signature verification
- 🎯 Token override - Per-method access token override support
- 🔧 Type-safe webhooks - Discriminated unions for proper TypeScript narrowing
``bash`
npm install @warriorteam/messenger-sdk
- Node.js 18+ (for native fetch support)
- A Facebook Page Access Token
`typescript
import { Messenger } from '@warriorteam/messenger-sdk';
const messenger = new Messenger({
accessToken: 'YOUR_PAGE_ACCESS_TOKEN',
version: 'v23.0'
});
// Send a text message
const result = await messenger.send.message({
recipient: { id: 'USER_PSID' },
message: { text: 'Hello from RedAI Messenger SDK!' }
});
console.log('Message sent:', result.message_id);
`
`typescript`
const messenger = new Messenger({
accessToken?: string; // Optional: Default page access token
version?: string; // Optional: API version (default: 'v23.0')
baseUrl?: string; // Optional: Custom base URL
timeout?: number; // Optional: Request timeout in ms (default: 30000)
maxRetries?: number; // Optional: Max retry attempts (default: 3)
});
Every API method supports per-call access token override, perfect for multi-tenant applications:
`typescript
const messenger = new Messenger({
accessToken: 'default_page_token'
});
// Use default token
await messenger.send.message({
recipient: { id: 'USER_PSID' },
message: { text: 'Hello from default page!' }
});
// Override token for this specific call
await messenger.send.message({
recipient: { id: 'USER_PSID' },
message: { text: 'Hello from different page!' }
}, {
accessToken: 'other_page_token'
});
// Works with all API methods
const profile = await messenger.profile.getBasic('USER_PSID', {
accessToken: 'specific_token'
});
`
#### Send Text Message
`typescript`
await messenger.send.message({
recipient: { id: 'USER_PSID' },
message: { text: 'Hello World!' }
});
#### Send Message with Quick Replies
`typescript`
await messenger.send.message({
recipient: { id: 'USER_PSID' },
message: {
text: 'Pick a color:',
quick_replies: [
{ content_type: 'text', title: 'Red', payload: 'PICKED_RED' },
{ content_type: 'text', title: 'Blue', payload: 'PICKED_BLUE' }
]
}
});
#### Sender Actions
Manage conversation state and reactions:
`typescript
// Typing indicators
await messenger.send.typingOn('USER_PSID');
await messenger.send.typingOff('USER_PSID');
// Helper to toggle typing based on boolean
await messenger.send.setTyping('USER_PSID', true);
// Mark as read/seen
await messenger.send.markRead('USER_PSID');
// React to a message
await messenger.send.addReaction('USER_PSID', {
messageId: 'MESSAGE_ID',
emoji: '❤️'
});
// Remove reaction
await messenger.send.removeReaction('USER_PSID', 'MESSAGE_ID');
`
#### Response Format
The Send API response includes a message_id and optionally a recipient_id:
`typescript`
const result = await messenger.send.message({...});
console.log('Message ID:', result.message_id); // Always present
console.log('Recipient ID:', result.recipient_id || 'Not provided'); // Optional
Note: recipient_id is only included when using recipient.id (PSID). It's not included when using recipient.user_ref or recipient.phone_number.
#### Upload Attachment from URL
`typescript
const attachment = await messenger.attachments.upload({
type: 'image',
url: 'https://example.com/image.jpg',
is_reusable: true
});
// Use the attachment ID to send
await messenger.send.message({
recipient: { id: 'USER_PSID' },
message: {
attachment: {
type: 'image',
payload: { attachment_id: attachment.attachment_id }
}
}
});
`
#### Send Attachment Directly
`typescript`
await messenger.send.message({
recipient: { id: 'USER_PSID' },
message: {
attachment: {
type: 'image',
payload: { url: 'https://example.com/image.jpg' }
}
}
});
#### Generic Template
`typescript`
await messenger.templates.generic({
recipient: { id: 'USER_PSID' },
elements: [{
title: 'Welcome',
subtitle: 'Check out our products',
image_url: 'https://example.com/image.jpg',
buttons: [{
type: 'web_url',
title: 'Shop Now',
url: 'https://example.com/shop'
}]
}]
});
#### Button Template
`typescript`
await messenger.templates.button({
recipient: { id: 'USER_PSID' },
text: 'What would you like to do?',
buttons: [{
type: 'postback',
title: 'Get Started',
payload: 'GET_STARTED'
}]
});
#### User Moderation
`typescript
// Block a user from messaging (can still see page content)
await messenger.moderation.blockUser('USER_PSID');
// Ban a user completely (no messaging + no Facebook interactions)
await messenger.moderation.banUser('USER_PSID');
// Move conversation to spam folder
await messenger.moderation.moveToSpam('USER_PSID');
// Block and spam (common combo)
await messenger.moderation.blockAndSpam('USER_PSID');
// Multiple users at once
await messenger.moderation.blockUser(['USER_1', 'USER_2', 'USER_3']);
// Unblock/unban users
await messenger.moderation.unblockUser('USER_PSID');
await messenger.moderation.unbanUser('USER_PSID');
`
#### Get User Profile Information
`typescriptHello ${profile.first_name}!
// Get basic profile info (first_name, last_name, profile_pic)
const profile = await messenger.profile.getBasic('USER_PSID');
console.log();
// Get comprehensive profile with all fields
const fullProfile = await messenger.profile.getFull('USER_PSID');
console.log('Profile:', fullProfile);
// Returns: { id, name, first_name, last_name, profile_pic, locale, timezone, gender }
// Get specific fields only
const customProfile = await messenger.profile.get({
psid: 'USER_PSID',
fields: ['first_name', 'locale', 'timezone']
});
// Helper methods for common use cases
const nameInfo = await messenger.profile.getName('USER_PSID');
const profilePic = await messenger.profile.getProfilePicture('USER_PSID');
`
#### Personalize Messages
`typescriptHello ${profile.first_name}! Welcome back!
// Use profile info to personalize messages
const profile = await messenger.profile.getName('USER_PSID');
const greeting = profile.first_name
?
: 'Hello! Welcome back!';
await messenger.send.message({
recipient: { id: 'USER_PSID' },
messaging_type: 'RESPONSE',
message: { text: greeting }
});
`
Note: Profile API requires "Advanced User Profile Access" feature and user interaction to grant permissions.
Retrieve conversation history and messages between users and your Page or Instagram Business Account.
#### Permissions Required
For Messenger conversations:
- pages_manage_metadatapages_read_engagement
- pages_messaging
-
For Instagram conversations:
- instagram_basicinstagram_manage_messages
- pages_manage_metadata
-
- Your app must be owned by a verified business
#### List Conversations
`typescript
// List Messenger conversations
const conversations = await messenger.conversations.list('PAGE_ID', {
platform: 'messenger',
limit: 25
});
// List Instagram conversations
const igConversations = await messenger.conversations.list('PAGE_ID', {
platform: 'instagram',
limit: 25
});
// Find conversation with specific user
const conversationId = await messenger.conversations.findByUser(
'PAGE_ID',
'USER_INSTAGRAM_SCOPED_ID',
'instagram'
);
`
#### Get Conversation Details
`typescript
// Get conversation with messages and participants
const conversation = await messenger.conversations.get('CONVERSATION_ID', {
fields: ['messages', 'participants'],
limit: 20
});
// Access participants
conversation.participants?.data.forEach(participant => {
console.log(${participant.name || participant.username} (${participant.id}));
});
// Access messages
conversation.messages?.data.forEach(msg => {
console.log(Message ID: ${msg.id}, Created: ${msg.created_time});`
});
#### Get Message Details
`typescript
// Get full message details
const message = await messenger.conversations.getMessage('MESSAGE_ID', {
fields: ['id', 'created_time', 'from', 'to', 'message', 'attachments', 'reactions', 'reply_to']
});
console.log(From: ${message.from?.username});Text: ${message.message}
console.log();
// Check attachments
if (message.attachments?.data) {
message.attachments.data.forEach(att => {
console.log(Attachment: ${att.file_url || att.image_data?.url});
});
}
// Check reactions
if (message.reactions?.data) {
message.reactions.data.forEach(reaction => {
console.log(${reaction.reaction} (${reaction.users.length} users));`
});
}
#### Get Recent Messages (Convenience Method)
`typescript
// Get the 20 most recent messages with full details
const messages = await messenger.conversations.getRecentMessages('CONVERSATION_ID');
messages.forEach(msg => {
const sender = msg.from?.name || msg.from?.username;
const text = msg.message || '(attachment)';
console.log(${sender}: ${text});`
});
#### Pagination
`typescript
// Paginate through conversations
let after: string | undefined;
let hasMore = true;
while (hasMore) {
const conversations = await messenger.conversations.list('PAGE_ID', {
platform: 'messenger',
limit: 25,
after
});
// Process conversations
console.log(Fetched ${conversations.data.length} conversations);
// Check for next page
if (conversations.paging?.cursors?.after) {
after = conversations.paging.cursors.after;
} else {
hasMore = false;
}
}
`
#### Important Limitations
- 20 Message Limit: You can only retrieve full details for the 20 most recent messages in a conversation. Older messages will return an error.
- Pending Messages: Conversations in the "pending" folder that are inactive for 30+ days are not returned.
- Private Keys: Accounts linked with private keys (email/phone) require Advanced Access approval to retrieve conversations.
The SDK provides comprehensive webhook support with full TypeScript safety through discriminated unions.
`typescript
import {
processWebhookEvents,
extractWebhookEvents,
getWebhookEventType,
getWebhookPayloadEventTypes,
getPageWebhookEventTypes,
WebhookEventType,
GenericWebhookPayload,
PageWebhookPayload
} from '@warriorteam/messenger-sdk';
// Process webhook with type-safe handlers
app.post('/webhook', express.json(), async (req, res) => {
const payload: GenericWebhookPayload = req.body;
await processWebhookEvents(payload, {
onMessage: async (event) => {
// TypeScript knows this is MessageWebhookEvent
console.log(Received message: ${event.message.text});Message edited to: ${event.message_edit.text}
},
onMessageEdit: async (event) => {
// TypeScript knows this is MessageEditWebhookEvent
console.log();Reaction: ${event.reaction.reaction}
},
onMessageReaction: async (event) => {
// TypeScript knows this is MessageReactionWebhookEvent
console.log();Postback: ${event.postback.payload}
},
onMessagingPostback: async (event) => {
// TypeScript knows this is MessagingPostbackWebhookEvent
console.log();`
}
});
res.sendStatus(200);
});
`typescript
// Extract events manually
const events = extractWebhookEvents(payload);
for (const event of events) {
const eventType = getWebhookEventType(event);
switch (eventType) {
case WebhookEventType.MESSAGE:
// Handle message event
break;
case WebhookEventType.MESSAGE_EDIT:
// Handle edit event
break;
// ... other cases
}
}
// Check what event types are in the payload (Messenger events)
const eventTypes = getWebhookPayloadEventTypes(payload);
console.log('Received Messenger event types:', eventTypes); // e.g., ['message', 'postback']
// For Page webhooks, use getPageWebhookEventTypes instead:
// const pagePayload: PageWebhookPayload = req.body;
// const pageEventTypes = getPageWebhookEventTypes(pagePayload);
// console.log('Received Page event types:', pageEventTypes); // e.g., [WebhookEventType.FEED, WebhookEventType.VIDEOS]
`
The SDK provides utilities for both subscription verification and signature validation:
`typescript
import {
verifyWebhookSubscription,
verifyWebhookSignature
} from '@warriorteam/messenger-sdk';
// Subscription verification (GET request)
app.get('/webhook', (req, res) => {
const challenge = verifyWebhookSubscription(
req.query as any,
process.env.VERIFY_TOKEN!
);
if (challenge) {
res.send(challenge);
} else {
res.status(403).send('Forbidden');
}
});
// Signature verification (POST request)
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const signature = req.get('X-Hub-Signature-256');
const result = await verifyWebhookSignature(
req.body,
signature,
process.env.APP_SECRET!
);
if (!result.isValid) {
return res.status(401).json({error: result.error});
}
// Process webhook events...
const payload = JSON.parse(req.body.toString());
// ... handle events
});
`
The SDK uses discriminated unions for perfect TypeScript support:
`typescript
import {
MessengerWebhookEvent,
isMessageEvent,
isMessageEditEvent,
isMessagingPostbackEvent
} from '@warriorteam/messenger-sdk';
function handleWebhookEvent(event: MessengerWebhookEvent) {
if (isMessageEvent(event)) {
// TypeScript knows event.message exists
console.log(Message: ${event.message.text});Edit: ${event.message_edit.text}
} else if (isMessageEditEvent(event)) {
// TypeScript knows event.message_edit exists
console.log();Postback: ${event.postback.payload}
} else if (isMessagingPostbackEvent(event)) {
// TypeScript knows event.postback exists
console.log();`
}
}
#### Messenger Platform Events
- MESSAGE - User sends a messageMESSAGE_EDIT
- - User edits a sent messageMESSAGE_REACTION
- - User reacts to a messageMESSAGE_READ
- - User reads messages (read receipts)MESSAGING_FEEDBACK
- - User submits feedback via templatesMESSAGING_POSTBACK
- - User clicks buttons/quick replies
#### Page Webhook Events
- FEED - Page feed changes (posts, photos, videos, status updates)VIDEOS
- - Video encoding status changes (processing, ready, error)LIVE_VIDEOS
- - Live video status changes (live, stopped, scheduled, VOD ready)
The SDK provides specialized type guards and helpers for Page webhook events:
`typescript
import {
isFeedEvent,
isVideoEvent,
isLiveVideoEvent,
isVideoProcessing,
isVideoReady,
isLiveVideoProcessing,
isLive,
extractVideoContext,
extractLiveVideoContext,
getPageWebhookEventTypes,
FeedActionVerb,
VideoStatus,
LiveVideoStatus,
PageWebhookPayload
} from '@warriorteam/messenger-sdk';
// Handle Page webhooks
app.post('/webhook/page', express.json(), async (req, res) => {
const payload: PageWebhookPayload = req.body;
// Check what types of Page events are in this payload
const eventTypes = getPageWebhookEventTypes(payload);
console.log('Received Page event types:', eventTypes); // e.g., [WebhookEventType.FEED, WebhookEventType.VIDEOS]
for (const entry of payload.entry) {
for (const change of entry.changes || []) {
// Feed events (posts, photos, videos, status)
if (isFeedEvent(change)) {
console.log(Feed ${change.value.verb}: ${change.value.item} by ${change.value.from.id});
if (change.value.verb === FeedActionVerb.ADD) {
console.log('New post created!');
}
}
// Video encoding events
if (isVideoEvent(change)) {
const context = extractVideoContext(entry.id, entry.time, change);
if (context.isReady) {
console.log(Video ${context.videoId} is ready!);Video ${context.videoId} is still processing...
} else if (context.isProcessing) {
console.log();
}
}
// Live video events
if (isLiveVideoEvent(change)) {
const context = extractLiveVideoContext(entry.id, entry.time, change);
if (context.isLive) {
console.log(Live stream ${context.videoId} is now live!);Live stream ${context.videoId} VOD is ready!
} else if (context.isVODReady) {
console.log();
}
}
}
}
res.sendStatus(200);
});
`
Note: Page webhooks use a different structure (entry[].changes[]) compared to Messenger webhooks (entry[].messaging[]). The SDK provides helpers for both.
Here's a complete TypeScript controller for handling webhooks with proper types:
`typescript
import { Controller, Post, Body, Headers, Res, Get, Query } from '@nestjs/common';
import { Response } from 'express';
import {
GenericWebhookPayload,
PageWebhookPayload,
verifyWebhookSignature,
verifyWebhookSubscription,
processWebhookEvents,
isFeedEvent,
isVideoEvent,
isLiveVideoEvent,
} from '@warriorteam/messenger-sdk';
@Controller('webhook')
export class WebhookController {
// Subscription verification (GET)
@Get()
verifyWebhook(@Query() query: any, @Res() res: Response) {
const challenge = verifyWebhookSubscription(
query,
process.env.VERIFY_TOKEN!
);
if (challenge) {
return res.send(challenge);
}
return res.status(403).send('Forbidden');
}
// Messenger webhooks (POST)
@Post('messenger')
async handleMessenger(
@Body() payload: GenericWebhookPayload, // ← Correct type for Messenger
@Headers('x-hub-signature-256') signature: string,
@Res() res: Response
) {
// Verify signature
const verification = await verifyWebhookSignature(
JSON.stringify(payload),
signature,
process.env.APP_SECRET!
);
if (!verification.isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process events with type-safe handlers
await processWebhookEvents(payload, {
onMessage: async (event) => {
// event is MessageWebhookEvent - fully typed!
console.log(From ${event.sender.id}: ${event.message.text});Button clicked: ${event.postback.payload}
},
onMessagingPostback: async (event) => {
// event is MessagingPostbackWebhookEvent
console.log();Reaction: ${event.reaction.reaction}
},
onMessageReaction: async (event) => {
// event is MessageReactionWebhookEvent
console.log();
},
});
return res.sendStatus(200);
}
// Page webhooks (POST)
@Post('page')
async handlePage(
@Body() payload: PageWebhookPayload, // ← Correct type for Page webhooks
@Headers('x-hub-signature-256') signature: string,
@Res() res: Response
) {
// Verify signature
const verification = await verifyWebhookSignature(
JSON.stringify(payload),
signature,
process.env.APP_SECRET!
);
if (!verification.isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process Page events
for (const entry of payload.entry) {
for (const change of entry.changes || []) {
if (isFeedEvent(change)) {
console.log(Page feed: ${change.value.verb} ${change.value.item});
}
if (isVideoEvent(change)) {
console.log(Video status: ${change.value.status.video_status});
}
if (isLiveVideoEvent(change)) {
console.log(Live video status: ${change.value.status});
}
}
}
return res.sendStatus(200);
}
}
`
Key Types to Use:
- GenericWebhookPayload - For Messenger Platform webhooks (entry[].messaging[])PageWebhookPayload
- - For Page webhooks (entry[].changes[])
The SDK provides specific error types for different scenarios:
`typescript
import {
MessengerAPIError,
MessengerNetworkError,
MessengerTimeoutError,
MessengerConfigError
} from '@warriorteam/messenger-sdk';
try {
await messenger.send.message({
recipient: { id: 'USER_PSID' },
message: { text: 'Hello!' }
});
} catch (error) {
if (error instanceof MessengerAPIError) {
console.error('API Error:', error.message, error.code);
} else if (error instanceof MessengerNetworkError) {
console.error('Network Error:', error.message);
} else if (error instanceof MessengerTimeoutError) {
console.error('Timeout Error:', error.timeout);
}
}
`
Check the examples/ directory for complete usage examples:
- send-message.ts - Basic message sendingupload-attachment.ts
- - Attachment handlingsend-template.ts
- - Template messagesuser-profile.ts
- - Profile API usagemoderation.ts
- - User moderationconversations.ts
- - Retrieve conversations and messageswebhook-handler.ts
- - Complete webhook setup with type safetymulti-tenant.ts
- - Token override for multi-tenant applicationssignature-verification.ts
- - Webhook security implementation
`bashInstall dependencies
npm install
Changelog
$3
- Added: getPageWebhookEventTypes() utility function for extracting event types from Page webhooks
- Returns WebhookEventType[] array with proper enum values (e.g., [WebhookEventType.FEED, WebhookEventType.VIDEOS])
- Maps Page webhook field values to WebhookEventType enum for type safety
- Similar to getWebhookPayloadEventTypes() but for PageWebhookPayload structure
- Properly exported from main package
- Improved: Documentation with usage examples for Page webhook event type extraction$3
- Added: Complete NestJS/Express webhook controller example in README
- Updated: WebhookEventType enum to include FEED, VIDEOS, LIVE_VIDEOS
- Improved: Documentation with clear type usage for webhook handlers and Page webhook utilities$3
- Added: Conversations API for retrieving conversation history and messages
- List conversations for Messenger and Instagram
- Get conversation details with participants and messages
- Retrieve individual message details with attachments, reactions, and replies
- Convenience method for getting recent messages with full details
- Find conversations by user ID
- Full pagination support
- Support for both Messenger and Instagram platforms
- Token override support for multi-tenant applications
- Added: Comprehensive TypeScript types for Conversations API
- Conversation, ConversationDetail, Message types
- Message attachments, reactions, shares, story replies
- Image/video data types for media attachments
- Request/response types for all operations$3
- Fixed: Resolved export naming conflicts for isProcessing function between video and live video webhooks
- isProcessing from videos module now exported as isVideoProcessing
- isProcessing from live-videos module now exported as isLiveVideoProcessing
- Added explicit exports for all video and live video helper functions with unique names
- Similar to existing pattern used for ReferralType and ReferralSource conflicts
- Improved: Better TypeScript type safety with no export ambiguities$3
- Added: Comprehensive Facebook Page webhook types
- Feed webhook events (posts, photos, videos, status updates)
- Video encoding webhook events (processing, ready, error states)
- Live video webhook events (live, stopped, scheduled, VOD ready)
- Added: Type guards and helper functions for all Page webhook events
- Added: Context extraction utilities for video events$3
- Added: Token override support for multi-tenant applications
- Improved: All API methods now accept optional RequestOptions` parameterMIT
For issues and questions, please visit our GitHub repository.