Creatives
Attach creatives, video, carousel, placement assets, and Advantage+ enhancements
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.
const result = await zernio.adcampaigns.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',
},
});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:
const result = await zernio.adcampaigns.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',
},
});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 optional. When you omit video.thumbnailUrl, the poster is auto-generated from Meta's own preferred video thumbnail (the same candidates Ads Manager shows), so "video": { "url": "..." } is a complete video creative. Supply your own public image URL (1200x628 or 1080x1080 recommended) to control the exact still frame — a supplied thumbnail always wins.
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.
Placement asset customization
This is Ads Manager's "use a different creative per placement" on one ad: a 9:16 asset on Stories and Reels, a 1:1 or 4:5 on Feed, all served by a single ad. Pass a placementAssets object to /v1/ads/create. Each rule pins one asset to one or more placements, and a default asset covers every placement no rule matches.
This is distinct from multi-creative campaigns (N separate ads Meta A/B-tests) and from dynamicCreative (an asset pool Meta auto-optimizes). placementAssets is deterministic: you decide exactly which asset shows where.
Meta only (facebook / instagram). The shared copy (headline, body, linkUrl, callToAction) comes from the top-level fields, only the image or video varies per placement.
Image per placement
const result = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: 'acc_metaads_123',
adAccountId: 'act_1234567890',
name: 'Spring launch (placement-tailored)',
goal: 'traffic',
budgetAmount: 50,
budgetType: 'daily',
headline: 'Spring drop is live',
body: 'See it in action.',
callToAction: 'SHOP_NOW',
linkUrl: 'https://example.com/spring',
placementAssets: {
// Catch-all: shown on any placement no rule below matches.
defaultImageUrl: 'https://cdn.example.com/1x1.jpg',
rules: [
{
imageUrl: 'https://cdn.example.com/9x16.jpg',
placements: {
publisherPlatforms: ['instagram', 'facebook'],
instagramPositions: ['story', 'reels'],
facebookPositions: ['story', 'facebook_reels'],
},
},
{
imageUrl: 'https://cdn.example.com/4x5.jpg',
placements: {
instagramPositions: ['stream'],
facebookPositions: ['feed'],
},
},
],
},
},
});Video per placement
Swap imageUrl for videoUrl (and defaultImageUrl for defaultVideoUrl). Poster thumbnails are optional here. Meta auto-generates one if you omit thumbnailUrl / defaultThumbnailUrl, though supplying your own gives you control over the still frame.
const result = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: 'acc_metaads_123',
adAccountId: 'act_1234567890',
name: 'Spring launch video (placement-tailored)',
goal: 'video_views',
budgetAmount: 75,
budgetType: 'daily',
headline: 'Spring drop is live',
body: 'See it in action.',
callToAction: 'SHOP_NOW',
linkUrl: 'https://example.com/spring',
placementAssets: {
defaultVideoUrl: 'https://cdn.example.com/1x1.mp4',
defaultThumbnailUrl: 'https://cdn.example.com/1x1-poster.jpg', // optional
rules: [
{
videoUrl: 'https://cdn.example.com/9x16.mp4',
thumbnailUrl: 'https://cdn.example.com/9x16-poster.jpg', // optional
placements: {
publisherPlatforms: ['instagram', 'facebook'],
instagramPositions: ['story', 'reels'],
facebookPositions: ['story', 'facebook_reels'],
},
},
{
videoUrl: 'https://cdn.example.com/4x5.mp4',
placements: {
instagramPositions: ['stream'],
facebookPositions: ['feed'],
},
},
],
},
},
});Rules
| Field | Type | Notes |
|---|---|---|
defaultImageUrl / defaultVideoUrl | string | Required. The catch-all asset for placements no rule matches. Set exactly one (image or video, see below). |
defaultThumbnailUrl | string | Optional. Poster for defaultVideoUrl; Meta auto-generates if omitted. |
rules[] | object[] | 1-10 rules. Each pins one asset to one or more placements. |
rules[].imageUrl / rules[].videoUrl | string | The asset for this rule. Must match the block's media mode. |
rules[].thumbnailUrl | string | Optional poster for videoUrl. |
rules[].placements | object | At least one placement field required (publisherPlatforms and/or a *Positions field). Same vocabulary as the Targeting placements object. |
All-image or all-video, never mixed. A placementAssets block is one ad format. Use imageUrl + defaultImageUrl throughout, or videoUrl + defaultVideoUrl throughout. Mixing the two returns a 400.
placementAssets builds a single placement-customized ad, so it's mutually exclusive with creatives[], adSetId, and dynamicCreative. Meta enforces placement co-selection rules (e.g. profile_feed requires feed) and we surface the resulting error verbatim.
Works on instant-form lead ads too. placementAssets is supported on goal: "lead_generation" (with a leadGenFormId) — the instant form is attached for you (the ad set is configured with the on-ad destination automatically). The auto-optimizing pool dynamicCreative is the exception: it needs a dedicated Dynamic Creative ad set, so dynamicCreative + a lead form returns a 422 (use placementAssets, a single creative, or run dynamicCreative on traffic/conversions).
Carousel ads
A hand-built carousel: 2-10 cards in a deterministic order you choose, unlike the auto-optimizing dynamicCreative pool. Pass carouselCards on POST /v1/ads/create:
const result = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: 'acc_metaads_123',
adAccountId: 'act_1234567890',
name: 'Spring collection carousel',
goal: 'traffic',
budgetAmount: 50,
budgetType: 'daily',
body: 'Three drops, one collection.',
linkUrl: 'https://example.com/collection',
callToAction: 'SHOP_NOW',
carouselCards: [
{ imageUrl: 'https://cdn.example.com/1.jpg', headline: 'The classic', linkUrl: 'https://example.com/classic' },
{ imageUrl: 'https://cdn.example.com/2.jpg', headline: 'The bold', description: 'New colorway' },
{ imageUrl: 'https://cdn.example.com/3.jpg', headline: 'The limited', callToAction: 'LEARN_MORE' },
],
},
});Per card, imageUrl is required; headline, description, linkUrl and callToAction are optional and fall back to the top-level values. The top-level fields keep their usual meaning: body is the primary text above the carousel, linkUrl is the see-more fallback link Meta requires, callToAction is the default CTA. Per-card image hashes come back on the created ad's creative.carouselCards.
One shape at a time. carouselCards is mutually exclusive with imageUrl / video, creatives[], adSetId, dynamicCreative, placementAssets, existingCreativeId, leadGenFormId, and goal: "catalog_sales" — the 400 names the conflicting field.
To read per-card performance, use the creative-asset breakdowns (image_asset, title_asset, ...).
Advantage+ creative enhancements
creativeFeatures opts a creative in or out of Meta's individual Advantage+ enhancements (auto-adjusted brightness, enhanced CTA, text optimizations, ...). It's accepted on POST /v1/ads/create (single, carousel and dynamic shapes) and on POST /v1/ads/creatives, and forwarded as Meta's degrees_of_freedom_spec.creative_features_spec:
{
"creativeFeatures": {
"enhance_cta": "OPT_IN",
"image_brightness_and_contrast": "OPT_IN",
"text_optimizations": "OPT_OUT"
}
}- The map is partial: keys are raw snake_case Meta feature names, and anything you don't mention defaults to
OPT_OUT. An unknown key returns Meta's400verbatim, which lists what it accepts (a read-back returns ~30 feature keys). - The legacy
standard_enhancementsbundle is deprecated by Meta (subcode3858504): individual features only. Old creatives still carrying it can't even be duplicated, rebuild them withcreativeFeatures.
Call-to-action values
callToAction accepts 27 values on all /v1/ads/create shapes: LEARN_MORE, SHOP_NOW, SIGN_UP, BOOK_TRAVEL, CONTACT_US, DOWNLOAD, GET_OFFER, GET_QUOTE, SUBSCRIBE, WATCH_MORE, ADD_TO_CART, APPLY_NOW, BOOK_NOW, BUY_NOW, BUY_TICKETS, DONATE, DONATE_NOW, GET_DIRECTIONS, GET_SHOWTIMES, LISTEN_NOW, ORDER_NOW, PLAY_GAME, REQUEST_TIME, SEE_MENU, START_ORDER, INSTALL_MOBILE_APP, USE_APP.
CALL_NOW and the messaging CTAs are not in this list on purpose: they belong to /v1/ads/call and /v1/ads/messaging, which build the ad set those CTAs require.
Swap the creative on a live ad
Creatives are immutable on Meta beyond their name, so "editing" an ad's creative means building a new one and swapping it in. PUT /v1/ads/{adId} does both in one call:
await zernio.adcampaigns.updateAd({
path: { adId: 'AD_ID' },
body: {
creative: {
headline: 'New angle',
body: 'Same offer, sharper copy.',
callToAction: 'SHOP_NOW',
linkUrl: 'https://example.com/spring',
imageUrl: 'https://cdn.example.com/v2.jpg',
},
},
});Use videoUrl instead of imageUrl for a video swap. The same endpoint also accepts targeting — on Meta the edit lands on the ad's ad set targeting, so there's no need to recreate anything. To manage creatives as standalone objects (list, rename, reuse via existingCreativeId), see Creative Library.