# Products and catalog

List, create and edit products, variants, images and stock on Shopify and WooCommerce, organise collections and discounts, and sync a store into a Meta catalog.

The Commerce API manages a connected store's catalog in one platform-neutral shape. The same calls create a product on Shopify or WooCommerce, change its stock, put it in a collection, discount it and keep a Meta catalog in step for catalog ads. Name the store with `accountId` on every call.

## List products

`query` goes to the platform's own product search unchanged (on Shopify, for example, `vendor:Acme tag:summer`). `collectionId` narrows the list to one collection. Pages hold up to 100 products, 20 by default.

```bash cURL
curl "https://api.pozy.ai/v1/commerce/products?accountId=6710c4b2e4b0d2f1a8c95e3a&status=active&limit=50" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
let cursor: string | undefined;
do {
  const query = new URLSearchParams({ accountId: "6710c4b2e4b0d2f1a8c95e3a", status: "active", limit: "50" });
  if (cursor) query.set("cursor", cursor);
  const res = await fetch(`https://api.pozy.ai/v1/commerce/products?${query}`, {
    headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
  });
  const data = await res.json();
  if (!res.ok) throw new Error(data.error);
  for (const p of data.products) console.log(p.id, p.title, p.variants[0]?.price.amount);
  cursor = data.nextCursor ?? undefined;
} while (cursor);
```
```python Python
import os, requests

params = {"accountId": "6710c4b2e4b0d2f1a8c95e3a", "status": "active", "limit": 50}
while True:
    page = requests.get(
        "https://api.pozy.ai/v1/commerce/products",
        headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
        params=params,
    ).json()
    for p in page["products"]:
        print(p["id"], p["title"])
    if not page["nextCursor"]:
        break
    params["cursor"] = page["nextCursor"]
```

Each product carries its `variants` (with `price`, `compareAtPrice`, `sku`, `inventoryQuantity` and `options`), `images`, `featuredImage`, `options`, `tags`, `seo`, the public storefront `url` and `totalInventory`. A `status` the platform has no equivalent for returns an empty page rather than an error.

## Create a product

A new product starts as `draft` unless you send `status: "active"`. It takes up to 3 options, 100 variants and 20 images. Image URLs must be public: the store downloads them.

```bash cURL
curl -X POST https://api.pozy.ai/v1/commerce/products \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6710c4b2e4b0d2f1a8c95e3a",
    "title": "Merino beanie",
    "descriptionHtml": "<p>Soft merino wool, one size.</p>",
    "status": "active",
    "tags": ["winter", "accessories"],
    "images": [{ "url": "https://cdn.example.com/beanie-grey.jpg", "altText": "Grey merino beanie" }],
    "options": [{ "name": "Colour", "values": ["Grey", "Navy"] }],
    "variants": [
      { "sku": "BEANIE-GRY", "price": "29.00", "options": [{ "name": "Colour", "value": "Grey" }] },
      { "sku": "BEANIE-NVY", "price": "29.00", "options": [{ "name": "Colour", "value": "Navy" }] }
    ]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/commerce/products", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "6710c4b2e4b0d2f1a8c95e3a",
    title: "Merino beanie",
    descriptionHtml: "<p>Soft merino wool, one size.</p>",
    status: "active",
    images: [{ url: "https://cdn.example.com/beanie-grey.jpg", altText: "Grey merino beanie" }],
    options: [{ name: "Colour", values: ["Grey", "Navy"] }],
    variants: [
      { sku: "BEANIE-GRY", price: "29.00", options: [{ name: "Colour", value: "Grey" }] },
      { sku: "BEANIE-NVY", price: "29.00", options: [{ name: "Colour", value: "Navy" }] },
    ],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.product.id);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/commerce/products",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "accountId": "6710c4b2e4b0d2f1a8c95e3a",
        "title": "Merino beanie",
        "status": "active",
        "options": [{"name": "Colour", "values": ["Grey", "Navy"]}],
        "variants": [
            {"sku": "BEANIE-GRY", "price": "29.00", "options": [{"name": "Colour", "value": "Grey"}]},
            {"sku": "BEANIE-NVY", "price": "29.00", "options": [{"name": "Colour", "value": "Navy"}]},
        ],
    },
)
product_id = resp.json()["product"]["id"]
```

## Edit products

| Change | Endpoint | Notes |
| --- | --- | --- |
| Title, description, handle, tags, SEO, vendor, type | `PATCH /v1/commerce/products/{productId}` | `tags` replaces the whole list |
| Variant prices and compare-at prices | `POST /v1/commerce/products/{productId}/price` | Send `compareAtPrice: null` to end a sale |
| Add or delete variants and options | `.../variants`, `.../options` | |
| Add, remove or reorder images | `.../images`, `.../images/reorder` | Removal on Shopify needs `products.images_remove` |
| Copy a product | `POST .../duplicate` | New `title`, starts as draft, `includeImages` defaults to `true` |
| Tags on many products | `POST /v1/commerce/products/tags` | `add` and `remove` lists, per-product results |
| Activate, deactivate, archive or delete | `POST /v1/commerce/products/state` | Up to 50 ids, per-product results |
| Custom fields | `PUT`, `GET`, `DELETE .../metafields` | Metafields on Shopify, custom fields on WooCommerce |
| Sales channels (Shopify) | `POST .../channels` | `publish` and `unpublish` lists of channel ids |

Bulk calls report each outcome in `succeeded` and `failed`, so one bad id does not stop the rest:

```json
{
  "action": "archive",
  "succeeded": ["8214569812345"],
  "failed": [{ "productId": "8214569899999", "message": "Product not found" }]
}
```

> **Deletes are permanent:**
> 
> `action: "delete"` removes the product on the platform and cannot be undone. Archive it if you might need it again.

## Set stock

Read location ids with `GET /v1/commerce/locations` (WooCommerce has one, `default`), then change variants. `mode: "set"` makes `quantity` the new available count. `mode: "adjust"` adds it, so send a negative number to subtract.

```bash cURL
curl -X POST https://api.pozy.ai/v1/commerce/products/8214569812345/inventory \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6710c4b2e4b0d2f1a8c95e3a",
    "mode": "adjust",
    "changes": [{ "variantId": "45123456789012", "locationId": "71234567890", "quantity": -3 }]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/commerce/products/8214569812345/inventory", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "6710c4b2e4b0d2f1a8c95e3a",
    mode: "adjust",
    changes: [{ variantId: "45123456789012", locationId: "71234567890", quantity: -3 }],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.inventory[0].levels);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/commerce/products/8214569812345/inventory",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "accountId": "6710c4b2e4b0d2f1a8c95e3a",
        "mode": "adjust",
        "changes": [{"variantId": "45123456789012", "locationId": "71234567890", "quantity": -3}],
    },
)
```

The response is the product's stock after the change, per location: `available`, `onHand`, `committed` and `incoming`. Read it any time with `GET /v1/commerce/inventory?accountId=...&productId=...`. On WooCommerce an adjust reads then writes, so a sale that lands in between can be overwritten.

## Collections and discounts

Collections group products (Shopify collections, WooCommerce categories). Create one with a list of `productIds`, then add or remove products with `POST /v1/commerce/collections/{collectionId}/products` and `add` / `remove` lists. `sortOrder` and SEO fields are Shopify-only.

Discounts take a `method` (`code` or `automatic`) and a `type` (`percentage`, `fixed_amount` or `free_shipping`):

```bash cURL
curl -X POST https://api.pozy.ai/v1/commerce/discounts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6710c4b2e4b0d2f1a8c95e3a",
    "title": "Instagram followers 15%",
    "method": "code",
    "type": "percentage",
    "code": "INSTA15",
    "percentage": 15,
    "oncePerCustomer": true,
    "endsAt": "2026-12-31T23:59:59Z"
  }'
```

Pair a code with a post: create the discount, then publish the code in a caption. WooCommerce supports code discounts (coupons) only, cannot start one in the future and cannot require a minimum quantity. Activate or pause a discount with `POST /v1/commerce/discounts/{discountId}/state`.

## Sync a store into a Meta catalog

A catalog sync keeps a Meta product catalog in step with the store, for catalog ads and shops. You need:

- `accountId`: the store.
- `catalogAccountId`: a connected `facebook`, `instagram` or `metaads` account whose Meta login can manage catalogs. WhatsApp accounts do not work here.
- `catalogId`: the Meta catalog id, from `GET /v1/ads/catalogs`.

```bash cURL
curl -X POST https://api.pozy.ai/v1/commerce/catalog-syncs \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6710c4b2e4b0d2f1a8c95e3a",
    "catalogAccountId": "6710c52de4b0d2f1a8c95e95",
    "catalogId": "1188234455667788"
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/commerce/catalog-syncs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ accountId: "6710c4b2e4b0d2f1a8c95e3a", catalogAccountId: "6710c52de4b0d2f1a8c95e95", catalogId: "1188234455667788" }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.catalogSync.id, data.catalogSync.runStatus); // "pending"
```
```python Python
sync = requests.post(
    "https://api.pozy.ai/v1/commerce/catalog-syncs",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"accountId": "6710c4b2e4b0d2f1a8c95e3a", "catalogAccountId": "6710c52de4b0d2f1a8c95e95", "catalogId": "1188234455667788"},
).json()["catalogSync"]
```

The call returns `202` and starts the first full run. Poll `GET /v1/commerce/catalog-syncs/{syncId}`:

| Field | Meaning |
| --- | --- |
| `runStatus` | `pending`, `running`, then `succeeded` or `failed` |
| `itemsSent` | Variants Meta accepted in the last run |
| `itemsSkipped` | Active products left out because they are unpublished or have no image |
| `itemsDeleted` | This store's items removed because they are gone or no longer listable |
| `lastError` | Why the last run failed, or a note on rejected items |

How a sync behaves:

- One catalog item per variant, grouped by product, with ids namespaced to the store. A run only touches that store's items, so one catalog can hold several stores.
- A product is listable when it is `active`, published to the online store and has at least one image.
- Product changes refresh their items within minutes. Every sync also runs once a day if it has not run in the last 20 hours. A run still going after 30 minutes counts as failed and is retried on the next daily pass.
- Run now with `POST .../run` (`409 catalog_sync_conflict` while a run is in progress). Stop with `DELETE`: items already in the catalog stay.
- One sync per store and catalog pair. Syncs of a disconnected store are skipped until it reconnects.

Then build catalog ads on top: see [Catalogs](https://pozy.ai/docs/ads/catalogs).

## Use products in posts

Products give your posts real data. A common pattern: list active products, pick the newest, and create a post with its first image and storefront `url`, tagged with UTM parameters. On Shopify, record the post as a marketing activity so the merchant sees it in their Marketing section (see [Shopify](https://pozy.ai/docs/platforms/shopify#shopify-only-operations)).

## Webhook events

Pozy subscribes to the store's product changes when it connects. The events carry the store and the product id; fetch the product with `GET /v1/commerce/products/{productId}` for its current state.

| Event | When it fires |
| --- | --- |
| [`commerce.product.created`](https://pozy.ai/docs/webhooks/events#commerce-product-created) | Commerce product created. |
| [`commerce.product.updated`](https://pozy.ai/docs/webhooks/events#commerce-product-updated) | Commerce product updated. |
| [`commerce.product.deleted`](https://pozy.ai/docs/webhooks/events#commerce-product-deleted) | Commerce product deleted. |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/commerce/products`](https://pozy.ai/docs/api/listCommerceProducts) | List products |
| [`POST /v1/commerce/products`](https://pozy.ai/docs/api/createCommerceProduct) | Create a product |
| [`POST /v1/commerce/products/state`](https://pozy.ai/docs/api/changeCommerceProductState) | Activate, deactivate, archive or delete products |
| [`GET /v1/commerce/products/{productId}`](https://pozy.ai/docs/api/getCommerceProduct) | Get a product |
| [`PATCH /v1/commerce/products/{productId}`](https://pozy.ai/docs/api/updateCommerceProduct) | Update a product |
| [`POST /v1/commerce/products/{productId}/price`](https://pozy.ai/docs/api/updateCommerceProductPrices) | Update variant prices |
| [`GET /v1/commerce/collections`](https://pozy.ai/docs/api/listCommerceCollections) | List collections |
| [`POST /v1/commerce/collections`](https://pozy.ai/docs/api/createCommerceCollection) | Create a collection |
| [`GET /v1/commerce/collections/{collectionId}`](https://pozy.ai/docs/api/getCommerceCollection) | Get a collection |
| [`PATCH /v1/commerce/collections/{collectionId}`](https://pozy.ai/docs/api/updateCommerceCollection) | Update a collection |
| [`DELETE /v1/commerce/collections/{collectionId}`](https://pozy.ai/docs/api/deleteCommerceCollection) | Delete a collection |
| [`POST /v1/commerce/collections/{collectionId}/products`](https://pozy.ai/docs/api/changeCommerceCollectionProducts) | Add or remove products in a collection |
| [`POST /v1/commerce/products/{productId}/variants`](https://pozy.ai/docs/api/createCommerceProductVariants) | Add variants |
| [`DELETE /v1/commerce/products/{productId}/variants`](https://pozy.ai/docs/api/deleteCommerceProductVariants) | Delete variants |
| [`POST /v1/commerce/products/{productId}/options`](https://pozy.ai/docs/api/createCommerceProductOptions) | Add options |
| [`DELETE /v1/commerce/products/{productId}/options`](https://pozy.ai/docs/api/deleteCommerceProductOptions) | Delete options |
| [`POST /v1/commerce/products/{productId}/images`](https://pozy.ai/docs/api/addCommerceProductImages) | Add images |
| [`DELETE /v1/commerce/products/{productId}/images`](https://pozy.ai/docs/api/removeCommerceProductImages) | Remove images |
| [`POST /v1/commerce/products/{productId}/images/reorder`](https://pozy.ai/docs/api/reorderCommerceProductImages) | Reorder images |
| [`POST /v1/commerce/products/{productId}/duplicate`](https://pozy.ai/docs/api/duplicateCommerceProduct) | Duplicate a product |
| [`GET /v1/commerce/products/{productId}/metafields`](https://pozy.ai/docs/api/listCommerceProductMetafields) | List product metafields |
| [`PUT /v1/commerce/products/{productId}/metafields`](https://pozy.ai/docs/api/setCommerceProductMetafields) | Set product metafields |
| [`DELETE /v1/commerce/products/{productId}/metafields`](https://pozy.ai/docs/api/deleteCommerceProductMetafields) | Delete product metafields |
| [`POST /v1/commerce/products/{productId}/channels`](https://pozy.ai/docs/api/changeCommerceProductChannels) | Publish or unpublish a product |
| [`GET /v1/commerce/collections/{collectionId}/metafields`](https://pozy.ai/docs/api/listCommerceCollectionMetafields) | List collection metafields |
| [`PUT /v1/commerce/collections/{collectionId}/metafields`](https://pozy.ai/docs/api/setCommerceCollectionMetafields) | Set collection metafields |
| [`DELETE /v1/commerce/collections/{collectionId}/metafields`](https://pozy.ai/docs/api/deleteCommerceCollectionMetafields) | Delete collection metafields |
| [`POST /v1/commerce/collections/{collectionId}/channels`](https://pozy.ai/docs/api/changeCommerceCollectionChannels) | Publish or unpublish a collection |
| [`POST /v1/commerce/products/tags`](https://pozy.ai/docs/api/changeCommerceProductTags) | Add or remove tags in bulk |
| [`POST /v1/commerce/collections/{collectionId}/reorder`](https://pozy.ai/docs/api/reorderCommerceCollectionProducts) | Reorder products in a collection |
| [`GET /v1/commerce/locations`](https://pozy.ai/docs/api/listCommerceLocations) | List locations |
| [`GET /v1/commerce/inventory`](https://pozy.ai/docs/api/listCommerceInventory) | Get a product's stock |
| [`POST /v1/commerce/products/{productId}/inventory`](https://pozy.ai/docs/api/changeCommerceInventory) | Set or adjust stock |
| [`GET /v1/commerce/channels`](https://pozy.ai/docs/api/listCommerceChannels) | List sales channels |
| [`GET /v1/commerce/discounts`](https://pozy.ai/docs/api/listCommerceDiscounts) | List discounts |
| [`POST /v1/commerce/discounts`](https://pozy.ai/docs/api/createCommerceDiscount) | Create a discount |
| [`GET /v1/commerce/discounts/{discountId}`](https://pozy.ai/docs/api/getCommerceDiscount) | Get a discount |
| [`PATCH /v1/commerce/discounts/{discountId}`](https://pozy.ai/docs/api/updateCommerceDiscount) | Update a discount |
| [`DELETE /v1/commerce/discounts/{discountId}`](https://pozy.ai/docs/api/deleteCommerceDiscount) | Delete a discount |
| [`POST /v1/commerce/discounts/{discountId}/state`](https://pozy.ai/docs/api/setCommerceDiscountActive) | Activate or deactivate a discount |
| [`POST /v1/commerce/discounts/{discountId}/codes`](https://pozy.ai/docs/api/addCommerceDiscountCodes) | Add codes to a discount |
| [`GET /v1/commerce/catalog-syncs`](https://pozy.ai/docs/api/listCommerceCatalogSyncs) | List catalog syncs |
| [`POST /v1/commerce/catalog-syncs`](https://pozy.ai/docs/api/createCommerceCatalogSync) | Sync a store into a Meta catalog |
| [`GET /v1/commerce/catalog-syncs/{syncId}`](https://pozy.ai/docs/api/getCommerceCatalogSync) | Get a catalog sync |
| [`DELETE /v1/commerce/catalog-syncs/{syncId}`](https://pozy.ai/docs/api/deleteCommerceCatalogSync) | Stop a catalog sync |
| [`POST /v1/commerce/catalog-syncs/{syncId}/run`](https://pozy.ai/docs/api/runCommerceCatalogSync) | Run a catalog sync now |

The older per-account product endpoints (`/v1/accounts/{accountId}/products`) still work but are deprecated. Use the Commerce API for new code.
