Meta Ads & Leads Integration Plan
Overview
This document outlines the plan to integrate Meta Marketing API for managing Facebook/Instagram Ads and syncing Leads directly from the JYT platform.
Current State Analysis
Existing Infrastructure ✅
We already have a solid foundation:
-
Socials Module (
src/modules/socials/)SocialPlatformmodel - stores OAuth credentials and API configSocialPostmodel - stores posts with insightsPublishingCampaignmodel - automated publishing campaigns- Services for hashtags, mentions, content generation, insights
-
Social Provider Module (
src/modules/social-provider/)FacebookService- OAuth, page management, post publishingInstagramService- Instagram publishing via Facebook APIContentPublishingService- unified publishing orchestration- Already using Facebook Graph API v24.0
-
Webhooks (
src/api/webhooks/social/facebook/)- Webhook verification and signature validation
- Event processing for post insights
-
OAuth Flow
- Complete Facebook OAuth implementation
- Page access token management
- Scope management (pages, instagram permissions)
What We Need to Add
1. New Permissions (OAuth Scopes)
Add to existing Facebook OAuth flow:
ads_read - Read ad account data
ads_management - Create/manage ads
leads_retrieval - Retrieve lead data from lead ads
pages_manage_ads - Manage ads for Pages
business_management - Manage business assets
2. New Data Models
AdAccount Model
const AdAccount = model.define("AdAccount", {
id: model.id().primaryKey(),
// Meta identifiers
meta_account_id: model.text().unique(), // act_123456789
name: model.text().searchable(),
// Account details
currency: model.text().default("USD"),
timezone: model.text().nullable(),
business_name: model.text().nullable(),
// Status
status: model.enum(["active", "disabled", "pending"]).default("active"),
account_status: model.number().nullable(), // Meta's status code
// Spending
amount_spent: model.bigNumber().default(0),
spend_cap: model.bigNumber().nullable(),
balance: model.bigNumber().nullable(),
// Sync metadata
last_synced_at: model.dateTime().nullable(),
sync_status: model.enum(["synced", "syncing", "error"]).default("synced"),
// Relationships
platform: model.belongsTo(() => SocialPlatform, { foreignKey: "platform_id" }),
campaigns: model.hasMany(() => AdCampaign, { mappedBy: "ad_account" }),
metadata: model.json().nullable(),
})
AdCampaign Model
const AdCampaign = model.define("AdCampaign", {
id: model.id().primaryKey(),
// Meta identifiers
meta_campaign_id: model.text().unique(),
name: model.text().searchable(),
// Campaign config
objective: model.enum([
"OUTCOME_AWARENESS",
"OUTCOME_ENGAGEMENT",
"OUTCOME_LEADS",
"OUTCOME_SALES",
"OUTCOME_TRAFFIC",
"OUTCOME_APP_PROMOTION"
]),
status: model.enum(["ACTIVE", "PAUSED", "DELETED", "ARCHIVED"]).default("PAUSED"),
effective_status: model.text().nullable(),
// Budget
buying_type: model.enum(["AUCTION", "RESERVED"]).default("AUCTION"),
daily_budget: model.bigNumber().nullable(),
lifetime_budget: model.bigNumber().nullable(),
budget_remaining: model.bigNumber().nullable(),
// Schedule
start_time: model.dateTime().nullable(),
stop_time: model.dateTime().nullable(),
// Performance (synced from insights)
impressions: model.bigNumber().default(0),
clicks: model.bigNumber().default(0),
spend: model.bigNumber().default(0),
leads: model.bigNumber().default(0),
// Sync
last_synced_at: model.dateTime().nullable(),
// Relationships
ad_account: model.belongsTo(() => AdAccount, { foreignKey: "ad_account_id" }),
ad_sets: model.hasMany(() => AdSet, { mappedBy: "campaign" }),
metadata: model.json().nullable(),
})
AdSet Model
const AdSet = model.define("AdSet", {
id: model.id().primaryKey(),
// Meta identifiers
meta_adset_id: model.text().unique(),
name: model.text().searchable(),
// Status
status: model.enum(["ACTIVE", "PAUSED", "DELETED", "ARCHIVED"]).default("PAUSED"),
effective_status: model.text().nullable(),
// Budget & Bidding
daily_budget: model.bigNumber().nullable(),
lifetime_budget: model.bigNumber().nullable(),
bid_amount: model.bigNumber().nullable(),
billing_event: model.enum(["IMPRESSIONS", "LINK_CLICKS", "APP_INSTALLS"]).default("IMPRESSIONS"),
optimization_goal: model.text().nullable(),
// Targeting (JSON - complex structure)
targeting: model.json().nullable(),
// Schedule
start_time: model.dateTime().nullable(),
end_time: model.dateTime().nullable(),
// Performance
impressions: model.bigNumber().default(0),
clicks: model.bigNumber().default(0),
spend: model.bigNumber().default(0),
leads: model.bigNumber().default(0),
// Relationships
campaign: model.belongsTo(() => AdCampaign, { foreignKey: "campaign_id" }),
ads: model.hasMany(() => Ad, { mappedBy: "ad_set" }),
metadata: model.json().nullable(),
})
Ad Model
const Ad = model.define("Ad", {
id: model.id().primaryKey(),
// Meta identifiers
meta_ad_id: model.text().unique(),
name: model.text().searchable(),
// Status
status: model.enum(["ACTIVE", "PAUSED", "DELETED", "ARCHIVED"]).default("PAUSED"),
effective_status: model.text().nullable(),
// Creative
creative_id: model.text().nullable(),
preview_url: model.text().nullable(),
// Performance
impressions: model.bigNumber().default(0),
clicks: model.bigNumber().default(0),
spend: model.bigNumber().default(0),
leads: model.bigNumber().default(0),
ctr: model.float().nullable(),
cpc: model.float().nullable(),
cpm: model.float().nullable(),
// Relationships
ad_set: model.belongsTo(() => AdSet, { foreignKey: "ad_set_id" }),
metadata: model.json().nullable(),
})
LeadForm Model
const LeadForm = model.define("LeadForm", {
id: model.id().primaryKey(),
// Meta identifiers
meta_form_id: model.text().unique(),
name: model.text().searchable(),
// Form config
status: model.enum(["ACTIVE", "ARCHIVED", "DELETED"]).default("ACTIVE"),
locale: model.text().nullable(),
// Form structure (JSON)
questions: model.json().nullable(), // Array of form fields
privacy_policy_url: model.text().nullable(),
thank_you_page_url: model.text().nullable(),
// Stats
leads_count: model.number().default(0),
// Relationships
page_id: model.text(), // Facebook Page ID
ad_account: model.belongsTo(() => AdAccount, { foreignKey: "ad_account_id" }),
leads: model.hasMany(() => Lead, { mappedBy: "lead_form" }),
metadata: model.json().nullable(),
})
Lead Model (Core - Most Important!)
const Lead = model.define("Lead", {
id: model.id().primaryKey(),
// Meta identifiers
meta_lead_id: model.text().unique(),
// Lead data
email: model.text().nullable(),
phone: model.text().nullable(),
full_name: model.text().nullable(),
first_name: model.text().nullable(),
last_name: model.text().nullable(),
// Additional fields (from form)
field_data: model.json().nullable(), // All form responses
// Source tracking
ad_id: model.text().nullable(),
ad_name: model.text().nullable(),
adset_id: model.text().nullable(),
adset_name: model.text().nullable(),
campaign_id: model.text().nullable(),
campaign_name: model.text().nullable(),
form_id: model.text().nullable(),
// Timestamps
created_time: model.dateTime(), // When lead was submitted
// Processing status
status: model.enum([
"new", // Just received
"contacted", // Reached out
"qualified", // Qualified lead
"converted", // Became customer
"disqualified", // Not a fit
"archived" // Archived
]).default("new"),
// Internal tracking
notes: model.text().nullable(),
assigned_to: model.text().nullable(), // User ID
contacted_at: model.dateTime().nullable(),
converted_at: model.dateTime().nullable(),
// Relationships
lead_form: model.belongsTo(() => LeadForm, { foreignKey: "lead_form_id" }),
platform: model.belongsTo(() => SocialPlatform, { foreignKey: "platform_id" }),
// Link to Person module (optional)
person_id: model.text().nullable(),
metadata: model.json().nullable(),
})
3. Meta Marketing API Service
Create src/modules/social-provider/meta-ads-service.ts:
export default class MetaAdsService {
private readonly API_VERSION = "v24.0"
// ============ AD ACCOUNTS ============
/** List ad accounts accessible by the user */
async listAdAccounts(userAccessToken: string): Promise<AdAccountData[]>
/** Get ad account details */
async getAdAccount(accountId: string, accessToken: string): Promise<AdAccountData>
// ============ CAMPAIGNS ============
/** List campaigns for an ad account */
async listCampaigns(accountId: string, accessToken: string): Promise<CampaignData[]>
/** Create a new campaign */
async createCampaign(accountId: string, data: CreateCampaignInput, accessToken: string): Promise<CampaignData>
/** Update campaign (status, budget, etc.) */
async updateCampaign(campaignId: string, data: UpdateCampaignInput, accessToken: string): Promise<CampaignData>
/** Pause/Resume campaign */
async setCampaignStatus(campaignId: string, status: "ACTIVE" | "PAUSED", accessToken: string): Promise<void>
// ============ AD SETS ============
/** List ad sets for a campaign */
async listAdSets(campaignId: string, accessToken: string): Promise<AdSetData[]>
/** Create ad set with targeting */
async createAdSet(campaignId: string, data: CreateAdSetInput, accessToken: string): Promise<AdSetData>
/** Update ad set */
async updateAdSet(adSetId: string, data: UpdateAdSetInput, accessToken: string): Promise<AdSetData>
// ============ ADS ============
/** List ads for an ad set */
async listAds(adSetId: string, accessToken: string): Promise<AdData[]>
/** Create ad with creative */
async createAd(adSetId: string, data: CreateAdInput, accessToken: string): Promise<AdData>
// ============ LEAD FORMS ============
/** List lead forms for a page */
async listLeadForms(pageId: string, accessToken: string): Promise<LeadFormData[]>
/** Get lead form details */
async getLeadForm(formId: string, accessToken: string): Promise<LeadFormData>
// ============ LEADS ============
/** Retrieve leads from a form (bulk) */
async getLeads(formId: string, accessToken: string, options?: {
since?: Date
until?: Date
limit?: number
}): Promise<LeadData[]>
/** Get single lead details */
async getLead(leadId: string, accessToken: string): Promise<LeadData>
// ============ INSIGHTS ============
/** Get insights for ad account/campaign/adset/ad */
async getInsights(objectId: string, level: "account" | "campaign" | "adset" | "ad", accessToken: string, options?: {
date_preset?: string
time_increment?: number
fields?: string[]
breakdowns?: string[]
}): Promise<InsightsData>
}
4. Lead Ads Webhook Handler
Extend existing webhook infrastructure:
File: src/api/webhooks/social/facebook/leadgen/route.ts
// Webhook for leadgen events
// Facebook sends: { object: "page", entry: [{ changes: [{ field: "leadgen", value: {...} }] }] }
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
// 1. Validate signature
// 2. Parse leadgen event
// 3. Fetch full lead data from API
// 4. Store in Lead model
// 5. Trigger notification workflow
// 6. Return 200 OK immediately
}
Webhook Event Structure:
{
"object": "page",
"entry": [{
"id": "PAGE_ID",
"time": 1234567890,
"changes": [{
"field": "leadgen",
"value": {
"leadgen_id": "LEAD_ID",
"page_id": "PAGE_ID",
"form_id": "FORM_ID",
"adgroup_id": "AD_ID",
"ad_id": "AD_ID",
"created_time": 1234567890
}
}]
}]
}
5. API Routes
Ad Accounts
GET /admin/meta-ads/accounts - List connected ad accounts
POST /admin/meta-ads/accounts/sync - Sync ad accounts from Meta
GET /admin/meta-ads/accounts/:id - Get account details
GET /admin/meta-ads/accounts/:id/insights - Get account insights
Campaigns
GET /admin/meta-ads/campaigns - List all campaigns
POST /admin/meta-ads/campaigns - Create campaign
GET /admin/meta-ads/campaigns/:id - Get campaign details
PATCH /admin/meta-ads/campaigns/:id - Update campaign
POST /admin/meta-ads/campaigns/:id/pause - Pause campaign
POST /admin/meta-ads/campaigns/:id/resume - Resume campaign
GET /admin/meta-ads/campaigns/:id/insights - Get campaign insights
POST /admin/meta-ads/campaigns/sync - Sync campaigns from Meta
Ad Sets
GET /admin/meta-ads/adsets - List all ad sets
POST /admin/meta-ads/adsets - Create ad set
GET /admin/meta-ads/adsets/:id - Get ad set details
PATCH /admin/meta-ads/adsets/:id - Update ad set
GET /admin/meta-ads/adsets/:id/insights - Get ad set insights
Ads
GET /admin/meta-ads/ads - List all ads
POST /admin/meta-ads/ads - Create ad
GET /admin/meta-ads/ads/:id - Get ad details
PATCH /admin/meta-ads/ads/:id - Update ad
GET /admin/meta-ads/ads/:id/insights - Get ad insights
Lead Forms
GET /admin/meta-ads/lead-forms - List lead forms
GET /admin/meta-ads/lead-forms/:id - Get form details
POST /admin/meta-ads/lead-forms/sync - Sync forms from Meta
Leads (Most Important!)
GET /admin/meta-ads/leads - List all leads (with filters)
GET /admin/meta-ads/leads/:id - Get lead details
PATCH /admin/meta-ads/leads/:id - Update lead status/notes
POST /admin/meta-ads/leads/:id/convert - Mark as converted
POST /admin/meta-ads/leads/sync - Sync leads from Meta
POST /admin/meta-ads/leads/export - Export leads to CSV
6. Workflows
Sync Ad Account Workflow
// Syncs ad account data, campaigns, ad sets, ads from Meta
export const syncAdAccountWorkflow = createWorkflow(...)
Process Lead Workflow
// Triggered by webhook or manual sync
// 1. Fetch full lead data
// 2. Store in database
// 3. Create Person record (optional)
// 4. Send notification
// 5. Trigger automation (email, CRM sync)
export const processLeadWorkflow = createWorkflow(...)
Sync Insights Workflow
// Scheduled job to sync performance data
export const syncAdsInsightsWorkflow = createWorkflow(...)
7. Admin UI Components
Meta Ads Dashboard
- Overview of all ad accounts
- Performance metrics (spend, leads, conversions)
- Quick actions (pause/resume campaigns)
Campaigns List
- DataTable with campaigns
- Status badges
- Performance columns
- Bulk actions
Leads Management (Priority!)
- Leads list with filters (status, date, campaign, form)
- Lead detail view
- Status workflow (new → contacted → qualified → converted)
- Notes and activity log
- Export functionality
- Link to Person module
Lead Form Viewer
- View form structure
- See questions/fields
- Lead count per form
Implementation Phases
Phase 1: Foundation (Week 1)
- ✅ Update OAuth scopes for ads permissions
- Create new data models (AdAccount, Campaign, AdSet, Ad, LeadForm, Lead)
- Run migrations
- Create MetaAdsService with basic methods
Phase 2: Leads Integration (Week 2) - PRIORITY
- Implement Lead webhook handler
- Create leads API routes
- Build leads sync workflow
- Create leads admin UI
Phase 3: Ads Management (Week 3)
- Implement campaign/adset/ad sync
- Create ads management API routes
- Build ads admin UI
- Implement insights sync
Phase 4: Advanced Features (Week 4)
- Scheduled insights sync job
- Lead automation workflows
- Person module integration
- Export/reporting features
Required Environment Variables
# Existing (already have)
FACEBOOK_CLIENT_ID=your_app_id
FACEBOOK_CLIENT_SECRET=your_app_secret
FACEBOOK_WEBHOOK_VERIFY_TOKEN=your_verify_token
# New (add to existing)
# No new env vars needed - uses same Facebook app
# Just need to add new OAuth scopes
OAuth Scope Update
Update src/modules/social-provider/facebook-service.ts:
// Current scope
const defaultScope = "pages_show_list,pages_manage_posts,pages_read_engagement,instagram_basic,instagram_content_publish,instagram_manage_comments,instagram_manage_insights,instagram_manage_messages";
// New scope (add ads permissions)
const defaultScope = "pages_show_list,pages_manage_posts,pages_read_engagement,instagram_basic,instagram_content_publish,instagram_manage_comments,instagram_manage_insights,instagram_manage_messages,ads_read,ads_management,leads_retrieval,pages_manage_ads,business_management";
Webhook Subscription Update
In Facebook App Dashboard, add subscription for:
- Object:
page - Field:
leadgen(for lead ads)
Data Flow Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ META ADS PLATFORM │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Campaigns│ │ Ad Sets │ │ Ads │ │ Leads │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼─────────────┼─────────────┼─────────────┼──────────────────┘
│ │ │ │
│ Sync API │ │ Webhook │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ JYT PLATFORM │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ MetaAdsService │ │
│ │ • listAdAccounts() • getInsights() • getLeads() │ │
│ │ • syncCampaigns() • createAd() • processLead() │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Data Models │ │
│ │ AdAccount → Campaign → AdSet → Ad │ │
│ │ LeadForm → Lead → Person (optional link) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Admin UI │ │
│ │ • Ads Dashboard • Leads List • Campaign Manager │ │
│ │ • Insights Charts • Lead Details • Export/Reports │ │
│ └──────────────────────────────────────── ─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Success Metrics
- Lead Capture: All leads from Meta ads synced in real-time
- Lead Management: Full workflow from new → converted
- Ads Visibility: View all campaigns, ad sets, ads in one place
- Performance Tracking: Insights synced and displayed
- Automation: Leads automatically create Person records
Next Steps
- Approve this plan - Review and confirm approach
- Start Phase 1 - Create models and service
- Priority: Leads - Focus on lead capture first
- Iterate - Add features based on usage