# Product catalogs and catalog 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](#build-a-catalog) first.

### Find the catalog

`GET /v1/ads/catalogs` lists the catalogs the ad account's business owns or has been shared.

```bash
curl "https://api.pozy.ai/v1/ads/catalogs?accountId=66b2e19d8c3f5a7e9d0b1c2e&adAccountId=act_1234567890" \
  -H "Authorization: Bearer $POZY_API_KEY"
```

```json
{
  "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`:

```json
{ "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.

```bash cURL
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"
    }
  }'
```
```ts Node
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);
```
```python Python
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`.

> **Set the optimization goal:**
> 
> 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`, `pixelId` and `customEventType` are 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 `400` naming `promotedObject.productSetId`.
- A catalog ad is one creative. It cannot be combined with `creatives[]`, `adSetId`, `dynamicCreative` or `placementAssets`.
- 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

```bash
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 |

```bash
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.

```json
{
  "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](https://pozy.ai/docs/commerce/products) 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:

```json
{
  "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](https://pozy.ai/docs/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. |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/ads/catalogs`](https://pozy.ai/docs/api/listAdCatalogs) | List Meta product catalogs |
| [`POST /v1/ads/catalogs`](https://pozy.ai/docs/api/createAdCatalog) | Create a Meta product catalog |
| [`GET /v1/ads/catalogs/{catalogId}`](https://pozy.ai/docs/api/getAdCatalog) | Get a product catalog |
| [`DELETE /v1/ads/catalogs/{catalogId}`](https://pozy.ai/docs/api/deleteAdCatalog) | Delete a product catalog |
| [`GET /v1/ads/catalogs/{catalogId}/products`](https://pozy.ai/docs/api/listAdCatalogProducts) | List a catalog's products |
| [`POST /v1/ads/catalogs/{catalogId}/products`](https://pozy.ai/docs/api/createAdCatalogProduct) | Add a product to a catalog |
| [`POST /v1/ads/catalogs/{catalogId}/products/batch`](https://pozy.ai/docs/api/batchAdCatalogProducts) | Create, update or delete products in bulk |
| [`GET /v1/ads/catalogs/{catalogId}/batches/{handle}`](https://pozy.ai/docs/api/getAdCatalogBatch) | Get a bulk request's status |
| [`GET /v1/ads/catalogs/{catalogId}/products/{productId}`](https://pozy.ai/docs/api/getAdCatalogProduct) | Get a product |
| [`PUT /v1/ads/catalogs/{catalogId}/products/{productId}`](https://pozy.ai/docs/api/updateAdCatalogProduct) | Update a product |
| [`DELETE /v1/ads/catalogs/{catalogId}/products/{productId}`](https://pozy.ai/docs/api/deleteAdCatalogProduct) | Delete a product |
| [`GET /v1/ads/catalogs/{catalogId}/feeds`](https://pozy.ai/docs/api/listAdCatalogFeeds) | List a catalog's product feeds |
| [`POST /v1/ads/catalogs/{catalogId}/feeds`](https://pozy.ai/docs/api/createAdCatalogFeed) | Create a product feed |
| [`GET /v1/ads/catalogs/{catalogId}/feeds/{feedId}/uploads`](https://pozy.ai/docs/api/listAdCatalogFeedUploads) | List a feed's uploads |
| [`POST /v1/ads/catalogs/{catalogId}/feeds/{feedId}/uploads`](https://pozy.ai/docs/api/createAdCatalogFeedUpload) | Fetch a feed file now |
| [`GET /v1/ads/catalogs/{catalogId}/product-sets`](https://pozy.ai/docs/api/listAdCatalogProductSets) | List a catalog's product sets |
| [`POST /v1/ads/catalogs/{catalogId}/product-sets`](https://pozy.ai/docs/api/createAdCatalogProductSet) | Create a product set |
| [`PUT /v1/ads/catalogs/{catalogId}/product-sets/{productSetId}`](https://pozy.ai/docs/api/updateAdCatalogProductSet) | Update a product set |
| [`DELETE /v1/ads/catalogs/{catalogId}/product-sets/{productSetId}`](https://pozy.ai/docs/api/deleteAdCatalogProductSet) | Delete a product set |
