Skip to main content

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.

  1. Catalogs: connecting a catalog currently returns error #100.

  2. 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

Status: Completed12 comments

Log in to comment and vote

Comments12

  • Miquel Palet López

    Team•

    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_spec exactly as sent, and the ad set comes back with is_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/catalogs to list catalogs and GET /v1/ads/catalogs/{catalogId}/product-sets to 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

        Team•

        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 own headline, body and description alongside its imageUrl or videoUrl. 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_spec referenced from asset_customization_rules, on a normal ad set with no dynamic creative, and it requires optimization_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: true to 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_creative on the ad set. In the Marketing API this is asset_feed_spec with multiple bodies / titles on a standard ad set (optimization_type: DEGREES_OF_FREEDOM, no asset_customization_rules), and it can be combined with object_story_spec. Could you expose top-level bodies[] / headlines[] / descriptions[] (max 5) on POST /v1/ads/create for single image/video ads, and — if Meta allows it — alongside placementAssets too? dynamicCreative doesn'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/catalogs returns 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/catalogs and /product-sets can't work for any customer. Can you confirm the status of that permission on your app?

          Damian, adhocq.io

          • Miquel Palet López

            Team•

            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/create takes bodies[], headlines[] and descriptions[] (1 to 5 each) next to a normal single image or video. That is exactly the shape you describe: asset_feed_spec with optimization_type: DEGREES_OF_FREEDOM, no asset_customization_rules, on a standard ad set with is_dynamic_creative left 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 with is_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 headline and body stay as the preview text; the arrays are the pool. It is documented on the bodies field 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. An asset_feed_spec that carries asset_customization_rules plus 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 whether optimization_type is 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/catalogs fails with Meta's "(#100) This application has not been approved to use this api" for customer tokens. That is the app-level catalog_management approval 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 as promotedObject.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_rules rule 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/create receives placementAssets together with bodies[] / headlines[] / descriptions[] (max 5 each), translate to the labelled asset_feed_spec above instead of rejecting the combination. No is_dynamic_creative on 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

                Team•

                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_story and title_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/create with placementAssets plus top-level bodies[] and/or headlines[] (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 own body or headline keeps that text pinned for its placement and only the other rules share the pool, so you can mix both styles in one creative. validateOnly: true works 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 via placementAssets.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

                  Team•

                  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.