Adding Facebook catalog and the post option for 5 headlines and primary text in Facebook ads
Hi Zernio team,
I'd like to request two features related to Facebook Ads.
Catalogs: connecting a catalog currently returns error #100.
Multiple headlines and primary texts: it's not possible to add more than one headline or primary text to an ad, while the Meta API allows up to five of each.
It will help my platform greatly is these could be added.
Thanks in advance,
Damian
adhocq.io
Log in to comment and vote
Comments12
Miquel Palet López
Sep 10
Both are sorted, and one of them was already possible, so here is the straight answer on each.
Multiple headlines and primary texts. This works today and I am sorry it was not obvious. Meta exposes it through dynamic creative, so you send pools rather than repeating fields, on
POST /v1/ads/create:"dynamicCreative": { "imageUrls": ["…"], "titles": ["Headline 1", "Headline 2", "Headline 3"], "bodies": ["Primary text 1", "Primary text 2", "Primary text 3"], "descriptions": ["Description 1", "Description 2"], "linkUrls": ["https://…"], "callToActionTypes": ["SHOP_NOW"], "adFormat": "SINGLE_IMAGE" }Up to five of each, which is Meta's own limit. Verified in production today: the pools land on the creative's
asset_feed_specexactly as sent, and the ad set comes back withis_dynamic_creative: true, so Meta rotates them for you.The catalog error 100. That was almost certainly a catalog id passed where a product set id belongs. They look identical, both are long numeric ids, and Meta's response was unhelpful about it. We now check before writing anything, so instead of error 100 you get:
400 "Meta did not return a product set for the supplied ID. promotedObject.productSetId must identify a product set, not a catalog ID. Select a visible product set via GET /v1/ads/catalogs/{catalogId}/product-sets and ensure its catalog is shared with the business that owns the ad account."Nothing is created when that fires, so you do not end up with a half-built campaign. Use
GET /v1/ads/catalogsto list catalogs andGET /v1/ads/catalogs/{catalogId}/product-setsto get the id you actually want. We confirmed today that a create with the correct product set id goes through and echoes the promoted object back.If you still see error 100 after using a real product set id, the usual remaining cause is the catalog not being shared with the business that owns the ad account. Send us the ids and we will look.
Mark Eter
Sep 10
Thanks for confirming that dynamicCreative works for text pools. For our use case, dynamicCreative is not the right fit.
Our core ad shape is placement customization (PAC): one ad with a fixed feed asset (e.g. 4:5) and a fixed story/reels asset (e.g. 9:16) in the same creative what Ads Manager calls “different creative per placement”. On your API that maps to placementAssets / asset_customization_rules, not to a single auto-optimizing pool.
For multiple headlines and primary texts, Meta’s Marketing API supports a different pattern than dynamicCreative: asset_feed_spec with labeled bodies/titles (e.g. body_feed / body_story, title_feed / title_story) and optimization_type: PLACEMENT. Meta rotates the text variants while keeping media pinned per placement without setting is_dynamic_creative on the ad set.
Your docs mark dynamicCreative and the bodies/headlines arrays as mutually exclusive with placementAssets. That leaves us unable to publish PAC sets with multiple text options through POST /v1/ads/create today.
Would you consider supporting multiple text options alongside placementAssets — the same combination Meta allows natively or exposing the underlying asset_feed_spec shape for placement-customized ads? That would unblock us without breaking feed+story sets
Miquel Palet López
Sep 10
Shipped. You can now put different copy on each placement in the same placement-customized ad.
Each entry in
placementAssets.rules[]accepts its ownheadline,bodyanddescriptionalongside itsimageUrlorvideoUrl. Your feed 4:5 and story 9:16 assets stay pinned exactly as they are today, and each now carries its own text. The top-level copy remains the default for placements no rule matches and for any field a rule omits.{ "headline": "Default headline", "body": "Default primary text", "placementAssets": { "defaultImageUrl": "https://cdn.example.com/1x1.jpg", "rules": [ { "imageUrl": "https://cdn.example.com/9x16.jpg", "headline": "Swipe up", "body": "Made for Stories", "placements": { "instagramPositions": ["story", "reels"] } }, { "imageUrl": "https://cdn.example.com/4x5.jpg", "headline": "Shop the collection", "body": "Made for Feed", "placements": { "instagramPositions": ["stream"] } } ] } }One important correction to your description, because it changes what you should build. You said Meta rotates the text variants while media stays pinned per placement. We could not establish that. What we verified is that copy is pinned per placement: each placement receives exactly the text its rule specifies, and Meta is not given a pool to optimise between. If you were planning to rely on rotation within a placement, do not, because we cannot demonstrate it happens.
You were right about the mechanism though. Meta accepts labelled bodies and titles in
asset_feed_specreferenced fromasset_customization_rules, on a normal ad set with no dynamic creative, and it requiresoptimization_type: PLACEMENT. Omitting that fails Meta's validation. That is what we build for you now.Two smaller things worth knowing. If a rule sets only
description, the other placements get a blank description rather than inheriting the default, so set every copy field you want on a rule that overrides any of them. And Meta suppresses primary text and headline on Stories and Reels in delivery, so copy that must be visible there still has to be part of the asset itself.Verified against Meta today with real 4:5 and 9:16 assets: the full campaign, ad set, creative and ad tree validated, nothing created. Use
validateOnly: trueto check your own payload before spending.Mark Eter
Sep 21
Hi Miquel,
Thanks for shipping per-placement copy. Two things are still open on our side:
1. Multiple text options without dynamic creative. We're not asking for rotation within a placement and not for DCO. We want what Ads Manager calls "text options": up to 5 primary texts and 5 headlines on a normal ad, media stays pinned per placement, no
is_dynamic_creativeon the ad set. In the Marketing API this isasset_feed_specwith multiplebodies/titleson a standard ad set (optimization_type: DEGREES_OF_FREEDOM, noasset_customization_rules), and it can be combined withobject_story_spec. Could you expose top-levelbodies[]/headlines[]/descriptions[](max 5) onPOST /v1/ads/createfor single image/video ads, and — if Meta allows it — alongsideplacementAssetstoo?dynamicCreativedoesn't work for us because it forces DCO on the ad set, which excludes our placement-customized ads in the same ad set.2. Catalogs: error #100 is on your side.
GET /v1/ads/catalogsreturns Meta error #100 "application has not been approved". That's not a catalog-ID vs product-set-ID mix-up on our end; it means your Meta app doesn't have Catalog API access (catalog_management) approved. Until that's fixed,GET /v1/ads/catalogsand/product-setscan't work for any customer. Can you confirm the status of that permission on your app?Damian, adhocq.io
Miquel Palet López
Sep 21
Both answered, one of them with a correction on my side.
1. Text options without dynamic creative: this exists today, as top-level fields.
POST /v1/ads/createtakesbodies[],headlines[]anddescriptions[](1 to 5 each) next to a normal single image or video. That is exactly the shape you describe:asset_feed_specwithoptimization_type: DEGREES_OF_FREEDOM, noasset_customization_rules, on a standard ad set withis_dynamic_creativeleft off, so several ads can share the ad set. Verified live again today: the creative came back from Meta with the three bodies, two titles and two descriptions, and the ad set withis_dynamic_creative: false.{ "accountId": "…", "adAccountId": "act_…", "goal": "traffic", "budgetType": "daily", "budgetAmount": 20, "imageUrl": "https://cdn.example.com/4x5.jpg", "linkUrl": "https://example.com", "callToAction": "SHOP_NOW", "headline": "Headline one", "body": "Primary text one", "bodies": ["Primary text one", "Primary text two", "Primary text three"], "headlines": ["Headline one", "Headline two"], "descriptions": ["Description one", "Description two"] }The top-level
headlineandbodystay as the preview text; the arrays are the pool. It is documented on thebodiesfield of the create endpoint, and I am sorry it was not more visible when you first asked.Alongside
placementAssets: Meta does not allow it, so we cannot either. I tested this directly against the Marketing API today rather than relying on the docs. Anasset_feed_specthat carriesasset_customization_rulesplus more than one body is rejected with error 100, subcode 1885878, "Multiple bodies assets can not be applied to rule #1", and it makes no difference whetheroptimization_typeis PLACEMENT, DEGREES_OF_FREEDOM or omitted. A rule accepts exactly one body and one title. So on a placement-customized ad each placement gets one text, which is what shipped for you last week; a text pool only exists on an ad that is not customized per placement. Two ads in the same ad set, one of each kind, is fine.2. Catalogs: you are right, and my earlier read was wrong for your case.
GET /v1/ads/catalogsfails with Meta's "(#100) This application has not been approved to use this api" for customer tokens. That is the app-levelcatalog_managementapproval on our Meta app, and it is not granted yet. Until it is, the catalog listing endpoints only work for accounts that hold a developer role on our app, which is why they worked in our own tests. Creating ads against a product set is not gated by it: take the product set id from Commerce Manager and pass it aspromotedObject.productSetId. I will post here the moment the approval lands.Mark Eter
Sep 22
Hi Miquel,
Thanks for the live test on subcode 1885878 and for shipping per-placement copy. Two follow-ups.
1. Text options on a placement-customized ad
We agree with the constraint you found: a single
asset_customization_rulesrule cannot reference multiple bodies. But that is not how Meta's Marketing API expresses text options on a PAC creative, and we don't think it means the combination is impossible.Before we moved to Zernio, we published exactly this shape directly against Graph API (v24), in one ad, on a standard ad set with
is_dynamic_creative: false:"asset_feed_spec": { "optimization_type": "PLACEMENT", "images": [ { "hash": "<4x5>", "adlabels": [{ "name": "feed_image" }] }, { "hash": "<9x16>", "adlabels": [{ "name": "story_image" }] } ], "bodies": [ { "text": "Primary text 1", "adlabels": [{ "name": "body_feed" }, { "name": "body_story" }] }, { "text": "Primary text 2", "adlabels": [{ "name": "body_feed" }, { "name": "body_story" }] } ], "titles": [ { "text": "Headline 1", "adlabels": [{ "name": "title_feed" }, { "name": "title_story" }] }, { "text": "Headline 2", "adlabels": [{ "name": "title_feed" }, { "name": "title_story" }] } ], "asset_customization_rules": [ { "customization_spec": { "publisher_platforms": ["facebook","instagram"], "facebook_positions": ["feed","marketplace","profile_feed"], "instagram_positions": ["stream","explore","profile_feed"] }, "image_label": { "name": "feed_image" }, "body_label": { "name": "body_feed" }, "title_label": { "name": "title_feed" } }, { "customization_spec": { "publisher_platforms": ["facebook","instagram"], "facebook_positions": ["story"], "instagram_positions": ["story","reels"] }, "image_label": { "name": "story_image" }, "body_label": { "name": "body_story" }, "title_label": { "name": "title_story" } } ] }The difference from what you tested: each rule references one label, and multiple bodies/titles share that label via
adlabels. Meta accepted this for us with 2 bodies × 2 titles, media pinned per placement, no DCO. This is what Ads Manager produces when you enable "text options" on an ad that uses "different creative per placement".Our request: when
POST /v1/ads/createreceivesplacementAssetstogether withbodies[]/headlines[]/descriptions[](max 5 each), translate to the labelledasset_feed_specabove instead of rejecting the combination. Nois_dynamic_creativeon the ad set, no splitting into multiple ads.If Meta now rejects even this labelled form, we'd like to know that specifically, since it would mean our previous Graph API path is deprecated rather than a gap in the mapping.
Thanks, Damian — adhocq.io
Miquel Palet López
Sep 23
You were right, and I owe you a correction: my probe put several bodies on one rule directly, not through a shared label. Your labelled shape is accepted by Meta today, I reproduced it on our own ad account with 2 bodies × 2 titles sharing
body_feed/body_storyandtitle_feed/title_story, and a mixed variant (pool on the feed rule, one pinned body on the story rule) also passed. So your previous Graph path is not deprecated; the gap was in our mapping.Shipped, live now (spec 1.56.0).
POST /v1/ads/createwithplacementAssetsplus top-levelbodies[]and/orheadlines[](max 5 each) now builds exactly that feed:optimization_type: PLACEMENT, every pooled body and headline carries the label of each rule, media stays pinned per rule, nothing dynamic on the ad set, one ad. A rule that sets its ownbodyorheadlinekeeps that text pinned for its placement and only the other rules share the pool, so you can mix both styles in one creative.validateOnly: trueworks with it for image assets.{ "headline": "Default headline", "body": "Default primary text", "bodies": ["Primary text 1", "Primary text 2"], "headlines": ["Headline 1", "Headline 2"], "placementAssets": { "defaultImageUrl": "https://cdn.example.com/1x1.jpg", "rules": [ { "imageUrl": "https://cdn.example.com/4x5.jpg", "placements": { "facebookPositions": ["feed"], "instagramPositions": ["stream"] } }, { "imageUrl": "https://cdn.example.com/9x16.jpg", "placements": { "facebookPositions": ["story"], "instagramPositions": ["story", "reels"] } } ] } }One limit, and it is Meta's:
descriptions[]cannot join a placement-customized creative. Labelled or not, a second description on a rule is refused with 100 / 1885878 "Multiple descriptions assets can not be applied to rule #1" (probed the same way, same day). We return a 400 that says so; keep one description per rule viaplacementAssets.rules[].description.Same caveat as before on delivery: Meta accepts the pool, we cannot observe how it rotates.
Catalog listing approval: still with Meta, I will post here when it lands.
Mark Eter
Sep 23
Hi Miquel,
Thank you for the quick and thorough updates. Everything is in good shape now and working as expected.
I really appreciate how engaged and responsive you’ve been throughout this. It makes a big difference.
I’m looking forward to seeing how the catalog setup performs in practice. Fingers crossed it lands well on our side.
Thanks again. You’re a star.
Best, Damian
Mark Eter
Sep 28
Hi Miquel,
Is there any update for the catalog option?
Best, Damian
Miquel Palet López
Oct 23
Hi Damian, sorry for the silence on this thread. Catalog access is still waiting on Meta's App Review of the catalog_management permission for our app. It depends entirely on Meta, so we can't give a firm date, but we hope to have it next month. I'll post here as soon as it's approved, and you'll only need to reconnect your Meta Ads connection once to pick it up.
In the meantime, creating ads against a product set you already have still works: take the product set id from Commerce Manager and pass it as
promotedObject.productSetId.