Ads
Build and fill a Meta product catalog, group products into sets, and run catalog ads that show each person the products they are most likely to buy.
A catalog ad is one ad that covers many products. Instead of uploading an image per item, you point the ad at a product set and Meta renders a card for each product, then picks which ones each person sees based on what they browsed. Pozy manages the catalog behind it too: create a catalog, add products one at a time, in bulk or from a feed file, and define product sets.
Catalogs and catalog ads run on Meta (Facebook and Instagram). The same catalog can also power Facebook and Instagram Shops and a WhatsApp Business catalog.
#Run a catalog ad
You need a Meta ads connection, an ad account with a pixel, and a catalog with at least one product set. If the catalog already exists in Meta Commerce Manager, start at step 1. To build one from scratch, see Build a catalog first.
#Find the catalog
GET /v1/ads/catalogs lists the catalogs the ad account's business owns or has been shared.
curl "https://api.pozy.ai/v1/ads/catalogs?accountId=66b2e19d8c3f5a7e9d0b1c2e&adAccountId=act_1234567890" \
-H "Authorization: Bearer $POZY_API_KEY"{
"businessId": "1092837465019283",
"catalogs": [
{ "id": "1003405825408877", "name": "Home store", "vertical": "commerce", "productCount": 412, "businessId": "1092837465019283" }
]
}The business is resolved from the first of these you give: businessId, the owner of adAccountId, the owner of a WhatsApp Business Account (when accountId is a WhatsApp connection), or the only business the Meta login can see.
#Pick the product set
The ad promotes a product set, not the whole catalog. List the sets with GET /v1/ads/catalogs/{catalogId}/product-sets:
{ "productSets": [{ "id": "1003260032095191", "name": "All products", "productCount": 412 }] }#Create the ad
Send goal: "catalog_sales" with all three promotedObject fields. There is no imageUrl or video: Meta builds the visuals from each product, and your copy becomes a template filled per item.
curl -X POST https://api.pozy.ai/v1/ads/create \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: home-store-catalog-q1" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2e",
"adAccountId": "act_1234567890",
"name": "Home store, catalog",
"goal": "catalog_sales",
"budgetAmount": 40,
"budgetType": "daily",
"headline": "{{product.name}}",
"body": "Free shipping on orders over $50",
"description": "{{product.price}}",
"callToAction": "SHOP_NOW",
"linkUrl": "https://shop.example.com",
"countries": ["US"],
"optimizationGoal": "OFFSITE_CONVERSIONS",
"promotedObject": {
"productSetId": "1003260032095191",
"pixelId": "1729525464415281",
"customEventType": "PURCHASE"
}
}'const res = await fetch("https://api.pozy.ai/v1/ads/create", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Idempotency-Key": "home-store-catalog-q1",
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "66b2e19d8c3f5a7e9d0b1c2e",
adAccountId: "act_1234567890",
name: "Home store, catalog",
goal: "catalog_sales",
budgetAmount: 40,
budgetType: "daily",
headline: "{{product.name}}",
body: "Free shipping on orders over $50",
description: "{{product.price}}",
callToAction: "SHOP_NOW",
linkUrl: "https://shop.example.com",
countries: ["US"],
optimizationGoal: "OFFSITE_CONVERSIONS",
promotedObject: {
productSetId: "1003260032095191",
pixelId: "1729525464415281",
customEventType: "PURCHASE",
},
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.ad._id, data.ad.platformObjective);import os, requests
resp = requests.post(
"https://api.pozy.ai/v1/ads/create",
headers={
"Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
"Idempotency-Key": "home-store-catalog-q1",
},
json={
"accountId": "66b2e19d8c3f5a7e9d0b1c2e",
"adAccountId": "act_1234567890",
"name": "Home store, catalog",
"goal": "catalog_sales",
"budgetAmount": 40,
"budgetType": "daily",
"headline": "{{product.name}}",
"body": "Free shipping on orders over $50",
"description": "{{product.price}}",
"callToAction": "SHOP_NOW",
"linkUrl": "https://shop.example.com",
"countries": ["US"],
"optimizationGoal": "OFFSITE_CONVERSIONS",
"promotedObject": {
"productSetId": "1003260032095191",
"pixelId": "1729525464415281",
"customEventType": "PURCHASE",
},
},
)
print(resp.json()["ad"]["status"])The response is a sales campaign (platformObjective: "OUTCOME_SALES"), an ad set bound to the product set, and a catalog template ad, starting in pending_review.
Without optimizationGoal: "OFFSITE_CONVERSIONS", the ad set optimizes for link clicks, which buys clicks for a campaign you meant to drive purchases. Send it, and the ad set optimizes toward the customEventType in promotedObject.
#Template tags
Copy fields accept Meta's catalog tags, replaced per product: {{product.name}}, {{product.price}}, and on a vehicle catalog {{vehicle.make}}, {{vehicle.model}}, {{vehicle.year}} and {{vehicle.price}}.
catalogFormat picks the layout: single_image, carousel_images_multi_items, carousel_images_single_item or carousel_slideshows.
#Rules for catalog ads
promotedObject.productSetId,pixelIdandcustomEventTypeare all required. Meta's "Promoted Object is Required" error usually means the pixel is missing, not the set.- Pozy checks the product set before creating anything. Passing the catalog id where the set id belongs, or a set the login cannot read, returns
400namingpromotedObject.productSetId. - A catalog ad is one creative. It cannot be combined with
creatives[],adSetId,dynamicCreativeorplacementAssets. - Targeting works as on any campaign. Broad targeting is fine here because Meta matches products to people.
- Instagram placements need an Instagram identity. When the Page has no linked Instagram account, Pozy uses the Page-backed one.
- Creating the ad from a known product set id does not need catalog permissions. Reading and editing the catalog does (below).
#Build a catalog
#Permission
Listing, creating and editing catalogs needs Meta's catalog_management permission on the Meta login. Not every login has it. When the connection you use cannot manage catalogs, Meta answers 403 (code 100). Pass catalogAccountId naming another connected facebook, instagram or metaads account whose Meta login does.
#Create the catalog
curl -X POST https://api.pozy.ai/v1/ads/catalogs \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2e", "adAccountId": "act_1234567890", "name": "Home store", "vertical": "commerce" }'vertical defaults to commerce. Others include vehicles, hotels, flights, destinations, home_listings, local_service_businesses and services. Deleting a catalog (DELETE /v1/ads/catalogs/{catalogId}) removes every product and takes it off any ads and WhatsApp numbers that use it.
#Add products
Products are keyed by retailerId, your SKU. It is your handle for later lookups and updates, and it cannot change.
| Way in | Call | Good for |
|---|---|---|
| One product | POST /v1/ads/catalogs/{catalogId}/products |
Adding items as they go live |
| Bulk | POST /v1/ads/catalogs/{catalogId}/products/batch |
Up to 5,000 creates, updates or deletes in one request |
| Feed file | POST /v1/ads/catalogs/{catalogId}/feeds |
Keeping a large catalog in sync from a CSV, TSV or XML file |
curl -X POST https://api.pozy.ai/v1/ads/catalogs/1003405825408877/products/batch \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2e",
"requests": [
{ "method": "CREATE", "product": {
"retailerId": "sku-981", "name": "Linen throw", "url": "https://shop.example.com/p/linen-throw",
"imageUrl": "https://cdn.example.com/linen-throw.jpg", "price": 64.99, "currency": "USD",
"availability": "in stock", "condition": "new", "brand": "Acme Home" } },
{ "method": "UPDATE", "product": { "retailerId": "sku-772", "salePrice": 39.00, "inventory": 12 } },
{ "method": "DELETE", "product": { "retailerId": "sku-105" } }
]
}'The bulk call answers 202 with handles. Meta processes them in the background; poll GET /v1/ads/catalogs/{catalogId}/batches/{handle} for the status and per-product errors. A CREATE needs name, url, imageUrl, price and currency.
Product fields include description, additionalImageUrls, salePrice with salePriceStartDate and salePriceEndDate, availability, condition, brand, category, googleProductCategory, gtin, mpn, inventory, visibility (published or staging), color, size, gender, material, pattern and customLabel0 to customLabel4. GET .../products?retailerId=sku-981 looks one product up by SKU; prices come back formatted by Meta, such as "$64.99".
#Keep it in sync with a feed
A feed pulls a product file from a URL. With schedule, Meta fetches it on a cadence; without it, trigger each fetch yourself.
{
"accountId": "66b2e19d8c3f5a7e9d0b1c2e",
"name": "Nightly product export",
"schedule": { "interval": "DAILY", "url": "https://shop.example.com/exports/products.csv", "hour": 3 }
}interval is HOURLY, DAILY or WEEKLY (with dayOfWeek). POST .../feeds/{feedId}/uploads with a url fetches a file right away, and GET .../feeds/{feedId}/uploads shows each import with itemsDetected, itemsDeleted, errorCount and warningCount.
If your store is connected to Pozy, you can read its products from the commerce endpoints and push them in with the bulk call, or point a feed at the store's own product export.
#Group products into sets
A product set is a saved filter over the catalog. It uses Meta's filter syntax:
{
"accountId": "66b2e19d8c3f5a7e9d0b1c2e",
"name": "Acme brand, in stock",
"filter": { "and": [ { "brand": { "i_contains": "acme" } }, { "availability": { "eq": "in stock" } } ] }
}A filter on SKUs looks like { "retailer_id": { "is_any": ["sku-981", "sku-772"] } }. Update a set's name or filter with PUT .../product-sets/{productSetId} and remove it with DELETE. Pass the set's id, never the catalog's, as promotedObject.productSetId.
#Sell in WhatsApp too
The same catalog can back a WhatsApp Business number. Link it with POST /v1/whatsapp/catalogs; the catalog must belong to the same business as the WhatsApp Business Account, and catalogAccountId must name a Meta login with catalog_management. See WhatsApp.
#Errors you will see
| Status and code | Cause | Fix |
|---|---|---|
403 (Meta code 100) |
The Meta login lacks catalog_management |
Pass catalogAccountId for a login that has it. |
400 naming promotedObject.productSetId |
The catalog id was sent as the set id, or the set is unreadable | Re-read ids from the product sets list. |
400 naming promotedObject.productCatalogId |
The catalog does not contain that set | Send the catalog that owns the set, or leave it out. |
400 naming creatives |
creatives[] with goal: "catalog_sales" |
Send one creative. |
| Meta "Promoted Object is Required" | The pixel is missing | Add promotedObject.pixelId. |
| Errors in a batch status | Products Meta refused, listed by retailerId |
Fix those products and resend them. |