Skip to Content
ProvidersFacebook (Pages API)

Facebook (Pages API)

Category: Social Publishing
Integration type: Tenant OAuth (stored in integrations table)
External API: Meta Graph API v19.0


Purpose

Facebook integration covers organic social publishing to a tenant’s Facebook Page. The Social Post Writer agent generates Facebook post copy; the Social Publisher worker pushes it as a scheduled or draft post to the Page via the Graph API.

The platform also pulls Page Insights (reach, impressions, engagement) for use in the Report Writer and Anomaly Detector.

This is distinct from Meta Ads — that integration manages paid campaigns. This integration manages organic Page content.


Config Structure

OAuth flow

scope: pages_manage_posts,pages_read_engagement,pages_show_list,publish_to_groups Stored in integrations: provider: 'facebook' api_key: encrypt(long_lived_page_access_token) metadata: { pageId: '111222333444', pageName: 'Acme Plumbing', accessToken: string, // Page access token (not user token) tokenExpiresAt: string, // Long-lived page tokens don't expire, but track anyway }

Page access token vs User access token

The platform exchanges the user access token (from OAuth) for a Page access token. Page tokens are tied to the Page, not the user, and do not expire. This is the correct token to use for automated posting.


Integration Pattern

Publishing a post

class FacebookTool { constructor( private pageId: string, private accessToken: string, private baseUrl: string = 'https://graph.facebook.com/v19.0', ) {} async publishPost(options: { message: string; // Post copy text link?: string; // Optional URL to attach (shows link preview) imageUrls?: string[]; // URLs of images to attach published?: boolean; // true = publish now; false = create as draft (default: false) scheduledAt?: Date; // Schedule for future time (requires published: false) }): Promise<{ postId: string; postUrl: string }> { // If images provided, create a multi-photo post if (options.imageUrls?.length) { return this.publishPhotoPost(options); } const params: Record<string, any> = { message: options.message, access_token: this.accessToken, }; if (options.link) params.link = options.link; if (options.published === false) params.published = false; if (options.scheduledAt) { params.published = false; params.scheduled_publish_time = Math.floor(options.scheduledAt.getTime() / 1000); } const response = await axios.post( `${this.baseUrl}/${this.pageId}/feed`, params, ); return { postId: response.data.id, postUrl: `https://www.facebook.com/${response.data.id}`, }; } private async publishPhotoPost(options: { message: string; imageUrls: string[]; published?: boolean; }): Promise<{ postId: string; postUrl: string }> { // Upload each photo as unpublished, then attach to a multi-image post const mediaIds = await Promise.all( options.imageUrls.map(url => axios.post(`${this.baseUrl}/${this.pageId}/photos`, { url, published: false, access_token: this.accessToken, }).then(r => ({ media_fbid: r.data.id })), ), ); const response = await axios.post(`${this.baseUrl}/${this.pageId}/feed`, { message: options.message, attached_media: mediaIds, published: options.published ?? false, access_token: this.accessToken, }); return { postId: response.data.id, postUrl: `https://www.facebook.com/${response.data.id}`, }; } async getPageInsights(options: { metrics: string[]; // e.g. ['page_impressions', 'page_engaged_users'] startDate: string; // YYYY-MM-DD endDate: string; period?: string; // 'day' | 'week' | 'month' (default: 'day') }): Promise<PageInsightRow[]> { const response = await axios.get( `${this.baseUrl}/${this.pageId}/insights`, { params: { metric: options.metrics.join(','), since: options.startDate, until: options.endDate, period: options.period ?? 'day', access_token: this.accessToken, }, }, ); return response.data.data.map((metric: any) => ({ name: metric.name, period: metric.period, values: metric.values, // [{ value, end_time }] })); } async getPages(): Promise<{ id: string; name: string; category: string }[]> { const response = await axios.get(`${this.baseUrl}/me/accounts`, { params: { access_token: this.accessToken }, }); return response.data.data.map((p: any) => ({ id: p.id, name: p.name, category: p.category, })); } async verify(): Promise<void> { const response = await axios.get(`${this.baseUrl}/${this.pageId}`, { params: { fields: 'id,name', access_token: this.accessToken, }, }); if (!response.data.id) throw new Error('Facebook Page not found'); } }

Social Calendar → Facebook publish workflow

Social Calendar Planner generates monthly post schedule Social Post Writer creates copy for each Facebook slot ▼ (HITL approval) Social Publisher worker ├── Resolve tenant's Facebook integration ├── Post copy → Graph API /feed (as draft or scheduled) └── Store postId + postUrl in social_posts.external_id + social_posts.publish_url

The platform always creates posts as drafts or scheduled (never auto-publishes with published: true) unless the tenant has explicitly opted in to auto-publish.


Post Limitations

LimitValue
Max text length63,206 characters
Max images per multi-photo post10
Scheduling window10 minutes to 75 days from now
API rate limit200 calls/hour per access token

Test Cases

Unit tests (packages/tools/src/facebook.test.ts)

TestApproach
publishPost() POSTs to /feed with correct paramsMock axios.post; assert message, access_token, published
publishPost() sets scheduled_publish_time when scheduledAt providedAssert Unix timestamp in params
publishPhotoPost() uploads photos first then creates postAssert two axios calls: photos upload then feed post
getPageInsights() builds correct paramsAssert metric, since, until, period
getPages() returns typed page listMock /me/accounts response
verify() throws when page ID not foundMock { error: { code: 100 } }; assert throws

© 2026 Leadmetrics — Internal use only