The Facebook Messenger API enables developers to build conversational experiences that reach over 1.3 billion monthly active users. Whether you're creating a customer support bot, an e-commerce assistant, or an interactive marketing campaign, the messenger platform api provides the tools you need to engage users in meaningful conversations.
This guide walks you through everything from initial setup to advanced features like message templates, quick replies, and the human handover protocol. You'll find working TypeScript examples throughout, ready to adapt for your own projects.
Introduction to Messenger Platform
The Messenger Platform is Meta's framework for building bots and integrations that communicate with users through Facebook Messenger. Unlike traditional web interfaces, the facebook chat api creates a conversational experience where users interact through messages, buttons, and rich media.
Key capabilities of the messenger bot api include:
| Feature | Description | Use Case |
|---|---|---|
| Text Messages | Basic message sending and receiving | Customer inquiries, notifications |
| Message Templates | Structured layouts with images and buttons | Product catalogs, receipts |
| Quick Replies | Suggested response buttons | Guided conversations, surveys |
| Persistent Menu | Always-available navigation options | Bot navigation, common actions |
| Media Messages | Images, videos, audio, and files | Product images, tutorials |
| Handover Protocol | Transfer between bots and human agents | Complex support escalation |
The platform operates on a webhook-based architecture. Your server receives incoming messages via webhooks, processes them, and responds using the Send API. This asynchronous model allows you to build responsive experiences without keeping connections open.
Setting Up Your Meta App
Before you can use the facebook messenger api, you need to create and configure a Meta App. This process establishes your application's identity and grants access to the Messenger Platform.
Step 1: Create a Meta App
Navigate to the Meta for Developers portal and create a new app. Select "Business" as your app type, which provides access to Messenger features.
// Environment variables you'll need after setup
interface MessengerConfig {
FACEBOOK_APP_ID: string;
FACEBOOK_APP_SECRET: string;
FACEBOOK_PAGE_ID: string;
FACEBOOK_PAGE_ACCESS_TOKEN: string;
MESSENGER_VERIFY_TOKEN: string;
}
// Validate your configuration at startup
function validateConfig(): MessengerConfig {
const required = [
'FACEBOOK_APP_ID',
'FACEBOOK_APP_SECRET',
'FACEBOOK_PAGE_ID',
'FACEBOOK_PAGE_ACCESS_TOKEN',
'MESSENGER_VERIFY_TOKEN'
];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
return {
FACEBOOK_APP_ID: process.env.FACEBOOK_APP_ID!,
FACEBOOK_APP_SECRET: process.env.FACEBOOK_APP_SECRET!,
FACEBOOK_PAGE_ID: process.env.FACEBOOK_PAGE_ID!,
FACEBOOK_PAGE_ACCESS_TOKEN: process.env.FACEBOOK_PAGE_ACCESS_TOKEN!,
MESSENGER_VERIFY_TOKEN: process.env.MESSENGER_VERIFY_TOKEN!
};
}
Step 2: Add Messenger Product
In your app dashboard, click "Add Product" and select Messenger. This enables the Messenger Platform features for your app.
Step 3: Connect a Facebook Page
Your bot needs a Facebook Page to send and receive messages. In the Messenger settings, click "Add or Remove Pages" and select the page you want to connect. Generate a Page Access Token, which you'll use to authenticate API requests.
Note: Page Access Tokens can be short-lived or long-lived. For production applications, exchange your token for a long-lived version that lasts approximately 60 days.
interface TokenExchangeResponse {
access_token: string;
token_type: string;
expires_in?: number;
}
async function exchangeForLongLivedToken(
shortLivedToken: string,
appId: string,
appSecret: string
): Promise<TokenExchangeResponse> {
const baseUrl = 'https://graph.facebook.com/v18.0';
const params = new URLSearchParams({
grant_type: 'fb_exchange_token',
client_id: appId,
client_secret: appSecret,
fb_exchange_token: shortLivedToken,
});
const response = await fetch(`${baseUrl}/oauth/access_token?${params.toString()}`);
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
const data = await response.json();
if (!data.access_token) {
throw new Error('No access token returned from token exchange');
}
const expiresInDays = data.expires_in
? Math.floor(data.expires_in / 86400)
: 'unknown';
console.log(`Token exchanged successfully (expires in ${expiresInDays} days)`);
return {
access_token: data.access_token,
token_type: data.token_type || 'bearer',
expires_in: data.expires_in,
};
}
Step 4: Configure App Permissions
Request the necessary permissions for your bot. At minimum, you'll need:
pages_messaging: Send and receive messagespages_manage_metadata: Subscribe to webhookspages_read_engagement: Access conversation data
Webhook Configuration and Verification
Webhooks are the backbone of the messenger platform api. They allow Facebook to notify your server when events occur, such as incoming messages or message deliveries.
Setting Up Your Webhook Endpoint
Create an endpoint that handles both GET requests (for verification) and POST requests (for receiving events).
import express, { Request, Response } from 'express';
import crypto from 'crypto';
const app = express();
// Parse raw body for signature verification
app.use(express.json({
verify: (req: any, res, buf) => {
req.rawBody = buf;
}
}));
// Webhook verification endpoint
app.get('/webhook', (req: Request, res: Response) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
const verifyToken = process.env.MESSENGER_VERIFY_TOKEN;
if (mode === 'subscribe' && token === verifyToken) {
console.log('Webhook verified successfully');
res.status(200).send(challenge);
} else {
console.error('Webhook verification failed');
res.sendStatus(403);
}
});
// Webhook event receiver
app.post('/webhook', (req: Request, res: Response) => {
// Verify request signature
const signature = req.headers['x-hub-signature-256'] as string;
if (!verifySignature(req, signature)) {
console.error('Invalid signature');
return res.sendStatus(401);
}
const body = req.body;
if (body.object !== 'page') {
return res.sendStatus(404);
}
// Process each entry
body.entry?.forEach((entry: any) => {
entry.messaging?.forEach((event: any) => {
handleMessagingEvent(event);
});
});
// Always respond with 200 OK quickly
res.sendStatus(200);
});
function verifySignature(req: any, signature: string): boolean {
if (!signature) return false;
const appSecret = process.env.FACEBOOK_APP_SECRET!;
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', appSecret)
.update(req.rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
async function handleMessagingEvent(event: any): Promise<void> {
const senderId = event.sender.id;
if (event.message) {
await handleMessage(senderId, event.message);
} else if (event.postback) {
await handlePostback(senderId, event.postback);
} else if (event.read) {
console.log(`Message read by ${senderId}`);
} else if (event.delivery) {
console.log(`Message delivered to ${senderId}`);
}
}
Subscribing to Webhook Events
After setting up your endpoint, subscribe to the events you want to receive. In the Meta App Dashboard, configure your webhook URL and select the relevant subscription fields:
messages: Incoming messagesmessaging_postbacks: Button clicks and menu selectionsmessaging_optins: User opt-insmessage_deliveries: Delivery confirmationsmessage_reads: Read receipts
Receiving Messages
When users send messages to your bot, the facebook chat api delivers them to your webhook. Messages can contain text, attachments, or both.
interface MessengerMessage {
mid: string;
text?: string;
attachments?: Array<{
type: 'image' | 'video' | 'audio' | 'file' | 'location' | 'fallback';
payload: {
url?: string;
coordinates?: {
lat: number;
long: number;
};
};
}>;
quick_reply?: {
payload: string;
};
reply_to?: {
mid: string;
};
}
async function handleMessage(
senderId: string,
message: MessengerMessage
): Promise<void> {
console.log(`Received message from ${senderId}:`, message);
// Handle quick reply responses
if (message.quick_reply) {
await handleQuickReply(senderId, message.quick_reply.payload);
return;
}
// Handle attachments
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
await handleAttachment(senderId, attachment);
}
return;
}
// Handle text messages
if (message.text) {
await processTextMessage(senderId, message.text);
}
}
async function handleAttachment(
senderId: string,
attachment: MessengerMessage['attachments'][0]
): Promise<void> {
switch (attachment.type) {
case 'image':
await sendTextMessage(senderId, `Thanks for the image! I received: ${attachment.payload.url}`);
break;
case 'location':
const { lat, long } = attachment.payload.coordinates!;
await sendTextMessage(senderId, `Got your location: ${lat}, ${long}`);
break;
default:
await sendTextMessage(senderId, `Received your ${attachment.type}`);
}
}
async function processTextMessage(
senderId: string,
text: string
): Promise<void> {
const lowerText = text.toLowerCase();
if (lowerText.includes('help')) {
await sendHelpMessage(senderId);
} else if (lowerText.includes('products')) {
await sendProductCatalog(senderId);
} else {
await sendTextMessage(senderId, `You said: "${text}". How can I help you today?`);
}
}
Sending Text Messages
The Send API is your primary tool for responding to users through the messenger bot api. Text messages are the simplest form of response.
const GRAPH_API_URL = 'https://graph.facebook.com/v18.0';
interface SendMessageResponse {
recipient_id: string;
message_id: string;
}
interface SendMessageError {
message: string;
type: string;
code: number;
error_subcode?: number;
fbtrace_id: string;
}
async function sendTextMessage(
recipientId: string,
text: string
): Promise<SendMessageResponse> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
const response = await fetch(
`${GRAPH_API_URL}/me/messages?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
message: { text },
messaging_type: 'RESPONSE'
}),
}
);
const data = await response.json();
if (!response.ok) {
const error = data.error as SendMessageError;
throw new Error(`Send failed: ${error.message} (code: ${error.code})`);
}
return data as SendMessageResponse;
}
// Send typing indicator for better UX
async function sendTypingIndicator(
recipientId: string,
isTyping: boolean
): Promise<void> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
await fetch(
`${GRAPH_API_URL}/me/messages?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
sender_action: isTyping ? 'typing_on' : 'typing_off'
}),
}
);
}
// Helper function to send messages with typing indicator
async function sendMessageWithTyping(
recipientId: string,
text: string,
typingDuration: number = 1000
): Promise<SendMessageResponse> {
await sendTypingIndicator(recipientId, true);
await new Promise(resolve => setTimeout(resolve, typingDuration));
return sendTextMessage(recipientId, text);
}
Message Templates (Generic, Button, Receipt)
Message templates create rich, interactive experiences that go beyond plain text. The facebook messenger api supports several template types, each designed for specific use cases.
Generic Template
The Generic Template displays a carousel of items, perfect for product listings or content feeds.
interface GenericTemplateElement {
title: string;
subtitle?: string;
image_url?: string;
default_action?: {
type: 'web_url';
url: string;
webview_height_ratio?: 'compact' | 'tall' | 'full';
};
buttons?: Array<TemplateButton>;
}
type TemplateButton =
| { type: 'web_url'; url: string; title: string }
| { type: 'postback'; title: string; payload: string }
| { type: 'phone_number'; title: string; payload: string }
| { type: 'account_link'; url: string }
| { type: 'account_unlink' };
async function sendGenericTemplate(
recipientId: string,
elements: GenericTemplateElement[]
): Promise<SendMessageResponse> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
// Limit to 10 elements per carousel
const limitedElements = elements.slice(0, 10);
const response = await fetch(
`${GRAPH_API_URL}/me/messages?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
message: {
attachment: {
type: 'template',
payload: {
template_type: 'generic',
elements: limitedElements
}
}
},
messaging_type: 'RESPONSE'
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Template send failed: ${error.error?.message}`);
}
return response.json();
}
// Example: Product catalog
async function sendProductCatalog(recipientId: string): Promise<void> {
const products: GenericTemplateElement[] = [
{
title: 'Wireless Headphones',
subtitle: '$149.99 - Premium sound quality',
image_url: 'https://example.com/headphones.jpg',
default_action: {
type: 'web_url',
url: 'https://example.com/products/headphones'
},
buttons: [
{ type: 'postback', title: 'Buy Now', payload: 'BUY_HEADPHONES' },
{ type: 'postback', title: 'More Info', payload: 'INFO_HEADPHONES' }
]
},
{
title: 'Smart Watch',
subtitle: '$299.99 - Track your fitness',
image_url: 'https://example.com/watch.jpg',
default_action: {
type: 'web_url',
url: 'https://example.com/products/watch'
},
buttons: [
{ type: 'postback', title: 'Buy Now', payload: 'BUY_WATCH' },
{ type: 'postback', title: 'More Info', payload: 'INFO_WATCH' }
]
}
];
await sendGenericTemplate(recipientId, products);
}
Button Template
The Button Template presents a text message with up to three buttons, ideal for simple choices.
async function sendButtonTemplate(
recipientId: string,
text: string,
buttons: TemplateButton[]
): Promise<SendMessageResponse> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
// Limit to 3 buttons
const limitedButtons = buttons.slice(0, 3);
const response = await fetch(
`${GRAPH_API_URL}/me/messages?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
message: {
attachment: {
type: 'template',
payload: {
template_type: 'button',
text,
buttons: limitedButtons
}
}
},
messaging_type: 'RESPONSE'
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Button template failed: ${error.error?.message}`);
}
return response.json();
}
// Example: Help menu
async function sendHelpMessage(recipientId: string): Promise<void> {
await sendButtonTemplate(
recipientId,
'How can I help you today?',
[
{ type: 'postback', title: 'Browse Products', payload: 'BROWSE_PRODUCTS' },
{ type: 'postback', title: 'Track Order', payload: 'TRACK_ORDER' },
{ type: 'postback', title: 'Contact Support', payload: 'CONTACT_SUPPORT' }
]
);
}
Receipt Template
The Receipt Template displays order confirmations with itemized details.
interface ReceiptElement {
title: string;
subtitle?: string;
quantity?: number;
price: number;
currency?: string;
image_url?: string;
}
interface ReceiptSummary {
subtotal?: number;
shipping_cost?: number;
total_tax?: number;
total_cost: number;
}
interface ReceiptAddress {
street_1: string;
street_2?: string;
city: string;
postal_code: string;
state: string;
country: string;
}
async function sendReceiptTemplate(
recipientId: string,
receipt: {
recipientName: string;
orderNumber: string;
currency: string;
paymentMethod: string;
orderUrl?: string;
timestamp?: string;
address?: ReceiptAddress;
elements: ReceiptElement[];
summary: ReceiptSummary;
}
): Promise<SendMessageResponse> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
const response = await fetch(
`${GRAPH_API_URL}/me/messages?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
message: {
attachment: {
type: 'template',
payload: {
template_type: 'receipt',
recipient_name: receipt.recipientName,
order_number: receipt.orderNumber,
currency: receipt.currency,
payment_method: receipt.paymentMethod,
order_url: receipt.orderUrl,
timestamp: receipt.timestamp,
address: receipt.address,
elements: receipt.elements,
summary: receipt.summary
}
}
},
messaging_type: 'RESPONSE'
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Receipt template failed: ${error.error?.message}`);
}
return response.json();
}
// Example: Order confirmation
async function sendOrderConfirmation(
recipientId: string,
orderId: string
): Promise<void> {
await sendReceiptTemplate(recipientId, {
recipientName: 'John Doe',
orderNumber: orderId,
currency: 'USD',
paymentMethod: 'Visa ****1234',
orderUrl: `https://example.com/orders/${orderId}`,
timestamp: new Date().toISOString(),
elements: [
{
title: 'Wireless Headphones',
subtitle: 'Black, Over-ear',
quantity: 1,
price: 149.99,
image_url: 'https://example.com/headphones.jpg'
}
],
summary: {
subtotal: 149.99,
shipping_cost: 4.99,
total_tax: 12.75,
total_cost: 167.73
}
});
}
Quick Replies and Persistent Menu
Quick Replies and the Persistent Menu guide users through conversations by presenting clear options.
Quick Replies
Quick Replies appear as buttons above the keyboard, providing suggested responses that disappear after selection.
interface QuickReply {
content_type: 'text' | 'user_phone_number' | 'user_email';
title?: string;
payload?: string;
image_url?: string;
}
async function sendQuickReplies(
recipientId: string,
text: string,
quickReplies: QuickReply[]
): Promise<SendMessageResponse> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
// Limit to 13 quick replies
const limitedReplies = quickReplies.slice(0, 13);
const response = await fetch(
`${GRAPH_API_URL}/me/messages?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
message: {
text,
quick_replies: limitedReplies
},
messaging_type: 'RESPONSE'
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Quick replies failed: ${error.error?.message}`);
}
return response.json();
}
// Example: Satisfaction survey
async function sendSatisfactionSurvey(recipientId: string): Promise<void> {
await sendQuickReplies(
recipientId,
'How would you rate your experience today?',
[
{ content_type: 'text', title: '😄 Great', payload: 'RATING_5' },
{ content_type: 'text', title: '🙂 Good', payload: 'RATING_4' },
{ content_type: 'text', title: '😐 Okay', payload: 'RATING_3' },
{ content_type: 'text', title: '😕 Poor', payload: 'RATING_2' },
{ content_type: 'text', title: '😞 Bad', payload: 'RATING_1' }
]
);
}
async function handleQuickReply(
senderId: string,
payload: string
): Promise<void> {
if (payload.startsWith('RATING_')) {
const rating = parseInt(payload.split('_')[1]);
await sendTextMessage(
senderId,
`Thank you for your feedback! You rated us ${rating}/5.`
);
}
}
Persistent Menu
The Persistent Menu provides always-available navigation through the hamburger icon.
interface MenuItem {
type: 'postback' | 'web_url' | 'nested';
title: string;
payload?: string;
url?: string;
webview_height_ratio?: 'compact' | 'tall' | 'full';
call_to_actions?: MenuItem[];
}
async function setPersistentMenu(
menuItems: MenuItem[],
locale: string = 'default'
): Promise<void> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
const response = await fetch(
`${GRAPH_API_URL}/me/messenger_profile?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
persistent_menu: [
{
locale,
composer_input_disabled: false,
call_to_actions: menuItems
}
]
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Persistent menu setup failed: ${error.error?.message}`);
}
console.log('Persistent menu configured successfully');
}
// Example: E-commerce bot menu
async function setupBotMenu(): Promise<void> {
await setPersistentMenu([
{
type: 'postback',
title: '🛍️ Shop Now',
payload: 'SHOP_NOW'
},
{
type: 'nested',
title: '📦 My Orders',
call_to_actions: [
{ type: 'postback', title: 'Track Order', payload: 'TRACK_ORDER' },
{ type: 'postback', title: 'Order History', payload: 'ORDER_HISTORY' },
{ type: 'postback', title: 'Returns', payload: 'RETURNS' }
]
},
{
type: 'postback',
title: '💬 Contact Support',
payload: 'CONTACT_SUPPORT'
}
]);
}
Media Messages (Images, Video, Files)
The messenger platform api supports rich media attachments, allowing you to send images, videos, audio files, and documents.
type AttachmentType = 'image' | 'video' | 'audio' | 'file';
interface AttachmentPayload {
url?: string;
is_reusable?: boolean;
attachment_id?: string;
}
async function sendMediaAttachment(
recipientId: string,
type: AttachmentType,
payload: AttachmentPayload
): Promise<SendMessageResponse> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
const response = await fetch(
`${GRAPH_API_URL}/me/messages?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
message: {
attachment: {
type,
payload
}
},
messaging_type: 'RESPONSE'
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Media send failed: ${error.error?.message}`);
}
return response.json();
}
// Send image by URL
async function sendImage(
recipientId: string,
imageUrl: string,
reusable: boolean = true
): Promise<SendMessageResponse> {
return sendMediaAttachment(recipientId, 'image', {
url: imageUrl,
is_reusable: reusable
});
}
// Send video by URL
async function sendVideo(
recipientId: string,
videoUrl: string
): Promise<SendMessageResponse> {
return sendMediaAttachment(recipientId, 'video', {
url: videoUrl,
is_reusable: true
});
}
// Send file/document
async function sendFile(
recipientId: string,
fileUrl: string
): Promise<SendMessageResponse> {
return sendMediaAttachment(recipientId, 'file', {
url: fileUrl,
is_reusable: true
});
}
// Upload attachment for reuse
async function uploadAttachment(
type: AttachmentType,
url: string
): Promise<string> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
const response = await fetch(
`${GRAPH_API_URL}/me/message_attachments?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: {
attachment: {
type,
payload: {
url,
is_reusable: true
}
}
}
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Attachment upload failed: ${error.error?.message}`);
}
const data = await response.json();
return data.attachment_id;
}
// Send using attachment ID (faster, no re-upload)
async function sendCachedAttachment(
recipientId: string,
type: AttachmentType,
attachmentId: string
): Promise<SendMessageResponse> {
return sendMediaAttachment(recipientId, type, {
attachment_id: attachmentId
});
}
Note: Media files must be accessible via HTTPS. Facebook downloads the file from your URL, so ensure your server can handle the traffic. For frequently-used media, upload once and reuse the attachment ID.
Human Handover Protocol
The Handover Protocol enables seamless transitions between automated bots and human agents. This is essential for complex support scenarios that require human intervention.
type ThreadOwner = 'primary' | 'secondary';
interface HandoverMetadata {
reason?: string;
conversationHistory?: string;
customData?: Record<string, any>;
}
// Pass thread control to another app (e.g., human agent inbox)
async function passThreadControl(
recipientId: string,
targetAppId: string,
metadata?: HandoverMetadata
): Promise<void> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
const response = await fetch(
`${GRAPH_API_URL}/me/pass_thread_control?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
target_app_id: targetAppId,
metadata: metadata ? JSON.stringify(metadata) : undefined
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Thread handover failed: ${error.error?.message}`);
}
console.log(`Thread control passed to app ${targetAppId}`);
}
// Take thread control back from secondary receiver
async function takeThreadControl(
recipientId: string,
metadata?: HandoverMetadata
): Promise<void> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
const response = await fetch(
`${GRAPH_API_URL}/me/take_thread_control?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
metadata: metadata ? JSON.stringify(metadata) : undefined
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Take thread control failed: ${error.error?.message}`);
}
console.log('Thread control taken back');
}
// Request thread control from primary receiver
async function requestThreadControl(
recipientId: string,
metadata?: HandoverMetadata
): Promise<void> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
const response = await fetch(
`${GRAPH_API_URL}/me/request_thread_control?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
metadata: metadata ? JSON.stringify(metadata) : undefined
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Request thread control failed: ${error.error?.message}`);
}
}
// Handle handover to human agent
async function escalateToHuman(
recipientId: string,
reason: string
): Promise<void> {
// Facebook Page Inbox app ID
const PAGE_INBOX_APP_ID = '263902037430900';
await sendTextMessage(
recipientId,
"I'm connecting you with a human agent. Please hold on a moment."
);
await passThreadControl(recipientId, PAGE_INBOX_APP_ID, {
reason,
conversationHistory: 'User requested human support'
});
}
// Handle postback for support escalation
async function handlePostback(
senderId: string,
postback: { payload: string }
): Promise<void> {
switch (postback.payload) {
case 'CONTACT_SUPPORT':
await escalateToHuman(senderId, 'User requested support');
break;
case 'BROWSE_PRODUCTS':
await sendProductCatalog(senderId);
break;
default:
await sendTextMessage(senderId, 'How can I help you?');
}
}
Message Tags for Follow-ups
Outside the 24-hour messaging window, you need Message Tags to send follow-up messages. These tags indicate the purpose of your message and must be used appropriately.
type MessageTag =
| 'CONFIRMED_EVENT_UPDATE'
| 'POST_PURCHASE_UPDATE'
| 'ACCOUNT_UPDATE'
| 'HUMAN_AGENT';
async function sendTaggedMessage(
recipientId: string,
text: string,
tag: MessageTag
): Promise<SendMessageResponse> {
const pageAccessToken = process.env.FACEBOOK_PAGE_ACCESS_TOKEN!;
const response = await fetch(
`${GRAPH_API_URL}/me/messages?access_token=${pageAccessToken}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: { id: recipientId },
message: { text },
messaging_type: 'MESSAGE_TAG',
tag
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Tagged message failed: ${error.error?.message}`);
}
return response.json();
}
// Example: Order shipping update
async function sendShippingUpdate(
recipientId: string,
orderId: string,
trackingNumber: string
): Promise<void> {
await sendTaggedMessage(
recipientId,
`Great news! Your order #${orderId} has shipped. Track it here: https://example.com/track/${trackingNumber}`,
'POST_PURCHASE_UPDATE'
);
}
// Example: Account security alert
async function sendSecurityAlert(
recipientId: string,
alertType: string
): Promise<void> {
await sendTaggedMessage(
recipientId,
`Security Alert: ${alertType}. If this wasn't you, please secure your account immediately.`,
'ACCOUNT_UPDATE'
);
}
// Example: Human agent follow-up (within 7 days of user message)
async function sendHumanAgentFollowup(
recipientId: string,
message: string
): Promise<void> {
await sendTaggedMessage(
recipientId,
message,
'HUMAN_AGENT'
);
}
Note: Message Tags have strict usage policies. Misuse can result in your bot being restricted or banned. The HUMAN_AGENT tag is only available within 7 days of the user's last message and requires human involvement in the response.
Rate Limits and Best Practices
The facebook messenger api enforces rate limits to ensure platform stability. Understanding these limits helps you build reliable bots.
Rate Limit Overview
| Limit Type | Threshold | Window |
|---|---|---|
| Calls per page | 200 calls | Per hour per user |
| Batch requests | 50 requests | Per batch |
| Send API | 250 messages | Per second per page |
Handling Rate Limits
interface RateLimitState {
requestCount: number;
windowStart: number;
isThrottled: boolean;
}
class RateLimiter {
private state: Map<string, RateLimitState> = new Map();
private readonly maxRequests: number;
private readonly windowMs: number;
constructor(maxRequests: number = 200, windowMs: number = 3600000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
}
async checkLimit(pageId: string): Promise<boolean> {
const now = Date.now();
let state = this.state.get(pageId);
if (!state || now - state.windowStart > this.windowMs) {
state = { requestCount: 0, windowStart: now, isThrottled: false };
}
if (state.requestCount >= this.maxRequests) {
state.isThrottled = true;
this.state.set(pageId, state);
return false;
}
state.requestCount++;
this.state.set(pageId, state);
return true;
}
getResetTime(pageId: string): number {
const state = this.state.get(pageId);
if (!state) return 0;
return Math.max(0, this.windowMs - (Date.now() - state.windowStart));
}
}
const rateLimiter = new RateLimiter();
async function sendWithRateLimiting(
pageId: string,
recipientId: string,
text: string
): Promise<SendMessageResponse | null> {
const canProceed = await rateLimiter.checkLimit(pageId);
if (!canProceed) {
const resetTime = rateLimiter.getResetTime(pageId);
console.warn(`Rate limited. Reset in ${Math.ceil(resetTime / 1000)}s`);
return null;
}
return sendTextMessage(recipientId, text);
}
Best Practices
- Respond quickly: Acknowledge messages within 20 seconds to avoid timeout errors
- Use typing indicators: Show users that your bot is processing their request
- Handle errors gracefully: Provide helpful error messages when things go wrong
- Respect user preferences: Honor opt-out requests and unsubscribe actions
- Cache attachment IDs: Upload media once and reuse the attachment ID
- Batch operations: Group multiple requests when possible
- Implement retry logic: Handle transient failures with exponential backoff
async function sendWithRetry(
recipientId: string,
text: string,
maxRetries: number = 3
): Promise<SendMessageResponse> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await sendTextMessage(recipientId, text);
} catch (error) {
lastError = error as Error;
// Check if error is retryable
const errorMessage = lastError.message.toLowerCase();
const isRetryable =
errorMessage.includes('timeout') ||
errorMessage.includes('temporarily unavailable') ||
errorMessage.includes('rate limit');
if (!isRetryable || attempt === maxRetries) {
throw lastError;
}
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
console.log(`Retry attempt ${attempt}/${maxRetries} in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
Using Late for Messenger Integration
Building and maintaining a Messenger integration requires handling webhooks, managing tokens, processing various message types, and keeping up with API changes. Late simplifies this entire process with a unified API that works across multiple messaging platforms.
Why Use Late for Messenger?
Instead of building custom webhook handlers, token management, and message formatting for each platform, Late provides:
- Unified API: Single endpoint for sending messages to Messenger, WhatsApp, Instagram, and more
- Automatic token refresh: Long-lived tokens managed automatically
- Webhook aggregation: One webhook endpoint for all platforms
- Template abstraction: Write once, render correctly for each platform
- Built-in rate limiting: Automatic handling of platform limits
- Error normalization: Consistent error responses across platforms
Quick Start with Late
import { LateClient } from '@anthropic/late-sdk';
const late = new LateClient({
apiKey: process.env.LATE_API_KEY!
});
// Send a message to Messenger (or any platform)
async function sendMessage(
channelId: string,
recipientId: string,
content: string
): Promise<void> {
await late.messages.send({
channel: channelId,
recipient: recipientId,
content: {
type: 'text',
text: content
}
});
}
// Send a template that works across platforms
async function sendProductCard(
channelId: string,
recipientId: string,
product: { name: string; price: number; imageUrl: string }
): Promise<void> {
await late.messages.send({
channel: channelId,
recipient: recipientId,
content: {
type: 'card',
title: product.name,
subtitle: `$${product.price.toFixed(2)}`,
imageUrl: product.imageUrl,
buttons: [
{ type: 'postback', title: 'Buy Now', payload: 'BUY' },
{ type: 'url', title: 'Details', url: 'https://example.com' }
]
}
});
}
Late handles the platform-specific formatting automatically. The same code works for Messenger, Instagram Direct, WhatsApp Business, and other supported platforms.
Handling Incoming Messages
Late aggregates webhooks from all connected platforms into a single endpoint:
// Single webhook handler for all platforms
app.post('/late-webhook', async (req, res) => {
const event = req.body;
// Platform-agnostic message handling
if (event.type === 'message') {
const { platform, channelId, senderId, content } = event;
console.log(`Message from ${platform}: ${content.text}`);
// Respond using Late's unified API
await late.messages.send({
channel: channelId,
recipient: senderId,
content: {
type: 'text',
text: `Thanks for your message!`
}
});
}
res.sendStatus(200);
});
Getting Started
- Sign up at getlate.dev
- Connect your Facebook Page in the dashboard
- Use Late's unified API to send and receive messages
Late's documentation includes detailed guides for Messenger integration, webhook setup, and message templates.
Building conversational experiences shouldn't require maintaining separate codebases for each platform. With Late, you write your bot logic once and reach users wherever they prefer to communicate.

Miquel is the founder of Late, building the most reliable social media API for developers. Previously built multiple startups and scaled APIs to millions of requests.
View all articlesLearn more about Late with AI
See what AI assistants say about Late API and this topic