Facebook Account Connection Guide for 3rd Party CRM
This guide explains how to implement Facebook account connections in your CRM system to enable messaging capabilities with the dm-voice-agent platform.
Overview
To send and receive messages via Facebook Messenger, your CRM needs to:
- Connect Facebook Business accounts via OAuth
- Register Facebook Pages as channels
- Subscribe to webhook events
- Handle message routing between Facebook and your CRM
Prerequisites
- Facebook App with Messenger API access
- Facebook Business Account verified
- CRM Backend with webhook endpoint capability
- dm-voice-agent API credentials (tenant_id, workspace_id, access token)
- SSL Certificate (required for webhook URLs)
Step 1: Create Facebook App
1.1 Register App on Facebook Developers
- Go to Facebook Developers
- Click "My Apps" → "Create App"
- Select "Business" as app type
- Fill in app details:
- App Name: "YourCRM Messenger Integration"
- App Contact Email: your-email@example.com
- Business Account: Select your business account
1.2 Add Messenger Product
- In your app dashboard, click "Add Product"
- Find "Messenger" and click "Set Up"
- Under "Access Tokens", you'll generate tokens later
1.3 Configure App Settings
json
{
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"required_permissions": [
"pages_messaging",
"pages_manage_metadata",
"pages_read_engagement"
]
}Important Settings:
- App Domain: your-crm.com
- Privacy Policy URL: https://your-crm.com/privacy
- Terms of Service URL: https://your-crm.com/terms
Step 2: Implement Facebook OAuth Flow
2.1 Facebook Connect Popup Flow
Кто открывает попап Facebook OAuth?
В вашем CRM фронтенд (React/Vue/Angular) выполняет роль "meta-connect" скрипта:
typescript
// services/facebook-connect.service.ts
/**
* Открывает попап Facebook OAuth для подключения аккаунта
* Это альтернатива полному редиректу - пользователь остается на странице CRM
*/
export class FacebookConnectService {
/**
* Открыть попап для Facebook OAuth
*/
openFacebookConnectPopup(): Promise<{ access_token: string; pages: Array<any> }> {
return new Promise((resolve, reject) => {
// 1. Генерируем state для CSRF защиты
const state = this.generateState();
// 2. Формируем URL для Facebook OAuth
const authUrl = new URL('https://www.facebook.com/v18.0/dialog/oauth');
authUrl.searchParams.set('client_id', process.env.REACT_APP_FACEBOOK_APP_ID!);
authUrl.searchParams.set('redirect_uri', `${window.location.origin}/auth/facebook/callback`);
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('scope', 'pages_messaging,pages_manage_metadata,pages_read_engagement,pages_show_list,instagram_basic,instagram_manage_messages');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('display', 'popup'); // ВАЖНО: режим попапа
// 3. Открываем попап окно
const popup = window.open(
authUrl.toString(),
'facebook-connect',
'width=600,height=700,scrollbars=yes'
);
if (!popup) {
reject(new Error('Popup blocked'));
return;
}
// 4. Слушаем сообщение от попапа (postMessage API)
const handleMessage = (event: MessageEvent) => {
// Проверяем origin для безопасности
if (event.origin !== window.location.origin) return;
if (event.data.type === 'FACEBOOK_AUTH_SUCCESS') {
window.removeEventListener('message', handleMessage);
popup.close();
resolve(event.data.payload);
}
if (event.data.type === 'FACEBOOK_AUTH_ERROR') {
window.removeEventListener('message', handleMessage);
popup.close();
reject(new Error(event.data.error));
}
};
window.addEventListener('message', handleMessage);
// 5. Проверяем, не закрыл ли пользователь попап
const checkPopupClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkPopupClosed);
window.removeEventListener('message', handleMessage);
reject(new Error('User closed popup'));
}
}, 1000);
});
}
private generateState(): string {
const state = Math.random().toString(36).substring(2, 15);
sessionStorage.setItem('fb_oauth_state', state);
return state;
}
}Callback страница (в попапе):
typescript
// pages/auth/facebook/callback.tsx
/**
* Эта страница загружается В ПОПАПЕ после того, как пользователь
* авторизовался на Facebook и дал разрешения
*/
export function FacebookCallbackPage() {
useEffect(() => {
const handleCallback = async () => {
// 1. Получаем code из URL
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
if (error) {
// Отправляем ошибку родительскому окну
window.opener.postMessage(
{ type: 'FACEBOOK_AUTH_ERROR', error },
window.location.origin
);
return;
}
// 2. Проверяем state
const savedState = sessionStorage.getItem('fb_oauth_state');
if (state !== savedState) {
window.opener.postMessage(
{ type: 'FACEBOOK_AUTH_ERROR', error: 'Invalid state' },
window.location.origin
);
return;
}
try {
// 3. Отправляем code на наш бэкенд для обмена на токены
const response = await fetch('/api/facebook/exchange-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const data = await response.json();
// 4. Отправляем успешный результат родительскому окну
window.opener.postMessage(
{
type: 'FACEBOOK_AUTH_SUCCESS',
payload: {
access_token: data.access_token,
pages: data.pages,
instagram_accounts: data.instagram_accounts
}
},
window.location.origin
);
} catch (error) {
window.opener.postMessage(
{ type: 'FACEBOOK_AUTH_ERROR', error: error.message },
window.location.origin
);
}
};
handleCallback();
}, []);
return <div>Connecting to Facebook...</div>;
}Использование в React компоненте:
tsx
// components/FacebookConnectButton.tsx
export function FacebookConnectButton() {
const [loading, setLoading] = useState(false);
const fbConnect = new FacebookConnectService();
const handleConnect = async () => {
setLoading(true);
try {
// Открываем попап и ждем результата
const result = await fbConnect.openFacebookConnectPopup();
// Получили токены и список страниц/Instagram аккаунтов
console.log('Connected pages:', result.pages);
console.log('Instagram accounts:', result.instagram_accounts);
// Показываем пользователю список для выбора
setPages(result.pages);
setShowPageSelector(true);
} catch (error) {
console.error('Connection failed:', error);
alert('Failed to connect Facebook account');
} finally {
setLoading(false);
}
};
return (
<button onClick={handleConnect} disabled={loading}>
{loading ? 'Connecting...' : 'Connect Facebook & Instagram'}
</button>
);
}Получение Instagram Business аккаунтов:
После OAuth нужно получить Instagram аккаунты, связанные с Facebook Pages:
typescript
// backend/routes/facebook-auth.routes.ts
/**
* POST /api/facebook/exchange-code
* Обменивает code на токены и получает список страниц/Instagram
*/
fastify.post('/api/facebook/exchange-code', async (request, reply) => {
const { code } = request.body as { code: string };
// 1. Обмениваем code на access_token
const tokenResponse = await axios.get(
'https://graph.facebook.com/v18.0/oauth/access_token',
{
params: {
client_id: process.env.FACEBOOK_APP_ID,
client_secret: process.env.FACEBOOK_APP_SECRET,
redirect_uri: `${process.env.APP_BASE_URL}/auth/facebook/callback`,
code: code
}
}
);
const { access_token } = tokenResponse.data;
// 2. Получаем Facebook Pages
const pagesResponse = await axios.get(
'https://graph.facebook.com/v18.0/me/accounts',
{
params: {
access_token: access_token,
fields: 'id,name,access_token,category,picture'
}
}
);
// 3. Для каждой страницы получаем Instagram Business аккаунт
const pagesWithInstagram = await Promise.all(
pagesResponse.data.data.map(async (page: any) => {
try {
const igResponse = await axios.get(
`https://graph.facebook.com/v18.0/${page.id}`,
{
params: {
access_token: page.access_token,
fields: 'instagram_business_account{id,username,profile_picture_url}'
}
}
);
return {
page_id: page.id,
page_name: page.name,
page_access_token: page.access_token,
instagram_account: igResponse.data.instagram_business_account || null
};
} catch (error) {
// Страница не связана с Instagram
return {
page_id: page.id,
page_name: page.name,
page_access_token: page.access_token,
instagram_account: null
};
}
})
);
return {
access_token,
pages: pagesWithInstagram.filter(p => p.instagram_account !== null),
instagram_accounts: pagesWithInstagram
.filter(p => p.instagram_account !== null)
.map(p => p.instagram_account)
};
});2.2 OAuth Configuration
typescript
// config/facebook.ts
export const facebookConfig = {
appId: process.env.FACEBOOK_APP_ID!,
appSecret: process.env.FACEBOOK_APP_SECRET!,
redirectUri: `${process.env.APP_BASE_URL}/auth/facebook/callback`,
scopes: [
'pages_messaging',
'pages_manage_metadata',
'pages_read_engagement',
'pages_show_list'
]
};2.2 Initiate OAuth (Frontend)
typescript
// services/facebook-auth.service.ts
export class FacebookAuthService {
/**
* Generate Facebook OAuth URL
* Called when user clicks "Connect Facebook Account" button
*/
generateAuthUrl(state: string): string {
const params = new URLSearchParams({
client_id: facebookConfig.appId,
redirect_uri: facebookConfig.redirectUri,
state: state, // CSRF protection token
scope: facebookConfig.scopes.join(','),
response_type: 'code'
});
return `https://www.facebook.com/v18.0/dialog/oauth?${params.toString()}`;
}
/**
* Handle OAuth callback
* Called when Facebook redirects back to your app
*/
async handleCallback(code: string, state: string) {
// 1. Verify state token (CSRF protection)
if (!this.verifyState(state)) {
throw new Error('Invalid state parameter');
}
// 2. Exchange code for access token
const tokenResponse = await axios.get(
'https://graph.facebook.com/v18.0/oauth/access_token',
{
params: {
client_id: facebookConfig.appId,
client_secret: facebookConfig.appSecret,
redirect_uri: facebookConfig.redirectUri,
code: code
}
}
);
const { access_token } = tokenResponse.data;
// 3. Exchange short-lived token for long-lived token
const longLivedTokenResponse = await axios.get(
'https://graph.facebook.com/v18.0/oauth/access_token',
{
params: {
grant_type: 'fb_exchange_token',
client_id: facebookConfig.appId,
client_secret: facebookConfig.appSecret,
fb_exchange_token: access_token
}
}
);
return longLivedTokenResponse.data;
}
}2.3 Backend OAuth Route
typescript
// routes/facebook-auth.routes.ts
import { FastifyInstance } from 'fastify';
import { FacebookAuthService } from '../services/facebook-auth.service';
export async function facebookAuthRoutes(fastify: FastifyInstance) {
const fbAuth = new FacebookAuthService();
/**
* GET /auth/facebook/connect
* Redirect user to Facebook OAuth
*/
fastify.get('/auth/facebook/connect', async (request, reply) => {
// Generate CSRF token
const state = crypto.randomBytes(32).toString('hex');
// Store state in session/database
await fastify.redis.setex(
`fb_oauth_state:${state}`,
600, // 10 minutes
request.user.id
);
// Redirect to Facebook
const authUrl = fbAuth.generateAuthUrl(state);
return reply.redirect(authUrl);
});
/**
* GET /auth/facebook/callback
* Handle OAuth callback from Facebook
*/
fastify.get('/auth/facebook/callback', async (request, reply) => {
const { code, state } = request.query as { code: string; state: string };
try {
// Exchange code for token
const tokenData = await fbAuth.handleCallback(code, state);
// Get user ID from state
const userId = await fastify.redis.get(`fb_oauth_state:${state}`);
if (!userId) {
throw new Error('Session expired');
}
// Store access token
await fastify.prisma.facebookAccount.create({
data: {
user_id: userId,
access_token: tokenData.access_token,
token_type: 'long_lived',
expires_at: new Date(Date.now() + (tokenData.expires_in * 1000))
}
});
// Redirect to success page
return reply.redirect('/settings/integrations?fb_connected=true');
} catch (error) {
fastify.log.error(error);
return reply.redirect('/settings/integrations?error=fb_connection_failed');
}
});
}Step 3: Fetch and Register Facebook Pages
3.1 Fetch User's Facebook Pages
typescript
// services/facebook-pages.service.ts
export class FacebookPagesService {
/**
* Fetch all pages user has access to
*/
async getUserPages(userAccessToken: string) {
const response = await axios.get(
'https://graph.facebook.com/v18.0/me/accounts',
{
params: {
access_token: userAccessToken,
fields: 'id,name,access_token,category,picture'
}
}
);
return response.data.data.map((page: any) => ({
page_id: page.id,
page_name: page.name,
page_access_token: page.access_token,
category: page.category,
picture_url: page.picture?.data?.url
}));
}
/**
* Subscribe page to webhook events
*/
async subscribePageToWebhook(pageId: string, pageAccessToken: string) {
const response = await axios.post(
`https://graph.facebook.com/v18.0/${pageId}/subscribed_apps`,
null,
{
params: {
access_token: pageAccessToken,
subscribed_fields: [
'messages',
'messaging_postbacks',
'messaging_optins',
'message_deliveries',
'message_reads'
].join(',')
}
}
);
return response.data;
}
}3.2 Register Page as Channel in dm-voice-agent
typescript
// services/channel-registration.service.ts
export class ChannelRegistrationService {
/**
* Register Facebook Page as a channel in dm-voice-agent
*/
async registerFacebookPageAsChannel(
tenantId: string,
workspaceId: string,
pageData: {
page_id: string;
page_name: string;
page_access_token: string;
},
accessToken: string
) {
// 1. Create channel in dm-voice-agent
const channelResponse = await axios.post(
`${process.env.VOICE_AGENT_API_URL}/v1/channels`,
{
tenant_id: tenantId,
workspace_id: workspaceId,
name: `Facebook: ${pageData.page_name}`,
type: 'facebook_messenger',
provider: 'meta',
resource_ref: pageData.page_id,
config_json: {
page_name: pageData.page_name,
page_access_token: pageData.page_access_token
},
is_active: true
},
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
const channel = channelResponse.data.data;
// 2. Subscribe page to webhooks
const fbPages = new FacebookPagesService();
await fbPages.subscribePageToWebhook(
pageData.page_id,
pageData.page_access_token
);
// 3. Store channel mapping in CRM database
await this.storeCRMChannelMapping(
channel.id,
pageData.page_id,
pageData.page_access_token
);
return channel;
}
/**
* Store channel mapping in CRM
*/
private async storeCRMChannelMapping(
channelId: string,
pageId: string,
pageAccessToken: string
) {
await prisma.crmChannel.create({
data: {
channel_id: channelId,
platform: 'facebook',
platform_page_id: pageId,
page_access_token: pageAccessToken,
status: 'active'
}
});
}
}Step 4: Setup Webhook Endpoint
4.1 Webhook Verification (Facebook Requirement)
typescript
// routes/webhooks/facebook.routes.ts
export async function facebookWebhookRoutes(fastify: FastifyInstance) {
/**
* GET /webhooks/facebook
* Webhook verification endpoint (required by Facebook)
*/
fastify.get('/webhooks/facebook', async (request, reply) => {
const {
'hub.mode': mode,
'hub.verify_token': token,
'hub.challenge': challenge
} = request.query as Record<string, string>;
// Verify token matches your configured token
if (mode === 'subscribe' && token === process.env.FB_WEBHOOK_VERIFY_TOKEN) {
fastify.log.info('Facebook webhook verified');
return reply.code(200).send(challenge);
}
return reply.code(403).send('Forbidden');
});
/**
* POST /webhooks/facebook
* Receive webhook events from Facebook
*/
fastify.post('/webhooks/facebook', async (request, reply) => {
const body = request.body as any;
// Verify webhook signature
const signature = request.headers['x-hub-signature-256'] as string;
if (!verifyFacebookSignature(signature, JSON.stringify(body))) {
return reply.code(401).send('Invalid signature');
}
// Process webhook event
if (body.object === 'page') {
for (const entry of body.entry) {
const pageId = entry.id;
// Process messaging events
if (entry.messaging) {
for (const event of entry.messaging) {
await processMessagingEvent(pageId, event);
}
}
}
}
return reply.code(200).send('EVENT_RECEIVED');
});
}
/**
* Verify Facebook webhook signature
*/
function verifyFacebookSignature(signature: string, payload: string): boolean {
const expectedSignature = crypto
.createHmac('sha256', process.env.FACEBOOK_APP_SECRET!)
.update(payload)
.digest('hex');
return signature === `sha256=${expectedSignature}`;
}4.2 Process Incoming Messages
typescript
// services/facebook-webhook.service.ts
export class FacebookWebhookService {
/**
* Process messaging event from Facebook
*/
async processMessagingEvent(pageId: string, event: any) {
const senderId = event.sender.id;
const recipientId = event.recipient.id;
// Handle different event types
if (event.message) {
await this.handleIncomingMessage(pageId, senderId, event.message);
} else if (event.postback) {
await this.handlePostback(pageId, senderId, event.postback);
} else if (event.delivery) {
await this.handleDeliveryConfirmation(event.delivery);
} else if (event.read) {
await this.handleReadReceipt(event.read);
}
}
/**
* Handle incoming message
*/
async handleIncomingMessage(
pageId: string,
senderId: string,
message: any
) {
// 1. Get channel mapping
const channel = await prisma.crmChannel.findUnique({
where: { platform_page_id: pageId }
});
if (!channel) {
fastify.log.error(`No channel found for page ${pageId}`);
return;
}
// 2. Generate conversation ID
const conversationId = generateConversationId(channel.channel_id, senderId);
// 3. Store message in dm-voice-agent
await axios.post(
`${process.env.VOICE_AGENT_API_URL}/v1/internal/messages`,
{
tenant_id: channel.tenant_id,
channel_id: channel.channel_id,
conversation_id: conversationId,
external_user_id: senderId,
direction: 'inbound',
message_text: message.text || '[attachment]',
message_type: message.text ? 'text' : 'attachment',
external_msg_id: message.mid,
handled_by: 'facebook_webhook',
metadata_json: {
platform: 'facebook',
page_id: pageId,
timestamp: message.timestamp,
attachments: message.attachments || []
}
},
{
headers: {
'Authorization': `Bearer ${process.env.VOICE_AGENT_INTERNAL_TOKEN}`
}
}
);
// 4. Trigger AI agent response (if configured)
await this.triggerAgentResponse(channel.channel_id, conversationId, senderId);
}
/**
* Generate deterministic conversation ID
*/
private generateConversationId(channelId: string, externalUserId: string): string {
const namespace = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
const hash = crypto
.createHash('sha1')
.update(namespace + channelId + externalUserId)
.digest('hex');
return `${hash.substr(0, 8)}-${hash.substr(8, 4)}-5${hash.substr(13, 3)}-${hash.substr(16, 4)}-${hash.substr(20, 12)}`;
}
}Step 5: Send Messages to Facebook
5.1 Send API Implementation
typescript
// services/facebook-send.service.ts
export class FacebookSendService {
/**
* Send message to Facebook user
*/
async sendMessage(
pageAccessToken: string,
recipientId: string,
messageText: string,
messageType: 'text' | 'quick_reply' = 'text'
) {
const response = await axios.post(
'https://graph.facebook.com/v18.0/me/messages',
{
recipient: { id: recipientId },
message: {
text: messageText
},
messaging_type: 'RESPONSE'
},
{
params: {
access_token: pageAccessToken
}
}
);
return response.data;
}
/**
* Send message with quick replies
*/
async sendQuickReply(
pageAccessToken: string,
recipientId: string,
messageText: string,
quickReplies: Array<{ title: string; payload: string }>
) {
const response = await axios.post(
'https://graph.facebook.com/v18.0/me/messages',
{
recipient: { id: recipientId },
message: {
text: messageText,
quick_replies: quickReplies.map(qr => ({
content_type: 'text',
title: qr.title,
payload: qr.payload
}))
},
messaging_type: 'RESPONSE'
},
{
params: {
access_token: pageAccessToken
}
}
);
return response.data;
}
}5.2 Integrate with dm-voice-agent
typescript
// services/agent-integration.service.ts
export class AgentIntegrationService {
/**
* Trigger AI agent to respond to conversation
*/
async triggerAgentResponse(
channelId: string,
conversationId: string,
externalUserId: string
) {
// 1. Get channel configuration
const channel = await prisma.crmChannel.findUnique({
where: { channel_id: channelId },
include: { agent_assignment: true }
});
if (!channel || !channel.agent_assignment) {
// No agent assigned, skip
return;
}
// 2. Get conversation history
const historyResponse = await axios.get(
`${process.env.VOICE_AGENT_API_URL}/v1/messages/conversation`,
{
params: {
tenant_id: channel.tenant_id,
channel_id: channelId,
external_user_id: externalUserId,
limit: 10
},
headers: {
'Authorization': `Bearer ${process.env.VOICE_AGENT_API_TOKEN}`
}
}
);
const messages = historyResponse.data.data.messages;
// 3. Call agent to generate response
const agentResponse = await axios.post(
`${process.env.VOICE_AGENT_API_URL}/v1/agents/${channel.agent_assignment.agent_id}/generate`,
{
conversation_history: messages,
user_id: externalUserId
},
{
headers: {
'Authorization': `Bearer ${process.env.VOICE_AGENT_API_TOKEN}`
}
}
);
// 4. Send response via Facebook
const fbSend = new FacebookSendService();
await fbSend.sendMessage(
channel.page_access_token,
externalUserId,
agentResponse.data.message_text
);
// 5. Store outbound message
await axios.post(
`${process.env.VOICE_AGENT_API_URL}/v1/internal/messages`,
{
tenant_id: channel.tenant_id,
channel_id: channelId,
conversation_id: conversationId,
external_user_id: externalUserId,
direction: 'outbound',
message_text: agentResponse.data.message_text,
message_type: 'text',
handled_by: 'ai_agent',
metadata_json: {
agent_id: channel.agent_assignment.agent_id,
model: agentResponse.data.model
}
},
{
headers: {
'Authorization': `Bearer ${process.env.VOICE_AGENT_INTERNAL_TOKEN}`
}
}
);
}
}Step 6: Configure Facebook App Webhooks
6.1 In Facebook App Dashboard
- Go to Products → Messenger → Settings
- Under Webhooks, click "Add Callback URL"
- Enter:
- Callback URL:
https://your-crm.com/webhooks/facebook - Verify Token: Your chosen verify token (same as
FB_WEBHOOK_VERIFY_TOKEN)
- Callback URL:
- Click "Verify and Save"
- Subscribe to webhook fields:
- ✅ messages
- ✅ messaging_postbacks
- ✅ messaging_optins
- ✅ message_deliveries
- ✅ message_reads
6.2 Test Webhook
bash
# Send test message via Facebook Graph API Explorer
curl -X POST "https://graph.facebook.com/v18.0/me/messages" \
-H "Content-Type: application/json" \
-d '{
"recipient": {"id": "USER_PSID"},
"message": {"text": "Test message"}
}' \
"?access_token=PAGE_ACCESS_TOKEN"Step 7: Database Schema (CRM Side)
sql
-- Store Facebook account connections
CREATE TABLE facebook_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
access_token TEXT NOT NULL,
token_type VARCHAR(50) DEFAULT 'long_lived',
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Store channel mappings
CREATE TABLE crm_channels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL, -- dm-voice-agent channel ID
tenant_id UUID NOT NULL,
platform VARCHAR(50) NOT NULL, -- 'facebook', 'whatsapp', etc.
platform_page_id VARCHAR(255) NOT NULL, -- Facebook Page ID
page_access_token TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(platform_page_id)
);
-- Store webhook events for debugging
CREATE TABLE webhook_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
platform VARCHAR(50) NOT NULL,
event_type VARCHAR(100) NOT NULL,
page_id VARCHAR(255),
sender_id VARCHAR(255),
payload JSONB NOT NULL,
processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);Step 8: Frontend UI Flow
8.1 Settings Page with Connect Button
tsx
// components/FacebookConnectButton.tsx
import React, { useState } from 'react';
export function FacebookConnectButton() {
const [loading, setLoading] = useState(false);
const handleConnect = async () => {
setLoading(true);
// Redirect to OAuth flow
window.location.href = '/auth/facebook/connect';
};
return (
<button
onClick={handleConnect}
disabled={loading}
className="btn btn-primary"
>
{loading ? 'Connecting...' : 'Connect Facebook Account'}
</button>
);
}8.2 Page Selection UI
tsx
// components/FacebookPageSelector.tsx
import React, { useEffect, useState } from 'react';
interface FacebookPage {
page_id: string;
page_name: string;
category: string;
picture_url: string;
}
export function FacebookPageSelector() {
const [pages, setPages] = useState<FacebookPage[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchPages();
}, []);
const fetchPages = async () => {
const response = await fetch('/api/facebook/pages');
const data = await response.json();
setPages(data.pages);
setLoading(false);
};
const handlePageSelect = async (page: FacebookPage) => {
// Register page as channel
await fetch('/api/channels/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
platform: 'facebook',
page_id: page.page_id,
page_name: page.page_name
})
});
alert(`Facebook Page "${page.page_name}" connected successfully!`);
};
return (
<div className="page-selector">
<h3>Select Facebook Pages to Connect</h3>
{loading ? (
<p>Loading pages...</p>
) : (
<ul>
{pages.map(page => (
<li key={page.page_id}>
<img src={page.picture_url} alt={page.page_name} />
<div>
<strong>{page.page_name}</strong>
<span>{page.category}</span>
</div>
<button onClick={() => handlePageSelect(page)}>
Connect
</button>
</li>
))}
</ul>
)}
</div>
);
}Step 9: Testing Checklist
- [ ] OAuth flow works (redirect → callback → token storage)
- [ ] Long-lived token exchange successful
- [ ] Facebook Pages fetched correctly
- [ ] Webhook verification passes
- [ ] Webhook signature validation works
- [ ] Incoming messages stored in dm-voice-agent
- [ ] Conversation IDs generated deterministically
- [ ] Agent responses sent back to Facebook
- [ ] Message history displayed in CRM
- [ ] Token refresh implemented (for expired tokens)
Step 10: Production Considerations
Security
- Store page access tokens encrypted in database
- Rotate webhook verify token periodically
- Implement rate limiting on webhook endpoint
- Validate all incoming webhook signatures
- Use environment variables for secrets
Monitoring
- Log all webhook events
- Alert on failed message deliveries
- Track token expiration dates
- Monitor Facebook API rate limits (200 calls per user per hour)
Error Handling
- Retry failed Facebook API calls with exponential backoff
- Handle token expiration gracefully
- Queue messages if Facebook API is down
- Notify users of connection issues
Compliance
- Implement message retention policies
- Support GDPR deletion requests
- Display clear privacy policy during OAuth
- Obtain user consent for data processing
Environment Variables
bash
# .env
FACEBOOK_APP_ID=your_app_id
FACEBOOK_APP_SECRET=your_app_secret
FB_WEBHOOK_VERIFY_TOKEN=your_verify_token_min_20_chars
# dm-voice-agent API
VOICE_AGENT_API_URL=https://api.dm-voice-agent.com
VOICE_AGENT_API_TOKEN=your_public_api_token
VOICE_AGENT_INTERNAL_TOKEN=your_internal_service_token
# Your app
APP_BASE_URL=https://your-crm.comTroubleshooting
"Webhook verification failed"
- Ensure verify token matches exactly
- Check callback URL is publicly accessible (HTTPS required)
- Verify SSL certificate is valid
"Invalid signature"
- Ensure app secret is correct
- Check payload is used exactly as received (no modifications)
- Use raw request body for signature verification
"Token expired"
- Implement token refresh logic
- Re-authenticate user via OAuth
- Store token expiration date and check before use
"Messages not received"
- Verify page is subscribed to webhook (
subscribed_appsendpoint) - Check webhook endpoint returns 200 status
- Review webhook event logs in Facebook App dashboard
Additional Resources
- Facebook Messenger Platform Documentation
- Facebook OAuth Documentation
- Webhook Reference
- Send API Reference
- Graph API Explorer
Support
For dm-voice-agent API support:
- Email: support@dm-voice-agent.com
- Documentation: https://docs.dm-voice-agent.com
- API Status: https://status.dm-voice-agent.com