Commerce
One API over Shopify and WooCommerce stores: products, variants, inventory, collections, discounts, channels, markets, metafields, pages and menus, plus syncing a store into a Meta catalog.
The Commerce API is one platform-neutral API at /v1/commerce over every connected store. Write an integration once and it works on Shopify and on WooCommerce (self-hosted WordPress). Pick the store with accountId: a query parameter on reads and a body field on writes. It is the _id of the connected shopify or wordpress account.
All data lives on the store. Zernio proxies each call and stores nothing, except the catalog syncs you create.
Conventions
| Convention | Detail |
|---|---|
| Store | accountId of a connected shopify account, or of a wordpress account whose site runs WooCommerce. Commerce objects report platform: "shopify" or platform: "woocommerce". |
| Ids | Products, variants, collections and the rest use the platform's own ids (numeric strings). Read them from responses; never construct them. |
| Money | Responses return { "amount": "24.00", "currency": "USD" } with the amount as a decimal string. Write bodies take a bare decimal amount in the store's currency. |
| Status | status uses Zernio's vocabulary (active, draft, pending_review, rejected, inactive, archived, deleted). The platform's raw value is always next to it as platformStatus. |
| Timestamps | ISO 8601, UTC. |
| Pagination | Lists return { <resource>, nextCursor }. Pass limit and the previous nextCursor as cursor; nextCursor is null on the last page. The cursor is opaque. |
| Unsupported operation | An operation the store's platform cannot serve answers 400 platform_not_supported. Check capabilities on Get a store first. |
Connect a store
A Commerce store is an account you already connect for blogs:
- Shopify: the OAuth flow with the store's
myshopify.comdomain, an install from the Shopify App Store, or a custom-app Admin token. See Shopify. - WooCommerce: connect the self-hosted WordPress site with a username and an Application Password. See WordPress. The user must be allowed to manage WooCommerce. WordPress.com sites cannot serve WooCommerce through Zernio: the WordPress.com token reaches no WooCommerce API.
Capabilities and extra permissions
Call Get a store before anything else. It returns the store's currency and country, the capabilities it can serve now, the missingCapabilities the platform supports but this store has not granted, and grantPermissionsUrl.
curl "https://zernio.com/api/v1/commerce/store?accountId=66b2e19d8c3f5a7e9d0b1c2d" \
-H "Authorization: Bearer $ZERNIO_API_KEY"{
"store": {
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platform": "shopify",
"name": "Acme",
"domain": "acme.myshopify.com",
"url": "https://acme.com",
"currency": "USD",
"country": "US",
"capabilities": ["products.read", "products.create", "products.update", "products.status", "products.price", "products.variants", "products.images", "collections.read", "collections.write", "collections.metafields", "metafields.read", "metafields.write", "pages.read", "pages.write"],
"missingCapabilities": ["products.images_remove", "inventory.read", "inventory.write", "channels.read", "channels.write", "discounts.read", "discounts.write", "discounts.codes", "navigation.read", "navigation.write", "metaobjects.read", "metaobjects.write", "markets.read", "markets.write", "marketing.write"],
"grantPermissionsUrl": "https://admin.shopify.com/store/acme/oauth/install?client_id=...&optional_scopes=write_files%2Cwrite_inventory%2Cread_locations%2C..."
}
}Shopify
Every install grants the products, collections, metafields and pages capabilities. The rest need optional scopes that the store owner approves separately, so a store that only uses blogs is never asked for them. Send the owner to grantPermissionsUrl: it opens the Shopify admin and approves the missing scopes on the existing install, with no reinstall, and the owner can revoke them later. Call Get a store again afterwards to read the new capabilities.
| Capability | Shopify scope |
|---|---|
products.read, collections.read, metafields.read | read_products (always granted) |
products.create, products.update, products.status, products.price, products.variants, products.images, collections.write, collections.metafields, metafields.write | write_products (always granted) |
pages.read / pages.write | read_content / write_content (always granted) |
products.images_remove | write_files |
inventory.read | write_inventory and read_locations |
inventory.write | write_inventory and read_locations |
channels.read, channels.write | write_publications |
discounts.read, discounts.write, discounts.codes | write_discounts |
navigation.read, navigation.write | write_online_store_navigation |
metaobjects.read, metaobjects.write | write_metaobjects |
markets.read | write_markets |
markets.write | write_markets and write_products |
marketing.write | write_marketing_events |
grantPermissionsUrl is null when nothing is missing, and on a store connected with a custom-app token: that token carries the custom app's scopes, not Zernio's, so the merchant adds the scopes to their custom app instead. A store connected before product access existed lacks the required product scopes, which the link does not cover: reconnect it.
Calling an operation whose scope is missing answers 403 insufficient_permissions, and the message points to grantPermissionsUrl.
WooCommerce
WooCommerce has no optional permissions, so grantPermissionsUrl is always null. The store capabilities appear when the site runs WooCommerce and the connected user can manage it; pages.read and pages.write appear when the user can edit pages, even on a site without WooCommerce. See WooCommerce differences for what the platform does not serve.
Endpoint map
Every operation also lists its platforms on its reference page.
Products
| Operation | Endpoint | Platforms |
|---|---|---|
| Get a store | GET /v1/commerce/store | Both |
| List products | GET /v1/commerce/products | Both |
| Get a product | GET /v1/commerce/products/{productId} | Both |
| Create a product | POST /v1/commerce/products | Both |
| Update a product | PATCH /v1/commerce/products/{productId} | Both |
| Duplicate a product | POST /v1/commerce/products/{productId}/duplicate | Both |
| Activate, deactivate, archive or delete products | POST /v1/commerce/products/state | Both |
| Add or remove tags in bulk | POST /v1/commerce/products/tags | Both |
| Update variant prices | POST /v1/commerce/products/{productId}/price | Both |
Variants, options and images
| Operation | Endpoint | Platforms |
|---|---|---|
| Add variants | POST /v1/commerce/products/{productId}/variants | Both |
| Delete variants | DELETE /v1/commerce/products/{productId}/variants | Both |
| Add options | POST /v1/commerce/products/{productId}/options | Both |
| Delete options | DELETE /v1/commerce/products/{productId}/options | Both |
| Add images | POST /v1/commerce/products/{productId}/images | Both |
| Remove images | DELETE /v1/commerce/products/{productId}/images | Both |
| Reorder images | POST /v1/commerce/products/{productId}/images/reorder | Both |
Inventory and locations
| Operation | Endpoint | Platforms |
|---|---|---|
| List locations | GET /v1/commerce/locations | Both |
| Get a product's stock | GET /v1/commerce/inventory | Both |
| Set or adjust stock | POST /v1/commerce/products/{productId}/inventory | Both |
Collections
On WooCommerce, collections are product categories.
| Operation | Endpoint | Platforms |
|---|---|---|
| List collections | GET /v1/commerce/collections | Both |
| Get a collection | GET /v1/commerce/collections/{collectionId} | Both |
| Create a collection | POST /v1/commerce/collections | Both |
| Update a collection | PATCH /v1/commerce/collections/{collectionId} | Both |
| Delete a collection | DELETE /v1/commerce/collections/{collectionId} | Both |
| Add or remove products | POST /v1/commerce/collections/{collectionId}/products | Both |
| Reorder products | POST /v1/commerce/collections/{collectionId}/reorder | Shopify |
Discounts
On WooCommerce, discounts are coupons.
| Operation | Endpoint | Platforms |
|---|---|---|
| List discounts | GET /v1/commerce/discounts | Both |
| Get a discount | GET /v1/commerce/discounts/{discountId} | Both |
| Create a discount | POST /v1/commerce/discounts | Both |
| Update a discount | PATCH /v1/commerce/discounts/{discountId} | Both |
| Activate or deactivate | POST /v1/commerce/discounts/{discountId}/state | Both |
| Add codes | POST /v1/commerce/discounts/{discountId}/codes | Shopify |
| Delete a discount | DELETE /v1/commerce/discounts/{discountId} | Both |
Sales channels
| Operation | Endpoint | Platforms |
|---|---|---|
| List sales channels | GET /v1/commerce/channels | Shopify |
| Publish or unpublish a product | POST /v1/commerce/products/{productId}/channels | Shopify |
| Publish or unpublish a collection | POST /v1/commerce/collections/{collectionId}/channels | Shopify |
Markets and price lists
| Operation | Endpoint | Platforms |
|---|---|---|
| List markets | GET /v1/commerce/markets | Shopify |
| List price lists | GET /v1/commerce/price-lists | Shopify |
| Set fixed prices | PUT /v1/commerce/price-lists/{priceListId}/prices | Shopify |
| Remove fixed prices | DELETE /v1/commerce/price-lists/{priceListId}/prices | Shopify |
Metafields and metaobjects
| Operation | Endpoint | Platforms |
|---|---|---|
| List product metafields | GET /v1/commerce/products/{productId}/metafields | Both |
| Set product metafields | PUT /v1/commerce/products/{productId}/metafields | Both |
| Delete product metafields | DELETE /v1/commerce/products/{productId}/metafields | Both |
| List collection metafields | GET /v1/commerce/collections/{collectionId}/metafields | Shopify |
| Set collection metafields | PUT /v1/commerce/collections/{collectionId}/metafields | Shopify |
| Delete collection metafields | DELETE /v1/commerce/collections/{collectionId}/metafields | Shopify |
| List metaobject definitions | GET /v1/commerce/metaobject-definitions | Shopify |
| List metaobjects of a type | GET /v1/commerce/metaobjects | Shopify |
| Get a metaobject | GET /v1/commerce/metaobjects/{metaobjectId} | Shopify |
| Create a metaobject | POST /v1/commerce/metaobjects | Shopify |
| Update a metaobject | PATCH /v1/commerce/metaobjects/{metaobjectId} | Shopify |
| Delete a metaobject | DELETE /v1/commerce/metaobjects/{metaobjectId} | Shopify |
Pages, menus and redirects
On WooCommerce, pages are the WordPress site's pages.
| Operation | Endpoint | Platforms |
|---|---|---|
| List pages | GET /v1/commerce/pages | Both |
| Get a page | GET /v1/commerce/pages/{pageId} | Both |
| Create a page | POST /v1/commerce/pages | Both |
| Update a page | PATCH /v1/commerce/pages/{pageId} | Both |
| Delete a page | DELETE /v1/commerce/pages/{pageId} | Both |
| List navigation menus | GET /v1/commerce/menus | Shopify |
| Get a navigation menu | GET /v1/commerce/menus/{menuId} | Shopify |
| Create a navigation menu | POST /v1/commerce/menus | Shopify |
| Replace a navigation menu | PUT /v1/commerce/menus/{menuId} | Shopify |
| Delete a navigation menu | DELETE /v1/commerce/menus/{menuId} | Shopify |
| List URL redirects | GET /v1/commerce/redirects | Shopify |
| Create a URL redirect | POST /v1/commerce/redirects | Shopify |
| Update a URL redirect | PATCH /v1/commerce/redirects/{redirectId} | Shopify |
| Delete a URL redirect | DELETE /v1/commerce/redirects/{redirectId} | Shopify |
Marketing activities
Record a post, ad or message you ran for the merchant in the store's Marketing section, with its link and UTM parameters for attribution. Use your own id (for example the Zernio post id) as remoteId.
| Operation | Endpoint | Platforms |
|---|---|---|
| Record a marketing activity | PUT /v1/commerce/marketing-activities | Shopify |
| Delete a marketing activity | DELETE /v1/commerce/marketing-activities/{remoteId} | Shopify |
| Report daily engagement | POST /v1/commerce/marketing-activities/{remoteId}/engagements | Shopify |
Examples
List and create products
curl "https://zernio.com/api/v1/commerce/products?accountId=66b2e19d8c3f5a7e9d0b1c2d&status=active&limit=50" \
-H "Authorization: Bearer $ZERNIO_API_KEY"query is passed to the platform's product search as is, and collectionId narrows the list to one collection. A status the platform has no equivalent of returns an empty page.
A new product starts as draft unless you send status: "active". It takes up to 3 options, 100 variants and 20 images:
curl -X POST "https://zernio.com/api/v1/commerce/products" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"title": "Linen Shirt",
"descriptionHtml": "<p>Breathable linen, cut for summer.</p>",
"status": "active",
"images": [{ "url": "https://cdn.example.com/linen-shirt.jpg", "altText": "Linen shirt" }],
"options": [{ "name": "Size", "values": ["M", "L"] }],
"variants": [
{ "sku": "LS-M", "price": "49.00", "options": [{ "name": "Size", "value": "M" }] },
{ "sku": "LS-L", "price": "49.00", "options": [{ "name": "Size", "value": "L" }] }
]
}'Activate, deactivate, archive or delete products takes up to 50 product ids and reports each product's outcome, so one failure does not abort the rest. A delete is permanent on both platforms.
Set stock
Read the location ids with List locations, then set or adjust per variant. mode: "set" makes quantity the new available count; mode: "adjust" adds it (negative to subtract). The response is the product's stock after the change.
curl -X POST "https://zernio.com/api/v1/commerce/products/8214569812345/inventory" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"mode": "adjust",
"changes": [{ "variantId": "44021234567890", "locationId": "71234567890", "quantity": -2 }]
}'Sync a store to a Meta catalog
A catalog sync keeps a Meta product catalog in step with a store, for catalog ads (goal: catalog_sales) and Shops. It works on both platforms.
You need:
accountId: the store.catalogAccountId: a connectedfacebook,instagramormetaadsaccount whose Meta login can manage the catalog (thecatalog_managementpermission). A WhatsApp account cannot hold a sync, because background runs refresh the catalog token through a Meta login.catalogId: the numeric Meta catalog id. Find it with List catalogs.
curl -X POST "https://zernio.com/api/v1/commerce/catalog-syncs" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"catalogAccountId": "66c3f2a0b1d4e5f6a7b8c9d0",
"catalogId": "1234567890123456"
}'The call answers 202 with the sync in runStatus: "pending", and the first full run starts in the background. One store syncs to a given catalog once: a second create for the same pair is 409 catalog_sync_conflict. A catalog can take several stores.
Once the run has finished, Get a catalog sync reads:
{
"catalogSync": {
"id": "66d4a1b2c3d4e5f6a7b8c9d0",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"catalogPlatform": "meta",
"catalogAccountId": "66c3f2a0b1d4e5f6a7b8c9d0",
"catalogId": "1234567890123456",
"runStatus": "succeeded",
"lastRunStartedAt": "2026-09-29T05:20:04.000Z",
"lastRunFinishedAt": "2026-09-29T05:21:37.000Z",
"lastError": null,
"itemsSent": 412,
"itemsSkipped": 9,
"itemsDeleted": 3,
"createdAt": "2026-09-20T10:02:11.000Z"
}
}What a run does
Each product variant becomes one catalog item, grouped by product (item_group_id). A product is listed only when it is active, published to the online store (it has a storefront url) and has at least one image. Item ids are namespaced to the store (<platform>:<store>:<variantId>), so a run only ever touches items this store put in the catalog.
A full run pushes every listable variant, then deletes this store's items that are no longer listable or no longer exist. The counters describe the last run:
| Field | Meaning |
|---|---|
itemsSent | Variant items Meta accepted in the last run (items Meta rejected are not counted). |
itemsSkipped | Active products left out because they are not published or have no image. |
itemsDeleted | Items of this store removed from the catalog because their product or variant is gone or no longer listable. |
lastError | Why the last run failed, or a note when Meta rejected some items or had not finished processing a batch. |
runStatus is pending until the first run starts, running during a run and succeeded or failed after it. A run in which Meta rejected every item is failed. A run that has not finished after 30 minutes reads as failed and is retried on the next daily pass.
Keeping it current
- Product changes: a product created, updated or deleted on the store refreshes its items within minutes. Shopify notifies Zernio through its product webhooks; WooCommerce through webhooks Zernio registers on the site when a self-hosted site is connected and again when a sync is created. A deleted product queues a full run of the sync, since its variant ids are gone with it.
- Daily pass: every sync that has not run in the last 20 hours runs once a day, which catches missed deliveries and deleted variants. Syncs of a disconnected store are skipped until it is reconnected.
- On demand: Run a catalog sync now (
POST /v1/commerce/catalog-syncs/{syncId}/run) queues a full run and answers202. It is409 catalog_sync_conflictwhile a run is in progress.
The same product changes also reach your own webhooks as commerce.product.created, commerce.product.updated and commerce.product.deleted, with the store and product id only; read the product with Get a product.
Stop a sync
Stop a catalog sync (DELETE /v1/commerce/catalog-syncs/{syncId}) stops syncing. Items already in the catalog stay there. List catalog syncs and Get a catalog sync read the state; reading and stopping keep working after the store is disconnected.
WooCommerce differences
WooCommerce is reached through the self-hosted site's REST API with the connected user's Application Password. What differs from Shopify:
| Area | On WooCommerce |
|---|---|
Not served (400 platform_not_supported) | Reordering collection products, adding codes to a discount (no discounts.codes), sales channels, navigation menus, URL redirects, metaobjects, markets and price lists, marketing activities. |
| Products | vendor, productType and seo are not supported (400). deactivate sets the product to draft and archive to private (inactive). A WooCommerce pending product reads as pending_review and a trashed one as deleted. |
| Collections | Product categories. They have no SEO fields and no sort order (400). |
| Discounts | Coupons, so code discounts only (no automatic discounts). A coupon cannot start in the future or require a minimum quantity (400). Deactivating one sets its expiry to now. |
| Inventory | One location, with the id default. An adjust reads the count and writes the sum, since WooCommerce has no atomic increment: a sale landing in between can be overwritten. |
| Metafields | Product custom fields (meta data) only: stores do not list collections.metafields, and collection metafields answer 400 platform_not_supported. A key without a dot is returned under the namespace woocommerce; namespace.key round-trips. |
| Pagination | The cursor wraps a page number and a page holds at most 100 rows. |
| Pages | WordPress pages, available whenever the user can edit pages, with or without WooCommerce. |
Common errors
| Error | Cause | Fix |
|---|---|---|
400 platform_not_supported | The account is not a store, or the store's platform does not serve this operation | Check capabilities on Get a store. |
400 invalid_field_value | The platform rejected a field, or the field does not exist on this platform | Read the message; see WooCommerce differences. |
403 insufficient_permissions | The store has not granted the scope the operation needs | Send the store owner to grantPermissionsUrl, or reconnect the store. |
404 product_not_found / 404 resource_not_found | The id was deleted or belongs to another store | List the resource to find a current id. |
409 catalog_sync_conflict | The store already syncs to that catalog, or a run is already in progress | Run the existing sync, or wait for the run to finish. |
429 | Rate limited by Zernio or by the platform | Retry later. See rate limits. |
502 | The platform failed or was unavailable | Retry once the earlier request's outcome is known. |
Related
- Commerce API reference: every endpoint with its fields.
- Shopify: connecting a store, blogs and tracking pixels.
- WordPress: connecting a site, blogs and tracking tags.
- Product catalogs: the Meta catalogs a sync writes to.
- Pixels and tracking tags: installing ad pixels on a store.
Pixels and Tracking Tags
One API for every ad platform's pixel or site tag: list, create, update, read stats, share, manage users and conversion events, and install on Shopify or WordPress.
Report Bugs & Requests
Send a structured bug report or feature request from your code or AI agent with POST /v1/feedback. Every report is read by the Zernio team.