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:
- Pull — Import Facebook and Instagram campaign performance data for reporting (impressions, reach, clicks, conversions, spend)
- 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)
- 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.0Integration 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)
| Test | Approach |
|---|---|
getCampaignInsights() builds correct API params | Mock axios.get; assert level, time_range, fields |
getCampaignInsights() maps spend/cpc to numbers | Assert string-to-number coercion on all metric fields |
getCampaignInsights() extracts purchase conversions from actions | Feed actions array; assert conversions extracted |
createAdCreative() sends correct object_story_spec | Assert 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 |
Related
- Google Ads Provider — Google campaign management
- Meta Ads Writer Agent — generates Meta ad copy variants
- Ads Analyst Agent — performance analysis
- Tool Integration Layer — OAuth management