Campaigns & Ad Sets
Create standalone campaigns and manage campaigns, ad sets, and budgets
Create a Standalone Campaign
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,
}});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,
)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_conversion", "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}).
bidStrategy | Required companion field | Notes |
|---|---|---|
LOWEST_COST_WITHOUT_CAP (default) | — | Auto-bid; Meta optimizes to spend the budget. |
LOWEST_COST_WITH_BID_CAP | bidAmount (whole currency units) | Auto-bid with a hard ceiling. |
COST_CAP | bidAmount (whole currency units) | Target average cost per result. |
LOWEST_COST_WITH_MIN_ROAS | roasAverageFloor (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.0 → 20000).
Create an ad with a $5 cost cap:
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' },
},
});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"},
)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:
await zernio.adcampaigns.updateAdSet({
path: { adSetId: 'AD_SET_ID' },
body: {
platform: 'facebook',
bidStrategy: 'LOWEST_COST_WITH_MIN_ROAS',
roasAverageFloor: 2.5,
},
});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,
)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:
| Field | Type | Notes |
|---|---|---|
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.). |
purchaseValue | number | Convenience sum of purchase-type action values, picked from actionValues via the same priority list as conversions (offsite_conversion.fb_pixel_purchase → omni_purchase → purchase). Same unit as spend. |
roas | number | Derived 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:
"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:
| Field | Type | Notes |
|---|---|---|
status | active | paused | pending_review | … | Delivery status derived from child ads |
reviewStatus | in_review | approved | rejected | with_issues | null | Platform-side review, distinct from status |
platformCampaignStatus | string | Raw Meta Campaign.effective_status |
campaignIssuesInfo | object[] | null | Meta's raw issues_info[] when delivery issues exist |
budgetLevel | campaign | adset | null | Canonical CBO/ABO switch |
campaignBudget | { amount, type } | null | Populated only for CBO |
adSetBudget (on ad-set nodes) | { amount, type } | null | Populated only for ABO |
isBudgetScheduleEnabled | boolean | Mirrors Meta Campaign.is_budget_schedule_enabled |
currency | string | ISO 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.
await zernio.adcampaigns.updateAdCampaign({
path: { campaignId: 'CAMPAIGN_ID' },
body: {
platform: 'facebook',
budget: { amount: 250, type: 'daily' },
},
});client.ad_campaigns.update_ad_campaign(
campaign_id="CAMPAIGN_ID",
platform="facebook",
budget={"amount": 250, "type": "daily"},
)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.
await zernio.adcampaigns.updateAdSet({
path: { adSetId: 'ADSET_ID' },
body: {
platform: 'facebook',
budget: { amount: 75, type: 'daily' },
status: 'active',
},
});client.ad_campaigns.update_ad_set(
ad_set_id="ADSET_ID",
platform="facebook",
budget={"amount": 75, "type": "daily"},
status="active",
)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:
await zernio.adcampaigns.updateAdCampaignStatus({
path: { campaignId: 'CAMPAIGN_ID' },
body: { status: 'paused', platform: 'facebook' },
});client.ad_campaigns.update_ad_campaign_status(
campaign_id="CAMPAIGN_ID",
status="paused",
platform="facebook",
)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:
await zernio.adcampaigns.bulkUpdateAdCampaignStatus({
body: {
status: 'paused',
campaigns: [
{ platformCampaignId: '1202...', platform: 'facebook' },
{ platformCampaignId: '1203...', platform: 'facebook' },
],
},
});client.ad_campaigns.bulk_update_ad_campaign_status(
status="paused",
campaigns=[
{"platformCampaignId": "1202...", "platform": "facebook"},
{"platformCampaignId": "1203...", "platform": "facebook"},
],
)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).
const result = await zernio.adcampaigns.duplicateAdCampaign({
path: { campaignId: 'CAMPAIGN_ID' },
body: {
platform: 'facebook',
deepCopy: true,
statusOption: 'PAUSED',
renameStrategy: 'DEEP_RENAME',
renameSuffix: ' (copy)',
},
});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)",
)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.
await zernio.adcampaigns.deleteAdCampaign({
path: { campaignId: 'CAMPAIGN_ID' },
body: { platform: 'facebook' },
});client.ad_campaigns.delete_ad_campaign(
campaign_id="CAMPAIGN_ID",
platform="facebook",
)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" }'