Skip to Content
ProvidersMeta Ads (Facebook / Instagram)

Meta Ads (Facebook / Instagram)

Category: Paid Ads
Integration type: Tenant OAuth (stored in integrations table)
External SDK: axios (Meta Graph API v19.0)


Purpose

Meta Ads integration covers:

  1. Pull — Import Facebook and Instagram campaign performance data for reporting (impressions, reach, clicks, conversions, spend)
  2. Push — Upload AI-generated ad copy variants created by the Meta Ads Writer agent into the tenant’s Meta Ads account (as paused ads for HITL review)
  3. Audience sync — List available custom audiences and ad accounts for the agent to target

Tenants authenticate via Facebook Login (OAuth). The platform requires the ads_management and ads_read permissions.


Config Structure

OAuth flow

scope: ads_read,ads_management,business_management Stored in integrations: provider: 'meta_ads' api_key: encrypt(long_lived_access_token) # 60-day token; refreshed periodically metadata: { adAccountId: 'act_123456789', // Meta Ad Account ID (prefixed with 'act_') businessId: '987654321', // Meta Business Manager ID pageId: '111222333', // Facebook Page ID (for Page-linked ads) tokenExpiresAt: string, }

Platform app credentials (env vars)

META_APP_ID=1234567890 META_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx META_API_VERSION=v19.0 META_API_BASE_URL=https://graph.facebook.com/v19.0

Integration Pattern

Pulling campaign performance

class MetaAdsTool { async getCampaignInsights( integration: Integration, options: { startDate: string; // YYYY-MM-DD endDate: string; fields?: string[]; // Default: impressions, reach, clicks, spend, cpm, cpc, ctr }, ): Promise<MetaCampaignInsight[]> { const meta = integration.metadata as MetaAdsMetadata; const token = decrypt(integration.api_key); const fields = options.fields ?? [ 'campaign_id', 'campaign_name', 'impressions', 'reach', 'clicks', 'spend', 'cpm', 'cpc', 'ctr', 'conversions', ]; const response = await axios.get( `${config.META_API_BASE_URL}/${meta.adAccountId}/insights`, { params: { access_token: token, level: 'campaign', time_range: JSON.stringify({ since: options.startDate, until: options.endDate }), fields: fields.join(','), limit: 50, }, }, ); return response.data.data.map((item: any) => ({ campaignId: item.campaign_id, campaignName: item.campaign_name, impressions: Number(item.impressions), reach: Number(item.reach), clicks: Number(item.clicks), spend: Number(item.spend), cpm: Number(item.cpm), cpc: Number(item.cpc), ctr: Number(item.ctr), conversions: item.actions?.find((a: any) => a.action_type === 'purchase')?.value ?? 0, })); }

Creating ad copy (HITL-gated)

Like Google Ads, Meta Ads copy is only pushed after HITL approval:

async createAdCreative( integration: Integration, options: { adAccountId: string; pageId: string; primaryText: string; // Max 125 chars for feed headline: string; // Max 40 chars description: string; // Max 30 chars callToAction: string; // 'LEARN_MORE' | 'SHOP_NOW' | 'SIGN_UP' | ... imageUrl?: string; // Link to creative image (must be uploaded separately) linkUrl: string; // Destination URL }, ): Promise<{ creativeId: string }> { const token = decrypt(integration.api_key); const response = await axios.post( `${config.META_API_BASE_URL}/${options.adAccountId}/adcreatives`, { access_token: token, name: `LM-${Date.now()}`, object_story_spec: { page_id: options.pageId, link_data: { message: options.primaryText, name: options.headline, description: options.description, call_to_action: { type: options.callToAction }, link: options.linkUrl, }, }, }, ); return { creativeId: response.data.id }; } async createAd( integration: Integration, options: { adAccountId: string; adSetId: string; creativeId: string; name: string; }, ): Promise<{ adId: string }> { const token = decrypt(integration.api_key); const response = await axios.post( `${config.META_API_BASE_URL}/${options.adAccountId}/ads`, { access_token: token, name: options.name, adset_id: options.adSetId, creative: { creative_id: options.creativeId }, status: 'PAUSED', // Always paused — HITL approval activates }, ); return { adId: response.data.id }; } }

Token Management

Meta access tokens are long-lived (60 days) but must be refreshed. The platform stores the expiry and refreshes the token 7 days before expiration:

// Scheduled daily job for (const integration of metaIntegrations) { const meta = integration.metadata as MetaAdsMetadata; const daysUntilExpiry = daysBetween(new Date(), new Date(meta.tokenExpiresAt)); if (daysUntilExpiry <= 7) { const newToken = await refreshMetaToken(decrypt(integration.api_key)); await db.update(integrations).set({ api_key: encrypt(newToken) }); } }

Test Cases

Unit tests (packages/tools/src/meta-ads.test.ts)

TestApproach
getCampaignInsights() builds correct API paramsMock axios.get; assert level, time_range, fields
getCampaignInsights() maps spend/cpc to numbersAssert string-to-number coercion on all metric fields
getCampaignInsights() extracts purchase conversions from actionsFeed actions array; assert conversions extracted
createAdCreative() sends correct object_story_specAssert page_id, message, name in body
createAd() creates ad with status: 'PAUSED'Assert status: 'PAUSED' in request body
Throws on 401 (token expired)Mock 401 response; assert error propagated

© 2026 Leadmetrics — Internal use only