Creatives
Attach creatives, video creatives, and placement asset customization
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.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',
},
});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",
)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:
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',
},
});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",
)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.
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.ads.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.ads.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).