Commerce & blogs
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.
curl "https://api.pozy.ai/v1/commerce/products?accountId=6710c4b2e4b0d2f1a8c95e3a&status=active&limit=50" \
-H "Authorization: Bearer $POZY_API_KEY"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);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.
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" }] }
]
}'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);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:
{
"action": "archive",
"succeeded": ["8214569812345"],
"failed": [{ "productId": "8214569899999", "message": "Product not found" }]
}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.
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 }]
}'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);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):
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 connectedfacebook,instagramormetaadsaccount whose Meta login can manage catalogs. WhatsApp accounts do not work here.catalogId: the Meta catalog id, fromGET /v1/ads/catalogs.
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"
}'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"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_conflictwhile a run is in progress). Stop withDELETE: 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.
#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).
#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 |
Commerce product created. |
commerce.product.updated |
Commerce product updated. |
commerce.product.deleted |
Commerce product deleted. |
#Endpoints
The older per-account product endpoints (/v1/accounts/{accountId}/products) still work but are deprecated. Use the Commerce API for new code.