@warriorteam/messenger-sdk
Version:
TypeScript SDK for Facebook Messenger Platform API with Conversations support
886 lines (714 loc) • 25.3 kB
Markdown
# @warriorteam/messenger-sdk
A modern TypeScript SDK for the Facebook Messenger Platform API, designed with simplicity and type safety in mind.
## Features
- 🚀 **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
## Installation
```bash
npm install @warriorteam/messenger-sdk
```
## Requirements
- Node.js 18+ (for native fetch support)
- A Facebook Page Access Token
## Quick Start
```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);
```
## API Reference
### Client Configuration
```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)
});
```
### Token Override Support
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 API
#### 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`.
### Attachments API
#### 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' }
}
}
});
```
### Templates API
#### 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'
}]
});
```
### Moderation API
#### 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');
```
### Profile API
#### Get User Profile Information
```typescript
// Get basic profile info (first_name, last_name, profile_pic)
const profile = await messenger.profile.getBasic('USER_PSID');
console.log(`Hello ${profile.first_name}!`);
// 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
```typescript
// Use profile info to personalize messages
const profile = await messenger.profile.getName('USER_PSID');
const greeting = profile.first_name
? `Hello ${profile.first_name}! Welcome back!`
: '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.
### Conversations API
Retrieve conversation history and messages between users and your Page or Instagram Business Account.
#### Permissions Required
**For Messenger conversations:**
- `pages_manage_metadata`
- `pages_read_engagement`
- `pages_messaging`
**For Instagram conversations:**
- `instagram_basic`
- `instagram_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}`);
console.log(`Text: ${message.message}`);
// 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.
## Webhook Support
The SDK provides comprehensive webhook support with full TypeScript safety through discriminated unions.
### Webhook Types and Processing
```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}`);
},
onMessageEdit: async (event) => {
// TypeScript knows this is MessageEditWebhookEvent
console.log(`Message edited to: ${event.message_edit.text}`);
},
onMessageReaction: async (event) => {
// TypeScript knows this is MessageReactionWebhookEvent
console.log(`Reaction: ${event.reaction.reaction}`);
},
onMessagingPostback: async (event) => {
// TypeScript knows this is MessagingPostbackWebhookEvent
console.log(`Postback: ${event.postback.payload}`);
}
});
res.sendStatus(200);
});
```
### Manual Event Processing
```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]
```
### Webhook Verification
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
});
```
### Type-Safe Event Handling
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}`);
} else if (isMessageEditEvent(event)) {
// TypeScript knows event.message_edit exists
console.log(`Edit: ${event.message_edit.text}`);
} else if (isMessagingPostbackEvent(event)) {
// TypeScript knows event.postback exists
console.log(`Postback: ${event.postback.payload}`);
}
}
```
### Supported Webhook Event Types
#### Messenger Platform Events
- `MESSAGE` - User sends a message
- `MESSAGE_EDIT` - User edits a sent message
- `MESSAGE_REACTION` - User reacts to a message
- `MESSAGE_READ` - User reads messages (read receipts)
- `MESSAGING_FEEDBACK` - User submits feedback via templates
- `MESSAGING_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)
### Page Webhook Type Helpers
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!`);
} else if (context.isProcessing) {
console.log(`Video ${context.videoId} is still processing...`);
}
}
// 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!`);
} else if (context.isVODReady) {
console.log(`Live stream ${context.videoId} VOD is ready!`);
}
}
}
}
res.sendStatus(200);
});
```
**Note**: Page webhooks use a different structure (`entry[].changes[]`) compared to Messenger webhooks (`entry[].messaging[]`). The SDK provides helpers for both.
### Complete Controller Example (NestJS/Express)
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}`);
},
onMessagingPostback: async (event) => {
// event is MessagingPostbackWebhookEvent
console.log(`Button clicked: ${event.postback.payload}`);
},
onMessageReaction: async (event) => {
// event is MessageReactionWebhookEvent
console.log(`Reaction: ${event.reaction.reaction}`);
},
});
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[]`)
## Error Handling
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);
}
}
```
## Examples
Check the `examples/` directory for complete usage examples:
- `send-message.ts` - Basic message sending
- `upload-attachment.ts` - Attachment handling
- `send-template.ts` - Template messages
- `user-profile.ts` - Profile API usage
- `moderation.ts` - User moderation
- `conversations.ts` - Retrieve conversations and messages
- `webhook-handler.ts` - Complete webhook setup with type safety
- `multi-tenant.ts` - Token override for multi-tenant applications
- `signature-verification.ts` - Webhook security implementation
## Development
```bash
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm test
# Lint code
npm run lint
# Format code
npm run format
```
## Changelog
### v1.5.2 (Latest)
- **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
### v1.5.1
- **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
### v1.5.0
- **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
### v1.4.2
- **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
### v1.4.x
- **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
### v1.3.x
- **Added**: Token override support for multi-tenant applications
- **Improved**: All API methods now accept optional `RequestOptions` parameter
### v1.1.0
- **Added**: User Profile API support
## License
MIT
## Support
For issues and questions, please visit our [GitHub repository](https://github.com/warriorteam/messenger-sdk).