Skip to content

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:

  1. Connect Facebook Business accounts via OAuth
  2. Register Facebook Pages as channels
  3. Subscribe to webhook events
  4. 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

  1. Go to Facebook Developers
  2. Click "My Apps""Create App"
  3. Select "Business" as app type
  4. 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

  1. In your app dashboard, click "Add Product"
  2. Find "Messenger" and click "Set Up"
  3. 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:


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

  1. Go to ProductsMessengerSettings
  2. Under Webhooks, click "Add Callback URL"
  3. Enter:
    • Callback URL: https://your-crm.com/webhooks/facebook
    • Verify Token: Your chosen verify token (same as FB_WEBHOOK_VERIFY_TOKEN)
  4. Click "Verify and Save"
  5. 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.com

Troubleshooting

"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_apps endpoint)
  • Check webhook endpoint returns 200 status
  • Review webhook event logs in Facebook App dashboard

Additional Resources


Support

For dm-voice-agent API support:

Twinlix platform documentation.