Skip to content

Meta Ads

Create and boost Facebook + Instagram ads via Zernio API - Campaigns, Custom Audiences, Lookalikes, Pixels, and analytics

Included with the Usage plan. No Meta App review needed. Zernio is an approved Meta Marketing Partner, so you skip the App Review + ads_management permissions entirely.

accountId accepts either shape. You can pass the posting account ID (facebook / instagram) or the ads credential account ID (metaads), Zernio resolves the sibling internally. For Instagram ads, Zernio auto-resolves the linked Facebook Page and the Instagram Business Account ID (no extra field needed). If the Instagram account has no linked Facebook account, the API returns code: "linked_account_required" so you can surface a reconnect flow.

What's Supported

FeatureStatus
Standalone campaigns (FB + IG)Yes
Multi-creative campaigns (1 campaign → N ads)Yes
Attach creative to existing ad setYes
Video creatives (all 3 shapes)Yes
Boost organic postsYes
CBO vs ABO budget routing + updatesYes
Campaign duplication (deep copy)Yes
Campaign deletion (cascades to ad sets + ads)Yes
Bulk pause / resume campaignsYes
Ad-set-scoped pause / resumeYes
Custom Audiences (customer list, website, lookalike)Yes
Detailed targeting (interests, age, gender, location)Yes
Campaign > Ad Set > Ad hierarchyYes
Real-time analytics (spend, CPC, CPM)Yes
ROAS + action_values (revenue per action_type)Yes
Pixel management — create, install code, rename, share, stats (details)Yes
Conversions API (offline events, hashed PII, dedup)Yes
Click-to-WhatsApp ads (CTWA)Yes
Ad comments (read + moderate dark posts)Yes
Ad Library APIRoadmap
Advantage+ campaignsRoadmap

Ad Comments

Comments on ad creatives (including dark posts, ads created directly in Meta Ads Manager that never went live organically on the Page feed) aren't reachable through the standard GET /v1/inbox/comments/{postId} endpoint, because dark posts don't live in Zernio's post database and Meta's public media-level Graph API endpoints don't resolve them.

Use GET /v1/ads/{adId}/comments instead. It resolves the creative's underlying story via the Marketing API (effective_object_story_id on Facebook, effective_instagram_media_id on Instagram), then fetches the comments from the Graph API. The response matches the inbox endpoint shape so you can reuse the same rendering logic.

typescript
const { comments, meta } = await zernio.ads.getAdComments({
  path: { adId: 'AD_ID' },
  query: { limit: 50 },
});

console.log(meta.platform);         // 'facebook' | 'instagram'
console.log(meta.effectiveStoryId); // underlying post ID

for (const c of comments) {
  console.log(c.from.name, c.message);
}
python
res = client.ads.get_ad_comments(
    ad_id="AD_ID",
    limit=50,
)
for c in res["comments"]:
    print(c["from"]["name"], c["message"])
bash
curl "https://api.zernio.com/v1/ads/AD_ID/comments?limit=50" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Returns ad_not_commentable when the creative format does not expose an underlying commentable post (Story ads, Dynamic Product Ads). Returns feature_not_available for non-Meta ad platforms.

Boost a Post

typescript
const ad = await zernio.ads.boostPost({ body: {
  postId: 'POST_ID',
  accountId: 'ACCOUNT_ID',
  adAccountId: 'act_1234567890',
  name: 'Spring launch boost',
  goal: 'traffic',
  budget: { amount: 40, type: 'daily' },
  schedule: { startDate: '2026-04-20', endDate: '2026-04-27' },
  targeting: {
    ageMin: 25,
    ageMax: 45,
    countries: ['US', 'CA'],
    interests: [{ id: '6003139266461', name: 'DevOps' }]
  }
}});
python
ad = client.ads.boost_post(
    post_id="POST_ID",
    account_id="ACCOUNT_ID",
    ad_account_id="act_1234567890",
    name="Spring launch boost",
    goal="traffic",
    budget={"amount": 40, "type": "daily"},
    schedule={"startDate": "2026-04-20", "endDate": "2026-04-27"},
    targeting={"ageMin": 25, "ageMax": 45, "countries": ["US", "CA"]},
)
bash
curl -X POST "https://zernio.com/api/v1/ads/boost" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "postId": "POST_ID",
    "accountId": "ACCOUNT_ID",
    "adAccountId": "act_1234567890",
    "name": "Spring launch boost",
    "goal": "traffic",
    "budget": { "amount": 40, "type": "daily" },
    "schedule": { "startDate": "2026-04-20", "endDate": "2026-04-27" },
    "targeting": {
      "ageMin": 25,
      "ageMax": 45,
      "countries": ["US", "CA"]
    }
  }'

Create a Standalone Campaign

typescript
const ad = await zernio.ads.createStandaloneAd({ body: {
  accountId: 'acc_metaads_123',
  adAccountId: 'act_1234567890',
  name: 'Spring sale - US Feed',
  goal: 'traffic',
  budgetAmount: 75,
  budgetType: 'daily',
  headline: 'Spring Sale - 30% off',
  body: 'Limited time. Upgrade today.',
  imageUrl: 'https://cdn.example.com/spring.jpg',
  callToAction: 'SHOP_NOW',
  linkUrl: 'https://example.com/spring',
  countries: ['US'],
  ageMin: 25,
  ageMax: 55,
}});
python
ad = client.ads.create_standalone_ad(
    account_id="acc_metaads_123",
    ad_account_id="act_1234567890",
    name="Spring sale - US Feed",
    goal="traffic",
    budget_amount=75,
    budget_type="daily",
    headline="Spring Sale - 30% off",
    body="Limited time. Upgrade today.",
    image_url="https://cdn.example.com/spring.jpg",
    call_to_action="SHOP_NOW",
    link_url="https://example.com/spring",
    countries=["US"],
    age_min=25,
    age_max=55,
)
bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acc_metaads_123",
    "adAccountId": "act_1234567890",
    "name": "Spring sale - US Feed",
    "goal": "traffic",
    "budgetAmount": 75,
    "budgetType": "daily",
    "headline": "Spring Sale - 30% off",
    "body": "Limited time. Upgrade today.",
    "imageUrl": "https://cdn.example.com/spring.jpg",
    "callToAction": "SHOP_NOW",
    "linkUrl": "https://example.com/spring",
    "countries": ["US"],
    "ageMin": 25,
    "ageMax": 55,
    "interests": [{ "id": "6003139266461", "name": "DevOps" }]
  }'

For goal: "conversions", "lead_generation", or "app_promotion", see Conversion campaigns and promoted objects below — Meta requires a promotedObject (Pixel + event, Page, or App) on the ad set for those optimization goals.

/v1/ads/create uses a flat body (every field at the top level). The /v1/ads/boost endpoint is different, it uses nested budget, schedule, and targeting objects. Don't mix the two shapes. Platform is inferred from accountId, so no platform field in the body.

Manage Campaigns & Ad Sets

Zernio exposes the full Meta campaign tree with CBO/ABO awareness, review-state, and budget-level routing, enough to build a Meta Ads Manager replacement on top of GET /v1/ads/tree and the write endpoints below.

Bid strategy

Meta's full bid-strategy enum is supported on writes (POST /v1/ads/create, POST /v1/ads/boost, PUT /v1/ads/ad-sets/{adSetId}, PUT /v1/ads/campaigns/{campaignId}) and surfaced on reads (GET /v1/ads/tree, /v1/ads/campaigns, /v1/ads/{adId}).

bidStrategyRequired companion fieldNotes
LOWEST_COST_WITHOUT_CAP (default)Auto-bid; Meta optimizes to spend the budget.
LOWEST_COST_WITH_BID_CAPbidAmount (whole currency units)Auto-bid with a hard ceiling.
COST_CAPbidAmount (whole currency units)Target average cost per result.
LOWEST_COST_WITH_MIN_ROASroasAverageFloor (decimal multiplier, e.g. 2.0 = 2.0× ROAS)Requires a value-optimized campaign (e.g. OUTCOME_SALES with a connected pixel/dataset).

bidAmount is whole currency units of the ad account (USD: 5 = $5.00; JPY: 100 = ¥100). Internally converted to Meta's smallest-denomination integer (cents for USD).

roasAverageFloor is a decimal multiplier; we encode it as Meta's bid_constraints.roas_average_floor × 10000 (so 2.020000).

Create an ad with a $5 cost cap:

typescript
const result = await zernio.ads.createStandaloneAd({
  body: {
    accountId: 'ACCOUNT_ID',
    adAccountId: 'act_123',
    name: 'Spring sale',
    goal: 'conversions',
    budgetAmount: 50,
    budgetType: 'daily',
    headline: 'Spring sale',
    body: '20% off everything',
    callToAction: 'SHOP_NOW',
    linkUrl: 'https://example.com',
    imageUrl: 'https://cdn.example.com/banner.jpg',
    bidStrategy: 'COST_CAP',
    bidAmount: 5,
    promotedObject: { pixelId: '1729525464415281', customEventType: 'PURCHASE' },
  },
});
python
result = client.ads.create_standalone_ad(
    account_id="ACCOUNT_ID",
    ad_account_id="act_123",
    name="Spring sale",
    goal="conversions",
    budget_amount=50,
    budget_type="daily",
    headline="Spring sale",
    body="20% off everything",
    call_to_action="SHOP_NOW",
    link_url="https://example.com",
    image_url="https://cdn.example.com/banner.jpg",
    bid_strategy="COST_CAP",
    bid_amount=5,
    promoted_object={"pixelId": "1729525464415281", "customEventType": "PURCHASE"},
)
bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "ACCOUNT_ID",
    "adAccountId": "act_123",
    "name": "Spring sale",
    "goal": "conversions",
    "budgetAmount": 50,
    "budgetType": "daily",
    "headline": "Spring sale",
    "body": "20% off everything",
    "callToAction": "SHOP_NOW",
    "linkUrl": "https://example.com",
    "imageUrl": "https://cdn.example.com/banner.jpg",
    "bidStrategy": "COST_CAP",
    "bidAmount": 5,
    "promotedObject": { "pixelId": "1729525464415281", "customEventType": "PURCHASE" }
  }'

Switch an ad set to ROAS-floor bidding:

typescript
await zernio.adcampaigns.updateAdSet({
  path: { adSetId: 'AD_SET_ID' },
  body: {
    platform: 'facebook',
    bidStrategy: 'LOWEST_COST_WITH_MIN_ROAS',
    roasAverageFloor: 2.5,
  },
});
python
client.ad_campaigns.update_ad_set(
    ad_set_id="AD_SET_ID",
    platform="facebook",
    bid_strategy="LOWEST_COST_WITH_MIN_ROAS",
    roas_average_floor=2.5,
)
bash
curl -X PUT "https://zernio.com/api/v1/ads/ad-sets/AD_SET_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "facebook",
    "bidStrategy": "LOWEST_COST_WITH_MIN_ROAS",
    "roasAverageFloor": 2.5
  }'

Campaign-level PUT /v1/ads/campaigns/{campaignId} accepts bidStrategy only — Meta's spec has no bid_amount or bid_constraints at the campaign level (those live on the ad set). Campaign-level bid edits also require the campaign to be CBO (campaign-level budget); ABO campaigns return 409 with a pointer to the ad-set endpoint.

ROAS + revenue-per-event

Every metrics object on /v1/ads/tree, /v1/ads/campaigns, and /v1/ads/{adId} now carries three Meta-specific monetary fields alongside the existing actions + conversions counts:

FieldTypeNotes
actionValues{[action_type]: number}Monetary mirror of actions, from Meta's action_values[]. Values in ad-account native currency (see the campaign's currency field). Populated for the same action types Meta reports values on (purchases, AddToCart with value, etc.).
purchaseValuenumberConvenience sum of purchase-type action values, picked from actionValues via the same priority list as conversions (offsite_conversion.fb_pixel_purchaseomni_purchasepurchase). Same unit as spend.
roasnumberDerived purchaseValue / spend. Recomputed from summed numerator + denominator at ad-set and campaign levels (not averaged across children) so the rollup is mathematically correct. Equivalent to Meta's purchase_roas under default attribution.

Example campaign rollup for a purchase campaign:

json
"metrics": {
  "spend": 493.39,
  "purchaseValue": 2456.78,
  "roas": 4.98,
  "conversions": 42,
  "costPerConversion": 11.75,
  "actions": { "offsite_conversion.fb_pixel_purchase": 42, "add_to_cart": 138, "link_click": 1205 },
  "actionValues": { "offsite_conversion.fb_pixel_purchase": 2456.78, "add_to_cart": 4230.50 }
}

For cost-per-AddToCart, cost-per-Lead, etc., read the relevant actions[key] divided by spend. Pick offsite_conversion.fb_pixel_* keys in actions / actionValues when available to avoid Meta's parallel pixel+omni+canonical reporting (same conversion counted under multiple keys).

Reading the campaign tree

GET /v1/ads/tree returns nested Campaign → Ad Set → Ad with rolled-up metrics (including conversions and raw actions counts). Each campaign node exposes:

FieldTypeNotes
statusactive | paused | pending_review | …Delivery status derived from child ads
reviewStatusin_review | approved | rejected | with_issues | nullPlatform-side review, distinct from status
platformCampaignStatusstringRaw Meta Campaign.effective_status
campaignIssuesInfoobject[] | nullMeta's raw issues_info[] when delivery issues exist
budgetLevelcampaign | adset | nullCanonical CBO/ABO switch
campaignBudget{ amount, type } | nullPopulated only for CBO
adSetBudget (on ad-set nodes){ amount, type } | nullPopulated only for ABO
isBudgetScheduleEnabledbooleanMirrors Meta Campaign.is_budget_schedule_enabled
currencystringISO 4217 code, budgets are in ad-account native currency, NOT normalized

Default ?source=all (matches the Zernio UI) returns both Zernio-created and platform-discovered ads. Pass ?source=zernio to restrict.

Update a CBO campaign budget

Use PUT /v1/ads/campaigns/{campaignId} when budgetLevel === 'campaign'. If you call this on an ABO campaign, Zernio returns 409 with code: "BUDGET_LEVEL_MISMATCH", route to the ad-set endpoint instead.

typescript
await zernio.adcampaigns.updateAdCampaign({
  path: { campaignId: 'CAMPAIGN_ID' },
  body: {
    platform: 'facebook',
    budget: { amount: 250, type: 'daily' },
  },
});
python
client.ad_campaigns.update_ad_campaign(
    campaign_id="CAMPAIGN_ID",
    platform="facebook",
    budget={"amount": 250, "type": "daily"},
)
bash
curl -X PUT "https://zernio.com/api/v1/ads/campaigns/CAMPAIGN_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "facebook",
    "budget": { "amount": 250, "type": "daily" }
  }'

Update an ABO ad-set (budget and/or status)

PUT /v1/ads/ad-sets/{adSetId} when budgetLevel === 'adset'. Supply budget and/or status, at least one is required. Returns 409 with code: "BUDGET_LEVEL_MISMATCH" if the parent campaign is CBO.

typescript
await zernio.adcampaigns.updateAdSet({
  path: { adSetId: 'ADSET_ID' },
  body: {
    platform: 'facebook',
    budget: { amount: 75, type: 'daily' },
    status: 'active',
  },
});
python
client.ad_campaigns.update_ad_set(
    ad_set_id="ADSET_ID",
    platform="facebook",
    budget={"amount": 75, "type": "daily"},
    status="active",
)
bash
curl -X PUT "https://zernio.com/api/v1/ads/ad-sets/ADSET_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "facebook",
    "budget": { "amount": 75, "type": "daily" },
    "status": "active"
  }'

For status-only toggles, the symmetric PUT /v1/ads/ad-sets/{adSetId}/status mirrors /v1/ads/campaigns/{campaignId}/status.

Pause / resume a campaign (single or bulk)

Single campaign, cascades to all ad sets and ads:

typescript
await zernio.adcampaigns.updateAdCampaignStatus({
  path: { campaignId: 'CAMPAIGN_ID' },
  body: { status: 'paused', platform: 'facebook' },
});
python
client.ad_campaigns.update_ad_campaign_status(
    campaign_id="CAMPAIGN_ID",
    status="paused",
    platform="facebook",
)
bash
curl -X PUT "https://zernio.com/api/v1/ads/campaigns/CAMPAIGN_ID/status" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "paused", "platform": "facebook" }'

Bulk (up to 50 campaigns), each row's result is reported independently so one failure doesn't fail the whole batch:

typescript
await zernio.adcampaigns.bulkUpdateAdCampaignStatus({
  body: {
    status: 'paused',
    campaigns: [
      { platformCampaignId: '1202...', platform: 'facebook' },
      { platformCampaignId: '1203...', platform: 'facebook' },
    ],
  },
});
python
client.ad_campaigns.bulk_update_ad_campaign_status(
    status="paused",
    campaigns=[
        {"platformCampaignId": "1202...", "platform": "facebook"},
        {"platformCampaignId": "1203...", "platform": "facebook"},
    ],
)
bash
curl -X POST "https://zernio.com/api/v1/ads/campaigns/bulk-status" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "paused",
    "campaigns": [
      { "platformCampaignId": "1202...", "platform": "facebook" },
      { "platformCampaignId": "1203...", "platform": "facebook" }
    ]
  }'

Duplicate a campaign

POST /v1/ads/campaigns/{campaignId}/duplicate wraps Meta's native POST /{campaign-id}/copies endpoint. Defaults to deepCopy: true + statusOption: "PAUSED" so the copy is created with the full hierarchy but doesn't start delivering until you activate it. Zernio triggers a sync discovery after the copy so the new hierarchy materializes in /ads/tree within seconds (set syncAfter: false to skip).

typescript
const result = await zernio.adcampaigns.duplicateAdCampaign({
  path: { campaignId: 'CAMPAIGN_ID' },
  body: {
    platform: 'facebook',
    deepCopy: true,
    statusOption: 'PAUSED',
    renameStrategy: 'DEEP_RENAME',
    renameSuffix: ' (copy)',
  },
});
python
result = client.ad_campaigns.duplicate_ad_campaign(
    campaign_id="CAMPAIGN_ID",
    platform="facebook",
    deep_copy=True,
    status_option="PAUSED",
    rename_strategy="DEEP_RENAME",
    rename_suffix=" (copy)",
)
bash
curl -X POST "https://zernio.com/api/v1/ads/campaigns/CAMPAIGN_ID/duplicate" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "facebook",
    "deepCopy": true,
    "statusOption": "PAUSED",
    "renameStrategy": "DEEP_RENAME",
    "renameSuffix": " (copy)"
  }'

Returns { copiedCampaignId, discovery, raw }. For hierarchies with > ~2 objects Meta requires async-batch mode; the synchronous path will return the exact Meta error message so you can fall back to async in your client.

Delete a campaign

DELETE /v1/ads/campaigns/{campaignId} cascades on Meta's side (removes all ad sets + ads) and marks local Ad docs as cancelled in the same pass. The route requires platform in the body so we know which platform's API to call.

typescript
await zernio.adcampaigns.deleteAdCampaign({
  path: { campaignId: 'CAMPAIGN_ID' },
  body: { platform: 'facebook' },
});
python
client.ad_campaigns.delete_ad_campaign(
    campaign_id="CAMPAIGN_ID",
    platform="facebook",
)
bash
curl -X DELETE "https://zernio.com/api/v1/ads/campaigns/CAMPAIGN_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "platform": "facebook" }'

Multi-creative campaigns (creative testing)

For creative A/B testing, don't spin up one campaign per creative, that fragments the budget across parallel ad sets and none of them exit Meta's learning phase (~50 conversions/week required). Instead, pass creatives[] on POST /v1/ads/create to get 1 campaign → 1 ad set → N ads sharing the same budget, targeting, and schedule. Meta's delivery algorithm allocates budget across the creatives inside the single ad set.

typescript
const result = await zernio.ads.createStandaloneAd({
  body: {
    accountId: 'acc_metaads_123',
    adAccountId: 'act_1234567890',
    name: 'Spring Launch',
    goal: 'traffic',
    budgetAmount: 100,
    budgetType: 'daily',
    countries: ['AR'],
    ageMin: 25,
    ageMax: 45,
    creatives: [
      { headline: 'Spring - 30% off', body: 'Limited time.', imageUrl: 'https://cdn.example.com/a.jpg', linkUrl: 'https://example.com/a', callToAction: 'SHOP_NOW' },
      { headline: 'Just for you', body: 'Curated picks.', imageUrl: 'https://cdn.example.com/b.jpg', linkUrl: 'https://example.com/b', callToAction: 'SHOP_NOW' },
      { headline: 'Free shipping', body: 'This week only.', imageUrl: 'https://cdn.example.com/c.jpg', linkUrl: 'https://example.com/c', callToAction: 'SHOP_NOW' },
    ],
  },
});
python
result = client.ads.create_standalone_ad(
    account_id="acc_metaads_123",
    ad_account_id="act_1234567890",
    name="Spring Launch",
    goal="traffic",
    budget_amount=100,
    budget_type="daily",
    countries=["AR"],
    age_min=25,
    age_max=45,
    creatives=[
        {"headline": "Spring - 30% off", "body": "Limited time.", "imageUrl": "https://cdn.example.com/a.jpg", "linkUrl": "https://example.com/a", "callToAction": "SHOP_NOW"},
        {"headline": "Just for you", "body": "Curated picks.", "imageUrl": "https://cdn.example.com/b.jpg", "linkUrl": "https://example.com/b", "callToAction": "SHOP_NOW"},
        {"headline": "Free shipping", "body": "This week only.", "imageUrl": "https://cdn.example.com/c.jpg", "linkUrl": "https://example.com/c", "callToAction": "SHOP_NOW"},
    ],
)
bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acc_metaads_123",
    "adAccountId": "act_1234567890",
    "name": "Spring Launch",
    "goal": "traffic",
    "budgetAmount": 100,
    "budgetType": "daily",
    "countries": ["AR"],
    "ageMin": 25,
    "ageMax": 45,
    "creatives": [
      { "headline": "Spring - 30% off", "body": "Limited time.", "imageUrl": "https://cdn.example.com/a.jpg", "linkUrl": "https://example.com/a", "callToAction": "SHOP_NOW" },
      { "headline": "Just for you", "body": "Curated picks.", "imageUrl": "https://cdn.example.com/b.jpg", "linkUrl": "https://example.com/b", "callToAction": "SHOP_NOW" },
      { "headline": "Free shipping", "body": "This week only.", "imageUrl": "https://cdn.example.com/c.jpg", "linkUrl": "https://example.com/c", "callToAction": "SHOP_NOW" }
    ]
  }'

Returns { ads: [...], platformCampaignId, platformAdSetId, message }, N Ad documents sharing the same platformCampaignId + platformAdSetId. Each ad's name is "<name> #N" (e.g. "Spring Launch #1", "Spring Launch #2"). To add more creatives later without spinning up a new campaign, use the attach shape below.

Meta-only today. Non-Meta platforms return 400.

Conversion campaigns and promoted objects

Optimization goals that point at a specific event/page/app need a promotedObject on the ad set. Without it Meta rejects the ad-set create with error_subcode: 1815430 ("Please select a promoted object for your ad set."). Zernio enforces this upfront so you get a clean 400 invalid_request_error with param: "promotedObject" instead of a passthrough Meta error.

goalMeta optimizationRequired promotedObject fields
conversionsOFFSITE_CONVERSIONSpixelId + customEventType
app_promotionAPP_INSTALLSapplicationId + objectStoreUrl
lead_generationLEAD_GENERATIONpageId (auto-filled from the connected Page when omitted)

customEventType accepts Meta's standard event names: PURCHASE, LEAD, COMPLETE_REGISTRATION, ADD_TO_CART, INITIATE_CHECKOUT, ADD_PAYMENT_INFO, SUBSCRIBE, START_TRIAL, VIEW_CONTENT, SEARCH, CONTACT, SUBMIT_APPLICATION, SCHEDULE. Custom-conversion-driven optimization is supported via customConversionId instead of (or alongside) customEventType.

typescript
const result = await zernio.ads.createStandaloneAd({
  body: {
    accountId: 'acc_metaads_123',
    adAccountId: 'act_1234567890',
    name: 'Spring sale - Purchase optimization',
    goal: 'conversions',
    budgetAmount: 75,
    budgetType: 'daily',
    headline: 'Spring Sale - 30% off',
    body: 'Limited time. Upgrade today.',
    imageUrl: 'https://cdn.example.com/spring.jpg',
    callToAction: 'SHOP_NOW',
    linkUrl: 'https://example.com/spring',
    countries: ['US'],
    promotedObject: {
      pixelId: '1729525464415281',
      customEventType: 'PURCHASE',
    },
  },
});
python
result = client.ads.create_standalone_ad(
    account_id="acc_metaads_123",
    ad_account_id="act_1234567890",
    name="Spring sale - Purchase optimization",
    goal="conversions",
    budget_amount=75,
    budget_type="daily",
    headline="Spring Sale - 30% off",
    body="Limited time. Upgrade today.",
    image_url="https://cdn.example.com/spring.jpg",
    call_to_action="SHOP_NOW",
    link_url="https://example.com/spring",
    countries=["US"],
    promoted_object={
        "pixelId": "1729525464415281",
        "customEventType": "PURCHASE",
    },
)
bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acc_metaads_123",
    "adAccountId": "act_1234567890",
    "name": "Spring sale - Purchase optimization",
    "goal": "conversions",
    "budgetAmount": 75,
    "budgetType": "daily",
    "headline": "Spring Sale - 30% off",
    "body": "Limited time. Upgrade today.",
    "imageUrl": "https://cdn.example.com/spring.jpg",
    "callToAction": "SHOP_NOW",
    "linkUrl": "https://example.com/spring",
    "countries": ["US"],
    "promotedObject": {
      "pixelId": "1729525464415281",
      "customEventType": "PURCHASE"
    }
  }'

Need a pixel ID? Use zernio.trackingtags.listTrackingTags() to list the pixels a connected ad account can see (or zernio.trackingtags.createTrackingTag() to make one) — see Meta Pixels below. zernio.ads.listConversionDestinations() returns the same set, in case you're already wiring that.

Other Meta-specific knobs on promotedObject:

FieldWhen you'd use it
customConversionIdOptimising against a Custom Conversion (instead of a standard event). Pair with pixelId.
productCatalogId + productSetIdCatalog Ads / Advantage+ Shopping campaigns.
applicationId + objectStoreUrlApp-install / app-engagement campaigns (goal: "app_promotion").

Attach a creative to an existing ad set

To iterate on creatives after a campaign is already running, pass adSetId on POST /v1/ads/create with a single creative. One new ad gets attached to the existing ad set, budget, targeting, schedule, and goal are inherited from the ad set on Meta's side. No new campaign is created.

typescript
const result = await zernio.ads.createStandaloneAd({
  body: {
    accountId: 'acc_metaads_123',
    adAccountId: 'act_1234567890',
    adSetId: '120250000000000000',
    name: 'Spring Launch #4',
    headline: 'One more angle',
    body: 'Social proof variant.',
    imageUrl: 'https://cdn.example.com/d.jpg',
    linkUrl: 'https://example.com/d',
    callToAction: 'SHOP_NOW',
  },
});
python
result = client.ads.create_standalone_ad(
    account_id="acc_metaads_123",
    ad_account_id="act_1234567890",
    ad_set_id="120250000000000000",
    name="Spring Launch #4",
    headline="One more angle",
    body="Social proof variant.",
    image_url="https://cdn.example.com/d.jpg",
    link_url="https://example.com/d",
    call_to_action="SHOP_NOW",
)
bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acc_metaads_123",
    "adAccountId": "act_1234567890",
    "adSetId": "120250000000000000",
    "name": "Spring Launch #4",
    "headline": "One more angle",
    "body": "Social proof variant.",
    "imageUrl": "https://cdn.example.com/d.jpg",
    "linkUrl": "https://example.com/d",
    "callToAction": "SHOP_NOW"
  }'

Returns { ad, message }. Useful for weekly creative drops onto a stable ad set that Meta has already optimised on.

Video creatives

All three shapes (standalone, multi-creative, attach) accept video. Replace imageUrl with a video object carrying the video URL and a still-image thumbnail:

typescript
const result = await zernio.ads.createStandaloneAd({
  body: {
    accountId: 'acc_metaads_123',
    adAccountId: 'act_1234567890',
    name: 'Spring launch video',
    goal: 'video_views',
    budgetAmount: 75,
    budgetType: 'daily',
    headline: 'Spring drop is live',
    body: 'See it in action.',
    video: {
      url: 'https://cdn.example.com/spring.mp4',
      thumbnailUrl: 'https://cdn.example.com/spring-poster.jpg',
    },
    callToAction: 'SHOP_NOW',
    linkUrl: 'https://example.com/spring',
  },
});
python
result = client.ads.create_standalone_ad(
    account_id="acc_metaads_123",
    ad_account_id="act_1234567890",
    name="Spring launch video",
    goal="video_views",
    budget_amount=75,
    budget_type="daily",
    headline="Spring drop is live",
    body="See it in action.",
    video={
        "url": "https://cdn.example.com/spring.mp4",
        "thumbnailUrl": "https://cdn.example.com/spring-poster.jpg",
    },
    call_to_action="SHOP_NOW",
    link_url="https://example.com/spring",
)
bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acc_metaads_123",
    "adAccountId": "act_1234567890",
    "name": "Spring launch video",
    "goal": "video_views",
    "budgetAmount": 75,
    "budgetType": "daily",
    "headline": "Spring drop is live",
    "body": "See it in action.",
    "video": {
      "url": "https://cdn.example.com/spring.mp4",
      "thumbnailUrl": "https://cdn.example.com/spring-poster.jpg"
    },
    "callToAction": "SHOP_NOW",
    "linkUrl": "https://example.com/spring"
  }'

For multi-creative, put video: { url, thumbnailUrl } on each entry of creatives[] instead of imageUrl. video and imageUrl are mutually exclusive per creative, supply exactly one.

Thumbnail is required. Meta rejects video ads without a still-image thumbnail. Pass a public image URL in video.thumbnailUrl (1200x628 or 1080x1080 recommended).

Sync upload, long-running. Zernio uploads the video to Meta via chunked transfer and blocks until Meta finishes transcoding (status.video_status === "ready"). Longer videos can take several minutes. The endpoint is configured with maxDuration = 800s on our side; set your HTTP client timeout to at least 15 minutes so it doesn't bail before Meta finishes. If transcoding hasn't completed within 10 minutes, the request fails with a platform_error.

Custom Audiences

Create a Lookalike Audience:

typescript
const audience = await zernio.adaudiences.createAdAudience({
  body: {
    accountId: 'acc_metaads_123',
    adAccountId: 'act_1234567890',
    type: 'lookalike',
    name: 'LAL 1% of US customers',
    sourceAudienceId: '6123456789',
    country: 'US',
    ratio: 0.01,
  },
});
python
audience = client.ad_audiences.create_ad_audience(
    account_id="acc_metaads_123",
    ad_account_id="act_1234567890",
    type="lookalike",
    name="LAL 1% of US customers",
    source_audience_id="6123456789",
    country="US",
    ratio=0.01,
)
bash
curl -X POST "https://zernio.com/api/v1/ads/audiences" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acc_metaads_123",
    "adAccountId": "act_1234567890",
    "type": "lookalike",
    "name": "LAL 1% of US customers",
    "sourceAudienceId": "6123456789",
    "country": "US",
    "ratio": 0.01
  }'

Add users to a customer list (SHA-256 hashed automatically):

typescript
await zernio.adaudiences.addUsersToAdAudience({
  path: { audienceId: 'AUDIENCE_ID' },
  body: {
    users: [
      { email: 'user@example.com' },
      { phone: '+14155551234' },
    ],
  },
});
python
client.ad_audiences.add_users_to_ad_audience(
    audience_id="AUDIENCE_ID",
    users=[
        {"email": "user@example.com"},
        {"phone": "+14155551234"},
    ],
)
bash
curl -X POST "https://zernio.com/api/v1/ads/audiences/AUDIENCE_ID/users" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "users": [
      { "email": "user@example.com" },
      { "phone": "+14155551234" }
    ]
  }'

Targeting

Use /v1/ads/interests?platform=metaads&q=devops to search interest IDs, and /v1/ads/targeting/search?type=city&q=Amsterdam&countryCode=NL to look up city / region keys.

FieldTypeDescription
ageMinnumberMinimum age (13-65)
ageMaxnumberMaximum age (13-65)
countriesstring[]ISO 3166-1 alpha-2 country codes. Defaults to ["US"] when no cities or regions are provided.
citiesobject[]City-level targeting. Each entry: { key, radius?, distance_unit? } where key is from /v1/ads/targeting/search, and radius + distance_unit ("kilometer" or "mile") must be set together.
regionsobject[]Region/state-level targeting. Each entry: { key } from /v1/ads/targeting/search?type=region.
interestsobject[]{ id, name } from /v1/ads/interests
genderstring"all", "male", or "female" (default "all")

On /v1/ads/create, targeting fields are flat at the top level (ageMin, ageMax, countries, cities, regions, interests). On /v1/ads/boost, they live inside a targeting: { ... } object that's passed through to Meta as-is.

Looking up city / region keys

Meta's cities and regions use opaque IDs that aren't derivable from the name. Use the targeting search helper to resolve them:

typescript
// Find Meta's key for Amsterdam in NL
const { results } = await zernio.ads.searchAdTargetingLocations({
  accountId: 'FB_ACCOUNT_ID',
  q: 'Amsterdam',
  type: 'city',
  countryCode: 'NL',
});
// results[0].key  → "2759794"

// Use the key on /v1/ads/create
await zernio.ads.createStandaloneAd({ body: {
  accountId: 'FB_ACCOUNT_ID',
  adAccountId: 'act_1234567890',
  name: 'Amsterdam launch',
  goal: 'traffic',
  budgetAmount: 20,
  budgetType: 'lifetime',
  endDate: '2026-05-15T00:00:00Z',
  headline: 'Hello Amsterdam',
  body: 'Try us this week.',
  linkUrl: 'https://example.com',
  imageUrl: 'https://cdn.example.com/promo.jpg',
  callToAction: 'LEARN_MORE',
  cities: [{ key: '2759794', radius: 25, distance_unit: 'kilometer' }],
  dsaBeneficiary: 'Acme BV',
  dsaPayor: 'Acme BV',
}});
python
results = client.ads.search_ad_targeting_locations(
    account_id="FB_ACCOUNT_ID",
    q="Amsterdam",
    type="city",
    country_code="NL",
)["results"]
# results[0]["key"]  → "2759794"

client.ads.create_standalone_ad(
    account_id="FB_ACCOUNT_ID",
    ad_account_id="act_1234567890",
    name="Amsterdam launch",
    goal="traffic",
    budget_amount=20,
    budget_type="lifetime",
    end_date="2026-05-15T00:00:00Z",
    headline="Hello Amsterdam",
    body="Try us this week.",
    link_url="https://example.com",
    image_url="https://cdn.example.com/promo.jpg",
    call_to_action="LEARN_MORE",
    cities=[{"key": "2759794", "radius": 25, "distance_unit": "kilometer"}],
    dsa_beneficiary="Acme BV",
    dsa_payor="Acme BV",
)
bash
# 1. Look up the city key
curl "https://zernio.com/api/v1/ads/targeting/search?accountId=FB_ACCOUNT_ID&q=Amsterdam&type=city&countryCode=NL" \
  -H "Authorization: Bearer YOUR_API_KEY"

# 2. Use the key on /v1/ads/create
curl -X POST "https://zernio.com/api/v1/ads/create" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "FB_ACCOUNT_ID",
    "adAccountId": "act_1234567890",
    "name": "Amsterdam launch",
    "goal": "traffic",
    "budgetAmount": 20,
    "budgetType": "lifetime",
    "endDate": "2026-05-15T00:00:00Z",
    "headline": "Hello Amsterdam",
    "body": "Try us this week.",
    "linkUrl": "https://example.com",
    "imageUrl": "https://cdn.example.com/promo.jpg",
    "callToAction": "LEARN_MORE",
    "cities": [{ "key": "2759794", "radius": 25, "distance_unit": "kilometer" }],
    "dsaBeneficiary": "Acme BV",
    "dsaPayor": "Acme BV"
  }'

type accepts city, region, country, subcity, neighborhood, zip, metro_area, geo_market. Pass countryCode to disambiguate when the same name exists in multiple countries (e.g. there are several "Eindhoven"s globally).

Don't combine cities with the same countries, Meta returns a "locations overlap" error because the city is already inside the country boundary. Either drop the country, or scope countries to a different country than the cities you're targeting.

Click-to-WhatsApp ads

Create ads that, when tapped, open a WhatsApp conversation with your business instead of sending the user to a website. Zernio drives the full hierarchy (campaign → ad set → creative → ad) in a single call. The CTA is locked to WHATSAPP_MESSAGE and the destination is hard-coded to https://api.whatsapp.com/send, Meta resolves the actual WhatsApp number from the Page-to-WhatsApp pairing configured in your Page settings.

Before calling, the Facebook Page must already be paired with a verified WhatsApp Business number (Meta Business Manager → WhatsApp Accounts → connect a Page). When the Page isn't paired, Meta rejects the ad set with subcode 2446886 and Zernio surfaces a clean platform_error envelope.

typescript
const result = await zernio.ads.createCtwaAd({ body: {
  accountId: 'FB_OR_IG_ACCOUNT_ID',
  adAccountId: 'act_123456789',
  name: 'Summer promo, WhatsApp',
  headline: 'Chat with us on WhatsApp',
  body: 'Get a personal quote in 2 minutes.',
  imageUrl: 'https://cdn.example.com/promo.jpg',
  budgetAmount: 20,
  budgetType: 'daily',
  currency: 'USD',
  countries: ['US', 'ES'],
}});
python
result = client.ads.create_ctwa_ad(
    account_id="FB_OR_IG_ACCOUNT_ID",
    ad_account_id="act_123456789",
    name="Summer promo, WhatsApp",
    headline="Chat with us on WhatsApp",
    body="Get a personal quote in 2 minutes.",
    image_url="https://cdn.example.com/promo.jpg",
    budget_amount=20,
    budget_type="daily",
    currency="USD",
    countries=["US", "ES"],
)
bash
curl -X POST "https://zernio.com/api/v1/ads/ctwa" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "FB_OR_IG_ACCOUNT_ID",
    "adAccountId": "act_123456789",
    "name": "Summer promo, WhatsApp",
    "headline": "Chat with us on WhatsApp",
    "body": "Get a personal quote in 2 minutes.",
    "imageUrl": "https://cdn.example.com/promo.jpg",
    "budgetAmount": 20,
    "budgetType": "daily",
    "currency": "USD",
    "countries": ["US", "ES"]
  }'

To close the attribution loop on conversions that happen inside the WhatsApp thread, see Conversions API for Business Messaging on the WhatsApp page.

Meta Pixels

A Meta Pixel is the measurement primitive you install on a website, send events to, and target ads against. Zernio exposes it under the platform-neutral tracking tags API — on Meta, kind is pixel. Uses the Meta Ads account you already connected; no extra OAuth, no business_management permission. The accountId is the Meta ads SocialAccount (the one the Ads add-on connect flow creates), not a Facebook/Instagram posting account; get your act_... ad account IDs from zernio.ads.listAdAccounts().

Create a pixel

typescript
const { data } = await zernio.trackingtags.createTrackingTag({
  path: { accountId: 'ACCOUNT_ID' },
  body: { adAccountId: 'act_1234567890', name: 'My Website Pixel' },
});

console.log(data.tag.id);    // new pixel ID — use it in promotedObject.pixelId, audiences, CAPI
console.log(data.tag.code);  // the base-code snippet to install on the site
python
result = client.tracking_tags.create_tracking_tag(
    account_id="ACCOUNT_ID",
    ad_account_id="act_1234567890",
    name="My Website Pixel",
)
print(result.tag.id)    # new pixel ID
print(result.tag.code)  # the install snippet
bash
curl -X POST "https://zernio.com/api/v1/accounts/ACCOUNT_ID/tracking-tags" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"adAccountId":"act_1234567890","name":"My Website Pixel"}'

The response is the new tag, including code — the base-code snippet to drop on the site. Creating a pixel doesn't install it: install code, or skip the snippet entirely and send events server-side via the Conversions API (a pixel works as a CAPI destination immediately). installed is derived from lastFiredTime (null = it has never fired).

List, fetch, and rename pixels

typescript
// Every pixel the connected ad account can see (pass query.adAccountId to scope to one)
const { data: list } = await zernio.trackingtags.listTrackingTags({
  path: { accountId: 'ACCOUNT_ID' },
});

// One pixel — includes its install code, lastFiredTime, ownerBusinessId
const { data: one } = await zernio.trackingtags.getTrackingTag({
  path: { accountId: 'ACCOUNT_ID', tagId: '1729525464415281' },
});

// Rename, or toggle Advanced Matching / first-party cookies / data-use
await zernio.trackingtags.updateTrackingTag({
  path: { accountId: 'ACCOUNT_ID', tagId: '1729525464415281' },
  body: { name: 'Website Pixel (renamed)', enableAutomaticMatching: true },
});
python
# Every pixel the connected ad account can see (pass ad_account_id to scope to one)
listing = client.tracking_tags.list_tracking_tags(account_id="ACCOUNT_ID")

# One pixel — includes its install code, last_fired_time, owner_business_id
one = client.tracking_tags.get_tracking_tag(
    account_id="ACCOUNT_ID", tag_id="1729525464415281"
)

# Rename, or toggle Advanced Matching / first-party cookies / data-use
client.tracking_tags.update_tracking_tag(
    account_id="ACCOUNT_ID",
    tag_id="1729525464415281",
    name="Website Pixel (renamed)",
    enable_automatic_matching=True,
)
bash
curl "https://zernio.com/api/v1/accounts/ACCOUNT_ID/tracking-tags" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://zernio.com/api/v1/accounts/ACCOUNT_ID/tracking-tags/1729525464415281" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl -X PATCH "https://zernio.com/api/v1/accounts/ACCOUNT_ID/tracking-tags/1729525464415281" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Website Pixel (renamed)","enableAutomaticMatching":true}'

Share a pixel with another ad account

By default a pixel only works in the ad account it was created on. Share it with others so their campaigns and audiences can use it (listTrackingTagSharedAccounts and removeTrackingTagSharedAccount round out the set):

typescript
await zernio.trackingtags.addTrackingTagSharedAccount({
  path: { accountId: 'ACCOUNT_ID', tagId: '1729525464415281' },
  body: { adAccountId: 'act_9876543210' },
});
python
client.tracking_tags.add_tracking_tag_shared_account(
    account_id="ACCOUNT_ID",
    tag_id="1729525464415281",
    ad_account_id="act_9876543210",
)
bash
curl -X POST "https://zernio.com/api/v1/accounts/ACCOUNT_ID/tracking-tags/1729525464415281/shared-accounts" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"adAccountId":"act_9876543210"}'

For aggregated event counts, call zernio.trackingtags.getTrackingTagStats(...)aggregation is event by default; also host, url, device_type, and more.

Things to know about Meta Pixels:

  • Not idempotent. Each create call makes a new pixel — don't retry blindly on timeout.
  • One pixel per ad account on create. POST .../tracking-tags returns a 400 (A pixel already exists for this account) if that ad account already has one. A Business Manager can own many pixels, just not two on the same ad account via this call.
  • No delete. Meta has no API to delete a pixel. To stop using one, unshare it (DELETE .../shared-accounts) or disable it in Events Manager.
  • Personal ad accounts can't share. A pixel created on an ad account that isn't owned by a Business Manager comes back with ownerBusinessId: null and can't be shared with other ad accounts (Meta rejects the share). Claim the ad account into a Business Manager first.
  • Not exposed (needs business_management): sharing a pixel with a partner/agency business, and assigning system users to a pixel.

Conversions API

Send offline conversion events (deal closed, lead qualified, trial converted) back to Meta via the Graph API events endpoint. Zernio uses the Meta Ads account you already connected, no additional OAuth, no pixel-scoped CAPI token to paste. PII is SHA-256 hashed server-side per Meta's spec before anything leaves your server.

Discover available pixels

typescript
const { data } = await zernio.ads.listConversionDestinations({
  path: { accountId: 'ACCOUNT_ID' },
});
python
data = client.ads.list_conversion_destinations(account_id="ACCOUNT_ID")
bash
curl "https://zernio.com/api/v1/accounts/ACCOUNT_ID/conversion-destinations" \
  -H "Authorization: Bearer YOUR_API_KEY"

Returns every pixel accessible to the connected ad accounts. Use the returned id as destinationId on the send call.

Send a conversion event

typescript
const result = await zernio.ads.sendConversions({ body: {
  accountId: 'ACCOUNT_ID',
  destinationId: '1729525464415281', // pixel ID
  events: [{
    eventName: 'Lead',
    eventTime: Math.floor(Date.now() / 1000),
    eventId: 'order_abc_123', // dedup key, must match pixel event if dual-tracking
    value: 42.50,
    currency: 'USD',
    actionSource: 'crm',
    user: {
      email: 'customer@example.com',
      phone: '+14155551234',
      firstName: 'Jane',
      lastName: 'Doe',
      country: 'US',
    },
  }],
}});
python
result = client.ads.send_conversions(
    account_id="ACCOUNT_ID",
    destination_id="1729525464415281",
    events=[{
        "eventName": "Lead",
        "eventTime": int(time.time()),
        "eventId": "order_abc_123",
        "value": 42.50,
        "currency": "USD",
        "actionSource": "crm",
        "user": {
            "email": "customer@example.com",
            "phone": "+14155551234",
            "firstName": "Jane",
            "lastName": "Doe",
            "country": "US",
        },
    }],
)
bash
curl -X POST "https://zernio.com/api/v1/ads/conversions" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "ACCOUNT_ID",
    "destinationId": "1729525464415281",
    "events": [{
      "eventName": "Lead",
      "eventTime": 1744732800,
      "eventId": "order_abc_123",
      "value": 42.50,
      "currency": "USD",
      "actionSource": "crm",
      "user": {
        "email": "customer@example.com",
        "phone": "+14155551234",
        "firstName": "Jane",
        "lastName": "Doe",
        "country": "US"
      }
    }]
  }'

Standard event names

Purchase, Lead, CompleteRegistration, AddToCart, InitiateCheckout, AddPaymentInfo, Subscribe, StartTrial, ViewContent, Search, Contact, SubmitApplication, Schedule. Custom event names are accepted too.

Test mode

Pass testCode: "TEST12345" at the request root to route events to the Test Events tab in Meta Events Manager without affecting production pixel data.

Deduplication

Pass a stable eventId on every event. Meta dedupes against your pixel within a 48-hour window when eventId matches. Missing or inconsistent eventId between pixel and CAPI double-counts conversions, the #1 cause of inflated reports.

Batching

Up to 1,000 events per request (Zernio chunks larger batches automatically). Meta rejects the entire batch if any event is malformed, the response includes failures[] with per-event error detail and a traceId you can look up in Meta support.

Media Requirements

TypeFormatMax SizeNotes
Feed ImageJPEG, PNG30 MB1080x1080 or 1200x628 recommended
Reels VideoMP4, MOV4 GB9:16 vertical, max 90 sec
StoryJPEG, PNG, MP430 MB / 4 GB9:16 vertical
CarouselJPEG, PNG, MP430 MB/card2-10 cards