Zernio
APIFor you
Zernio
APIFor you
Platforms

Meta Ads

Overview

Set up

Connect

Build

CampaignsAd setsCreativesCreative libraryPreviewsBoost a postLead Gen Forms

Target

TargetingCustom Audiences

Ad types

Creative testingConversion campaignsCatalog adsMessaging & call adsClick-to-WhatsApp adsReach & frequency

Measure

InsightsMeta PixelsConversions APIAd URL tracking tags

Operate

Duplicate & lifecycleAccount & ops readsAd LibraryAd comments

Reference

Enums, limits & error codes
Dashboard
OverviewPlatformsAPI ReferenceResources
Meta Ads

Duplicate & lifecycle

Validate a Meta create before it spends, pause and resume delivery, duplicate a campaign, ad set or ad, and delete a campaign.


Change the state of a hierarchy that already exists: start and stop delivery, copy a level, delete, and validate a create before it costs anything.

Before you start

Every call here works on a campaign, ad set or ad you already have. You need its platform id from an earlier create or from GET /v1/ads/tree.

Pause or resume

PUT /v1/ads/campaigns/{campaignId}/status switches one campaign's own on/off switch and nothing else. Its ad sets and ads keep their own switches, exactly as in Ads Manager.

const { data: paused } = await zernio.adcampaigns.updateAdCampaignStatus({
  path: { campaignId: '120250000000000000' },
  body: { status: 'paused', platform: 'facebook' }
});

Response (200):

{ "status": "paused", "platformCampaignStatus": "PAUSED", "statusReadAt": "2026-09-29T08:55:34.565Z", "updated": 1, "skipped": 0 }

Pausing a campaign stops delivery for everything under it. Meta reports the children as CAMPAIGN_PAUSED.

Resuming it lets each ad set and ad deliver again only if its own switch is on. An ad set or ad you paused in its own right stays paused.

Resume it with PUT /v1/ads/ad-sets/{adSetId}/status or PUT /v1/ads/{adId}/status, the narrower scopes with the same body. The same rule holds on every ad platform.

Zernio reads the campaign's switch from Meta first:

  • If it is already in the requested state, nothing is written and the response says updated: 0, skipped: 1 with a reason.
  • Otherwise the response says updated: 1.

platformCampaignStatus is the switch read back after the write. status is the delivery status that follows from it.

POST /v1/ads/campaigns/bulk-status takes up to 50 campaigns and reports each row on its own, so one bad row does not fail the batch.

const { data: bulk } = await zernio.adcampaigns.bulkUpdateAdCampaignStatus({
  body: {
    status: 'paused',
    campaigns: [
      { platformCampaignId: '120250000000000000', platform: 'facebook' },
      { platformCampaignId: '120250000000000002', platform: 'facebook' }
    ]
  }
});

Response (200):

{
  "status": "paused",
  "totals": { "updated": 1, "skipped": 0, "failed": 1 },
  "results": [
    { "platformCampaignId": "120250000000000000", "platform": "facebook", "updated": 1, "skipped": 0, "platformCampaignStatus": "PAUSED" },
    { "platformCampaignId": "120250000000000002", "platform": "facebook", "error": "Campaign not found" }
  ]
}

Duplicate a campaign

POST /v1/ads/campaigns/{campaignId}/duplicate wraps Meta's native /copies.

It defaults to deepCopy: true with statusOption: "PAUSED". The copy arrives with the full hierarchy and delivers nothing until you activate it.

Zernio triggers an ads discovery sync afterwards, so the new hierarchy shows up in the tree within seconds. Pass syncAfter: false to skip that.

const { data: copy } = await zernio.adcampaigns.duplicateAdCampaign({
  path: { campaignId: '120250000000000000' },
  body: {
    platform: 'facebook',
    deepCopy: true,
    statusOption: 'PAUSED',
    renameStrategy: 'DEEP_RENAME',
    renameSuffix: ' (copy)'
  }
});

Response (200):

{ "copiedCampaignId": "120250000000000013", "discovery": "triggered", "raw": {} }

statusOption also takes:

  • ACTIVE, which launches the copy at once
  • INHERITED_FROM_SOURCE, which mirrors each entity's source status

renameStrategy is DEEP_RENAME, ONLY_TOP_LEVEL_RENAME or NO_RENAME, with renamePrefix and renameSuffix.

startTime and endTime reschedule the copy.

Duplicate an ad set or an ad

Same shape, one and two levels down. Both use Meta's native /copies, create the copy paused by default, and accept syncAfter: false.

POST /v1/ads/ad-sets/{adSetId}/duplicate takes:

  • an optional campaignId to retarget the copy into another campaign
  • deepCopy (default true, so ads and creatives come along)
  • statusOption, startTime, endTime and the rename options

POST /v1/ads/{adId}/duplicate takes an optional adSetId to retarget the copy into another ad set. It accepts the Zernio ad id or the Meta ad id.

The ad duplicate takes no platform, because the ad resolves it.

const { data: adSetCopy } = await zernio.adcampaigns.duplicateAdSet({
  path: { adSetId: '120250000000000001' },
  body: { platform: 'facebook', campaignId: '120250000000000013', statusOption: 'PAUSED' }
});

Response (200):

{ "copiedAdSetId": "120250000000000014", "discovery": "triggered", "raw": {} }

Re-running one creative under a different ad set is the ad-level copy:

curl -X POST "https://zernio.com/api/v1/ads/66d4a1b2c3e4f5a6b7c8d9e2/duplicate" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "adSetId": "120250000000000014" }'

Response (200):

{ "copiedAdId": "120260000000000003", "discovery": "triggered", "raw": {} }

When the native copy requires a creative rebuild, the creative settings Meta returns are preserved. That includes explicit promotion metadata and creativeFeatures.

Metadata Meta does not return on the copy cannot be recovered.

Meta caps a synchronous copy at fewer than 3 total objects, so even a one-ad ad set can trip it. The count appears to include children deleted by earlier copies.

Duplicate the campaign for anything bigger; Meta's own 400 surfaces verbatim.

Deprecated enhancements. A creative still carrying Meta's deprecated standard_enhancements field blocks the campaign and ad set copies. Rebuild it with creativeFeatures first.

The ad copy drops the field and retries on its own.

Delete a campaign

DELETE /v1/ads/campaigns/{campaignId} cascades on Meta's side, removing the ad sets and ads. It marks the local ad documents cancelled in the same pass.

platform is required in the body.

const { data: deleted } = await zernio.adcampaigns.deleteAdCampaign({
  path: { campaignId: '120250000000000013' },
  body: { platform: 'facebook' }
});

Response (200):

{ "deleted": true, "adCount": 3 }

Empty campaigns. A campaign with zero ads has no local ad documents to resolve, so this route would 404 on it.

Send accountId in the body to delete such a shell. Zernio then skips the local lookup and forwards the delete to Meta. It is ignored when the campaign does have ads.

DELETE /v1/ads/{adId} removes one ad and leaves its ad set and campaign in place.

To retire an ad without losing its history, pause it instead. Deleted objects stop reporting.

Dry-run a create

validateOnly: true on POST /v1/ads/create runs every node through Meta's own execution_options: validate_only.

Nothing is created on Meta and nothing is persisted in Zernio. You get 200 with per-node results instead of an ad.

import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: check } = await zernio.adcampaigns.createStandaloneAd({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    adAccountId: 'act_1234567890',
    name: 'Spring sale (dry run)',
    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',
    promotedObject: { pixelId: '1729525464415281', customEventType: 'PURCHASE' },
    validateOnly: true
  }
});

for (const result of check.results) console.log(result.node, result.status);

Response (200):

{
  "validateOnly": true,
  "results": [
    { "node": "campaign", "status": "validated" },
    { "node": "adSet", "status": "skipped" },
    { "node": "creative", "status": "validated" },
    { "node": "ad", "status": "skipped" }
  ]
}

A Meta validation failure comes back as its own 400, verbatim.

That is the point of the call. Meta's validator catches configuration errors a schema cannot, such as:

  • a budget sharing conflict
  • a CBO campaign whose ad sets share an optimization goal
  • a landing page Meta refuses

Each node reports validated or skipped, and a skipped one carries a reason.

Meta can validate a child only against a real parent. On a fresh tree, the campaign and the creative are validated and the ad set is skipped.

Pass existingCampaignId to validate the ad set against that campaign too. The ad is never validatable before create.

validateOnly works on the single standalone shape only. Combining it with creatives[], an adSetId attach or buyingType: "RESERVED" returns a 400.

A non-Meta platform returns a 501 with code: "feature_not_available".

Media uploads. Media referenced by imageUrl is uploaded to the ad account's image library during the dry run, because Meta needs a real image hash to validate the creative.

Uploads are content-deduped, so repeated dry runs add nothing.

If it fails

A 400 on a dry run is Zernio's own shape check, before the request reaches Meta:

{
  "error": "validateOnly supports the single standalone shape only (not creatives[], adSetId attach, or RESERVED buying).",
  "type": "invalid_request_error",
  "code": "invalid_field_value",
  "param": "validateOnly"
}

Send the single standalone shape for a dry run.

A 404 on the delete means the campaign has no local ads. Send accountId in the body to solve it.

Related

  • Campaigns: the create these calls operate on.
  • Ad sets: status and budget edits that are not lifecycle changes.
  • Reference: the copy limits Meta imposes.
  • Duplicate campaign and Bulk status: every field.
Was this page helpful?

Ad URL tracking tags

Read and set the click-URL parameters on a Meta ad with GET and PATCH /v1/ads/{adId}/tracking-tags, without rebuilding the creative yourself.

Account & ops reads

Read a Meta ad account's change log, A/B studies, finances, labels, budget schedules, Business portfolios, Instagram identities and app-promotion setup.

On this page

Before you startPause or resumeDuplicate a campaignDuplicate an ad set or an adDelete a campaignDry-run a createIf it failsRelated