---
title: "HubSpot WooCommerce Product Sync: Push & Update Products"
description: "Mirror your WooCommerce catalog into HubSpot — push products on publish, PATCH on update — with the 2026-03 HubSpot CRM API and Code Glue. Free + Pro paths."
url: "https://wpwebhooks.org/examples/hubspot-product-sync-woocommerce/"
date: "2026-05-15"
---

[WP Webhooks](/) / [Examples](/examples/)/ [HubSpot × WooCommerce](/examples/hubspot-woocommerce-integration/) / Product Sync

/Example · HubSpot × WooCommerce

# HubSpot WooCommerce Product Sync: Push & Update Products from WC

Mirror your WooCommerce catalog into HubSpot. Publish a WC product → HubSpot product created. Edit it → HubSpot PATCHed. Built on the new 2026-03 products endpoint, with a free one-way path and a Pro stateful path that prepares the ground for line-item associations in Part 3.

**~10 min read** May 15, 2026 Part **2** of 5

[](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwpwebhooks.org%2Fexamples%2Fhubspot-product-sync-woocommerce%2F "Share on LinkedIn")[](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwpwebhooks.org%2Fexamples%2Fhubspot-product-sync-woocommerce%2F&text=HubSpot%20WooCommerce%20Product%20Sync%3A%20Push%20%26%20Update%20Products%20from%20WC "Share on X")

Filter OK Pro hubspotwoocommerceproduct-synccatalog

TL;DR — What you'll build

-   **Webhook 1 — Product Create:** fires on `woocommerce_new_product` + `woocommerce_update_product`, POSTs WC product fields to `https://api.hubapi.com/crm/objects/2026-03/products`
-   **Post-dispatch Code Glue:** stores returned HubSpot product ID in WC product meta as `_hubspot_product_id` — Part 3 reads this
-   **Webhook 2 — Product Update:** fires on `woocommerce_update_product`, PATCHes `/crm/objects/2026-03/products/{{ _hs_product_id }}` only when an ID already exists
-   **Condition split keeps them orthogonal:** create has `_hs_existing_id is_empty`; update has `_hs_product_id is_not_empty`

/ Prerequisites

## What You Need **Before Starting**

⚡

This hubspot woocommerce product sync uses [Webhook Actions by Flow Systems](/wordpress-webhook-plugin/). The create webhook works on the free plugin alone — one-way push, no Code Glue. The update webhook requires Pro (Code Glue + dynamic URL templates) and is the prerequisite for line-item associations in [Part 3](/examples/hubspot-woocommerce-integration/).

1.  1
    
    **WordPress + WooCommerce with at least one product to test**
    
    Triggers fire for any product type — simple, variable, virtual, downloadable.
    
2.  2
    
    **Webhook Actions (free) v1.12.0+**
    
    Search for **FlowSystems** under **Plugins → Add Plugin**.
    
3.  3
    
    **Webhook Actions Pro Pro v1.1.0+ — only for the update half**
    
    Required for Code Glue and `{{ field.path }}` URL templates. Skip if you're doing one-way push only.
    
4.  4
    
    **HubSpot Private App token with product scopes**
    
    If you followed [Part 1](/examples/hubspot-create-deal-woocommerce/), edit your existing app and add `e-commerce` scope. Copy the new token.
    
5.  5
    
    **Webhook Actions API token with `full` scope**
    
    Create at **WP Admin → Webhook Actions → API Tokens**.
    
    shell variables
    
    ```
    export SITE="https://your-wordpress-site.com"
    export TOKEN="fswa_full_..."
    export HS_PAT="pat-eu1-xxxxxxxx..."
    ```
    

/ Architecture

## Two Webhooks, **Same Product Object**

WooCommerce fires `woocommerce_new_product` on creation and `woocommerce_update_product` on every subsequent edit. The publish-from-draft transition fires _update_, not _new_ — so we listen to both. HubSpot's 2026-03 products endpoint takes the same JSON shape as deals: a `properties` bag at the body root.

The trick that keeps two webhooks orthogonal on the same trigger: opposite conditions. Webhook 32 fires only when no HubSpot ID is stored yet. Webhook 33 fires only when one is. Each WC update event matches exactly one.

flow diagram

```
# 1. PRODUCT PUBLISHED (first save with status=publish)
WooCommerce product saved
  └── fires woocommerce_new_product | woocommerce_update_product
        └── Webhook #1 POST /crm/objects/2026-03/products
              └── HubSpot 201 + {"id": "30258041729"}
                    └── post-glue: update_post_meta($product_id, "_hubspot_product_id", "30258041729")

# 2. PRODUCT EDITED (any subsequent change)
Product price/name/desc changed
  └── fires woocommerce_update_product
        └── pre-glue: read _hubspot_product_id → inject as _hs_product_id
              └── URL template resolves → /crm/objects/2026-03/products/30258041729
                    └── Webhook #2 PATCH with updated {"properties": ...}
                          └── HubSpot 200
```

**Same hard rule as Part 1.** Pre-dispatch glue is payload shaping only — no `update_post_meta`, no outbound HTTP. Pre-glue runs on every retry and replay, so any side effect there duplicates. Writes go to post-glue. [Read why →](/examples/hubspot-create-deal-woocommerce/)

FIG 01 — Product create vs update split

/ Step 1

## Create the **"Product Create"** Webhook

Register a single webhook on two triggers so the first publish and any new-product REST call both fire it.

First, store the HubSpot PAT once in the plugin's **Credentials Vault** — webhooks then reference it by id via `auth_credential_id`, which takes precedence over the legacy plaintext `auth_header`. The secret is encrypted at rest and write-only over the API: it never shows up in webhook configs, logs, or agent responses.

one-time — store the PAT in the Credentials Vault

```
CRED_ID=$(curl -sk -X POST "$SITE/wp-json/fswa/v1/credentials" \
  -H "X-FSWA-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "HubSpot PAT", "type": "bearer", "secret": "'"$HS_PAT"'"}' | jq -r '.id')
```

step 1.1 — create the webhook

```
curl -sk -X POST "$SITE/wp-json/fswa/v1/webhooks" \
  -H "X-FSWA-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "HS - Create Product",
    "endpoint_url": "https://api.hubapi.com/crm/objects/2026-03/products",
    "http_method": "POST",
    "auth_credential_id": '"$CRED_ID"',
    "is_enabled": true,
    "is_synchronous": true,
    "retry_limit": 1,
    "triggers": ["woocommerce_new_product", "woocommerce_update_product"]
  }'
```

response — webhook ID 32

```
{
  "id": "32",
  "name": "HS - Create Product",
  "endpoint_url": "https://api.hubapi.com/crm/objects/2026-03/products",
  "http_method": "POST"
}
```

### 1.2 — Condition: only fire if the product has no HubSpot ID yet

Repeated updates must not create new HubSpot products. The condition checks a pre-glue-injected field (`_hs_existing_id`) and skips dispatch when it's not empty.

add condition

```
for TRIGGER in woocommerce_new_product woocommerce_update_product; do
  curl -sk -X POST \
    "$SITE/wp-json/fswa/v1/schemas/webhook/32/trigger/$TRIGGER" \
    -H "X-FSWA-Token: $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "conditions": {
        "enabled": true,
        "type": "and",
        "rules": [{"field": "_hs_existing_id", "operator": "is_empty"}]
      },
      "conditions_evaluate_on": "transformed"
    }'
done
```

**Common bug: `evaluate_on` placement.** Webhook Actions reads `conditions_evaluate_on` as a _top-level_ field on the schema, not nested inside `conditions`. Putting `"evaluate_on": "transformed"` inside the conditions object is silently dropped and the condition then evaluates against the original (pre-glue) payload — meaning fields like `_hs_product_id` that only exist after pre-glue won't match. Verified live 2026-05-15 on webhook-actions.local.

/ Step 2 Pro

## Map WC Product → HubSpot Product, **Capture the ID**

### 2.1 — Pre-dispatch: map WC product to HubSpot properties

snippet code — PHP

```
// Pre-dispatch glue — payload shaping only. NO side effects.

$product    = $payload["args"][0] ?? [];
$productId  = is_object($product) ? $product->get_id() : ((int)($product["id"] ?? 0));
if (!$productId) return $payload;

$wc = wc_get_product($productId);
if (!$wc) return $payload;

$existingId = (string) get_post_meta($productId, "_hubspot_product_id", true);

return [
    "_wc_product_id"  => (string) $productId,
    "_hs_existing_id" => $existingId,
    "properties"       => [
        "name"        => $wc->get_name(),
        "description" => wp_strip_all_tags($wc->get_short_description() ?: $wc->get_description()),
        "price"       => (string) $wc->get_regular_price(),
        "hs_sku"      => $wc->get_sku() ?: "wc-{$productId}",
        "hs_url"      => get_permalink($productId),
    ],
];
```

**Variations.** For variable products, branch on `$wc->is_type("variable")` and either iterate `$wc->get_children()` (recommended for Part 3) or flatten variations into a single HubSpot product with attribute properties.

### 2.2 — Post-dispatch: store the returned HubSpot product ID

snippet code — PHP

```
// Post-dispatch glue — side effects allowed.

$data      = json_decode($responseBody, true);
$productId = (int) ($payload["_wc_product_id"] ?? 0);

if ($responseCode === 201 && !empty($data["id"]) && $productId) {
    update_post_meta($productId, "_hubspot_product_id", $data["id"]);
    update_post_meta($productId, "_hubspot_product_synced_at", current_time("mysql"));
}
```

### 2.3 — Attach both snippets

attach pre + post glue to both triggers

```
for TRIGGER in woocommerce_new_product woocommerce_update_product; do
  curl -sk -X POST \
    "$SITE/wp-json/fswa/v1/pro/trigger-snippets/32/trigger/$TRIGGER" \
    -H "X-FSWA-Token: $TOKEN" \
    -H "Content-Type: application/json" \
    -d "{"pre_snippet_id": $PRE_ID, "pre_enabled": true, "post_snippet_id": $POST_ID, "post_enabled": true}"
done
```

Webhook Actions admin → Code Glue Snippets list with Product Create pre + post snippets attached to webhook 32.

/ Step 3 Pro

## Create the **"Product Update"** Webhook

Same trigger as Step 1, opposite condition. It fires only when the product _already_ has `_hubspot_product_id`, PATCHes the HubSpot product by ID, and refreshes the property bag.

step 3.1 — create the update webhook

```
curl -sk -X POST "$SITE/wp-json/fswa/v1/webhooks" \
  -H "X-FSWA-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "HS - Update Product",
    "endpoint_url": "https://api.hubapi.com/crm/objects/2026-03/products/{{ _hs_product_id }}",
    "http_method": "PATCH",
    "auth_credential_id": '"$CRED_ID"',
    "is_enabled": true,
    "is_synchronous": false,
    "retry_limit": 2,
    "triggers": ["woocommerce_update_product"]
  }'
```

### 3.2 — Pre-glue: inject ID from product meta

snippet code — PHP

```
// Pre-dispatch glue — read product meta, shape PATCH body.

$product   = $payload["args"][0] ?? [];
$productId = is_object($product) ? $product->get_id() : ((int)($product["id"] ?? 0));
$hsId      = $productId ? (string) get_post_meta($productId, "_hubspot_product_id", true) : "";
if (!$hsId) return $payload;

$wc = wc_get_product($productId);
if (!$wc) return $payload;

return [
    "_hs_product_id" => $hsId,
    "properties"     => [
        "name"        => $wc->get_name(),
        "description" => wp_strip_all_tags($wc->get_short_description() ?: $wc->get_description()),
        "price"       => (string) $wc->get_regular_price(),
        "hs_sku"      => $wc->get_sku() ?: "wc-{$productId}",
    ],
];
```

### 3.3 — Condition: only fire when there's an ID to PATCH against

add condition

```
curl -sk -X POST \
  "$SITE/wp-json/fswa/v1/schemas/webhook/33/trigger/woocommerce_update_product" \
  -H "X-FSWA-Token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "conditions": {
      "enabled": true,
      "type": "and",
      "rules": [{"field": "_hs_product_id", "operator": "is_not_empty"}]
    },
    "conditions_evaluate_on": "transformed"
  }'
```

**The two-webhook trick.** Webhook 32 has `_hs_existing_id is_empty`. Webhook 33 has `_hs_product_id is_not_empty`. The same `woocommerce_update_product` event flows through both pipelines — the condition system picks exactly one based on whether the meta is already present. No coordination, no race. **Note:** webhook IDs depend on your install — substitute your own.

/ Step 4

## End-to-End Test — **Publish, Edit, Verify**

**WP Admin → Products → Add New.** Name, price, SKU (e.g. `WIDGET-001`). Click **Publish**. Within a second, the Webhook Actions log shows 201 from webhook 32, and HubSpot's **CRM → Products** shows the new entry.

Open the same product, change the price, click **Update**. The Webhook Actions log shows webhook 32 _skipped_ (matched `_hs_existing_id is_not_empty`) and webhook 33 returning 200 against `/crm/objects/2026-03/products/{ID}`. Refresh HubSpot — price reflects the new value.

HubSpot → CRM → Products with the newly synced WooCommerce widget visible.

Webhook Actions admin → Logs showing webhook 32 (201) on first publish, webhook 32 (skipped) + webhook 33 (200) on the second edit.

/ Backfill

## One-Off Sync for **Existing Products**

If you already have a catalog, fire the create webhook once per existing product. The simplest path is WP-CLI:

wp-cli backfill loop

```
wp eval '
$products = wc_get_products(["limit" => -1, "status" => "publish", "return" => "ids"]);
foreach ($products as $pid) {
    if (get_post_meta($pid, "_hubspot_product_id", true)) {
        continue;
    }
    do_action("woocommerce_new_product", $pid);
    echo "fired for product $pid
";
}'
```

**Why `do_action` instead of curl loops?** Firing the WC action runs the full Webhook Actions pipeline (conditions, pre-glue, dispatch, post-glue) so post-glue still persists `_hubspot_product_id` on every success. Curl-looping the HubSpot API directly skips that and loses the link back to WC meta.

/ Common Pitfalls

## Things **That Bite**

**Same trigger, two webhooks, no condition.** Forget the `_hs_existing_id is_empty` / `is_not_empty` split and every product update creates a duplicate. The condition keeps the two orthogonal.

**SKU collisions.** HubSpot stores `hs_sku` as free-text. Empty WC SKUs would all share an identity in HubSpot reporting — the fallback `wc-{$productId}` prevents that.

**Variations vs simple products.** Pick one model and document it: per-variation HubSpot products (recommended for Part 3) or flattened parent with attribute properties.

**HubSpot scope 403.** Response `{"status":"error","message":"This app hasn't been granted required scopes"}` means the Private App is missing `e-commerce`. Edit the app, add it, refresh `$HS_PAT`.

**Pre-glue side effects.** Re-stating: no `update_post_meta` in pre-glue. Tag the product as in-flight from a post-glue branch instead.

/ HubSpot × WooCommerce Series

[← Previous Part 1 — Create & Update Deals](/examples/hubspot-create-deal-woocommerce/) [All 5 parts Series Index](/examples/hubspot-woocommerce-integration/) [Next → Part 3 — Order Line Items](/examples/woocommerce-order-line-items-hubspot/)

/Notes

→ [WordPress Webhook REST API](/webhook-wordpress-plugin-api/) — full reference for webhooks, schemas, conditions, and logs

→ [Webhook Actions by Flow Systems](/wordpress-webhook-plugin/) — plugin overview

→ [HubSpot Products — create-product reference (2026-03)](https://developers.hubspot.com/docs/api-reference/latest/crm/objects/products/create-product)

→ [HubSpot Products — update-product reference (2026-03)](https://developers.hubspot.com/docs/api-reference/latest/crm/objects/products/update-product)

→ [All WordPress webhook automation examples](/examples/)

/FAQ

## Common questions always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

Why sync WooCommerce products to HubSpot at all? +

HubSpot's line\_items API requires an hs\_product\_id to associate purchased items with a deal. Without product sync, deals have no SKU-level detail in HubSpot reporting.

Do I need Pro for this hubspot woocommerce product sync? +

No — one-way push runs on the free plugin. Pro is required if you want post-dispatch persistence of the returned hs\_product\_id (which Part 3 needs).

What happens on product update? Does it create duplicates? +

Not with the two-webhook condition split. Webhook 32 fires only when \_hs\_existing\_id is empty; webhook 33 fires only when \_hs\_product\_id is not empty. Exactly one matches per WC update event.

Why not use HubSpot's e-commerce bridge for products? +

The ecommerce-bridge endpoint is being deprecated in favor of the unified CRM objects API. The 2026-03 dated path is the supported way to push products.

What about product variations? +

Two options — push each variation as its own HubSpot product (recommended for Part 3), or collapse variations into a single HubSpot product with attribute properties.

/More examples

## Related integrations.

[

Part 1

Create & Update Deals from WooCommerce Orders

Build the canonical hubspot woocommerce integration: deal on order placed, Closed Won on completion.

Read →](/examples/hubspot-create-deal-woocommerce/)[

Part 3 (next)

Order Line Items in HubSpot

Iterate WC line\_items, POST each to HubSpot, associate to the deal — using the product IDs you just stored.

Read →](/examples/woocommerce-order-line-items-hubspot/)[

Series Hub

HubSpot × WooCommerce — All 5 Parts

The complete integration series — deals, products, line items, customers, and the full graph.

Read →](/examples/hubspot-woocommerce-integration/)

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"HubSpot WooCommerce Product Sync: Push and Update Products on Publish","description":"Mirror your WooCommerce catalog into HubSpot — push products on publish, PATCH on update — with the 2026-03 HubSpot CRM API and Code Glue.","datePublished":"2026-05-15","dateModified":"2026-05-15","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org","sameAs":["https://flowsystems.pl"]},"url":"https://wpwebhooks.org/examples/hubspot-product-sync-woocommerce/","image":{"@type":"ImageObject","url":"https://wpwebhooks.org/examples/hubspot-woocommerce-integration/og_image.jpg"},"keywords":["hubspot woocommerce","hubspot product sync","woocommerce product sync hubspot","hubspot wordpress plugin","woocommerce hubspot integration"],"isPartOf":{"@type":"CreativeWorkSeries","name":"HubSpot × WooCommerce Integration Series","url":"https://wpwebhooks.org/examples/hubspot-woocommerce-integration/"}}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Why sync WooCommerce products to HubSpot at all?","acceptedAnswer":{"@type":"Answer","text":"HubSpot's line_items API requires an hs_product_id to associate purchased items with a deal. Without product sync, deals have no SKU-level detail in HubSpot reporting."}},{"@type":"Question","name":"Do I need Pro for this hubspot woocommerce product sync?","acceptedAnswer":{"@type":"Answer","text":"No — one-way push runs on the free plugin. Pro is required if you want post-dispatch persistence of the returned hs_product_id (which Part 3 needs)."}},{"@type":"Question","name":"What happens on product update? Does it create duplicates?","acceptedAnswer":{"@type":"Answer","text":"Not with the two-webhook condition split. Webhook 32 fires only when _hs_existing_id is empty; webhook 33 fires only when _hs_product_id is not empty. Exactly one matches per WC update event."}},{"@type":"Question","name":"Why not use HubSpot's e-commerce bridge for products?","acceptedAnswer":{"@type":"Answer","text":"The ecommerce-bridge endpoint is being deprecated in favor of the unified CRM objects API. The 2026-03 dated path is the supported way to push products."}},{"@type":"Question","name":"What about product variations?","acceptedAnswer":{"@type":"Answer","text":"Two options — push each variation as its own HubSpot product (recommended for Part 3), or collapse variations into a single HubSpot product with attribute properties."}}]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Examples","item":"https://wpwebhooks.org/examples/"},{"@type":"ListItem","position":3,"name":"HubSpot × WooCommerce","item":"https://wpwebhooks.org/examples/hubspot-woocommerce-integration/"},{"@type":"ListItem","position":4,"name":"Product Sync","item":"https://wpwebhooks.org/examples/hubspot-product-sync-woocommerce/"}]}
```
