Work in progress — this plugin is under active development. For questions or support contact info@poletto.es.
Bidirectional sync between WooCommerce products/stock and Holded ERP.
| Direction | Trigger | Notes |
|---|---|---|
| WC → Holded | Real-time on product save or stock change | Can also be triggered manually (bulk push) |
| Holded → WC | Scheduled pull via Action Scheduler | Default every 15 min — configurable. Manual pull available |
| Orders | On payment confirmation (woocommerce_payment_complete) |
Creates a Holded document (invoice or sales order) for each paid order |
Each sync direction can be enabled independently in settings.
| Field | WC → Holded | Holded → WC |
|---|---|---|
| Product name | ✓ | ✓ |
| Regular price | ✓ optional | ✓ optional |
| Sale price | ✓ optional ¹ | — |
| Description | ✓ optional | ✓ optional |
| SKU | ✓ | ✓ |
| Stock quantity | ✓ optional | ✓ optional |
| Brand appended to name | ✓ optional | — |
¹ Sent only when the product is currently on sale (respects scheduled date range). If no sale price is active, the regular price is sent instead.
WooCommerce → Holded Sync in the WordPress admin.
| Option | Description |
|---|---|
| Holded API Key | Generate in Holded under Configuración → Más → Desarrolladores |
| Warehouse | Holded warehouse for stock movements |
| Default tax rate | Fallback VAT rate (default: 21) |
| Sync direction | Enable WC→Holded and/or Holded→WC independently |
| Pull interval | How often to pull from Holded (min 5 min, default 15) |
| Prices include tax | Strips VAT before sending to Holded; adds it back when pulling |
| Sync stock | Enable stock sync (both directions) |
| Sync regular price | Enable regular price sync (both directions) |
| Sync sale price | Send sale price to Holded instead of regular price when on sale (WC→Holded only) |
| Sync description | Enable description sync (both directions) |
| Description source | Custom field (Holded Sync tab) or full WooCommerce description |
| Append brand to name | Appends product_brand taxonomy term to product name in Holded |
| Variation name format | Space, Dash (–) or Parentheses separator between parent name and attribute values |
| Enable log | Stores last 50 sync events for debugging |
| Option | Description |
|---|---|
| Create document | Enable automatic document creation in Holded on payment confirmation |
| Document type | Invoice (factura, reduces Holded stock) or Sales order (pedido de venta, does not affect stock) |
| Avoid stock duplication | After creating a document, re-pushes WC stock to Holded. For invoices: corrects the reduction caused by the invoice. For sales orders: prevents the Holded→WC pull from restoring the pre-sale quantity |
| NIF/CIF meta key | Order meta key where the customer NIF/CIF/NIE is stored. Default: _billing_nif |
| Email meta key | Order meta key for the customer email. Leave empty to use the standard WooCommerce billing email |
When a document is created in Holded, the following meta keys are stored on the WC order:
| Meta key | Content |
|---|---|
_cthls_contact_id |
Holded contact ID (found or created) |
_cthls_invoice_id |
Holded document ID (invoice or sales order) |
The Holded contact ID is also saved on the WP user (_cthls_contact_id) so subsequent orders by the same customer reuse the existing Holded contact without an extra API call.
Each product has a Holded Sync tab in the Product data meta box.
| Field | Meta key | Notes |
|---|---|---|
| Description for Holded | _cthls_description |
Used when Description source is set to "Custom field" |
| Cost price | _cost_price |
Net cost price sent to Holded (excl. tax) |
| Barcode | _barcode |
EAN, UPC or any barcode format |
| Holded product ID | _cthls_product_id |
Auto-populated on first sync. Read-only |
Holded stores prices as net (excl. tax). WooCommerce can store prices either way.
- WC → Holded: if Prices include tax is enabled, the plugin strips VAT before sending the net price to Holded.
- Holded → WC: if Prices include tax is enabled, the plugin adds VAT back before saving in WooCommerce.
All prices are rounded to 2 decimal places.
Products are matched by SKU. On first sync the Holded product ID is stored in _cthls_product_id. Before creating a new product in Holded, the plugin searches by SKU to avoid duplicates — if a match is found it links and updates instead of creating.
- WordPress 6.3+
- WooCommerce (required, 8.0+ recommended) — tested up to 10.7.0
- PHP 7.4+
- Clone or download the ZIP → upload to
/wp-content/plugins/. - Activate from Plugins in WordPress admin.
- Go to WooCommerce → Holded Sync, enter your API key and configure.
- Variable products — flattened to simple in Holded — Holded does not handle product variants reliably via API. Variable products in WooCommerce are pushed to Holded as separate simple products — one per variation — each with its own SKU. The parent variable product is never created in Holded. During pull (Holded → WC), each Holded product is matched by SKU to the corresponding WC variation; only stock and price are updated. The product structure in WooCommerce is never altered by the pull.
- Multiple price tiers — The Holded API does not expose secondary price rates (e.g. Ho.re.ca). Only the main price is synced. Additional price tiers must be set manually in Holded for each product (including each variation pushed as a simple product). Support will be added as soon as Holded makes them available via API.
- Product images — Product images are uploaded automatically to Holded when a product is first created. Updates to the image are not synced (Holded does not support replacing existing images via API).
- Fix: variations sharing the parent's SKU are now skipped (they were all being linked to the same Holded product).
- Fix:
taxfield replaced withtaxes[](array of Holded tax ID strings) — the v2 API ignores the old numeric field; IDs are auto-fetched fromGET /api/v2/taxesand matched by rate. - Fix:
pricefield now sent as a decimal string as required by the v2 API. - Fix: stale
_cthls_product_id(404 from Holded) is cleared and the product is automatically recreated or re-linked by SKU. - Notice: settings page now shows a notice that Holded API v2 (June 2026) with full permissions is required.
- Fix: variations without a SKU are now skipped during sync — they are no longer created in Holded.
- Fix: admin panel notes updated to reflect SKU requirement for variations and automatic image upload behaviour.
- Breaking: migrated to Holded API v2 — base URL updated to
/api/v2/, auth changed fromkey:header toAuthorization: Bearer. - New: product image uploaded automatically to Holded on first product creation.
- Fix: product pagination now cursor-based (removes the practical 50-products-per-page cap of v1).
- Fix: pull prices now parsed correctly (v2 returns comma-decimal strings, e.g.
"16,53"). - Fix: invoice/sales-order endpoints updated (
invoices,sales-orders);contact_idfield andYYYY-MM-DDdate format. - Fix: contact search by NIF now uses the native
?code=filter of v2.
- New: "Document notes" setting — customise the text added to the Holded document notes field; use
%sas a placeholder for the order number. - Fix: admin button icons aligned to the bottom after WP 7 update — added
!importanttodisplay:inline-flexandalign-items:centerto prevent core style override.
- Fix: Holded document notes text changed from "Pedido WooCommerce n.º X" to a configurable site-specific label.
- WordPress 7.0 compatibility.
- Enhancement: configurable log entries limit — new "Log entries limit" field (default 50, range 10–500) replaces the hardcoded 50.
- Enhancement: "Export log (JSON)" button in the system log — downloads the current log as a JSON file.
- Fix: missing translations for Single SKU sync card and several event reference descriptions (es_ES, it_IT).
- Fix: "Avoid stock duplication" now also applies to sales orders. When Holded→WC pull is active and the document type is Sales order, the pull was restoring the pre-sale quantity in WooCommerce (Holded stock not reduced → pull sees old value → overwrites WC). The re-sync now runs after both invoices and sales orders.
- Fix: stock update to Holded was not applying — the
/stockendpoint requires a nested body format{"stock": {"warehouseId": {"productId": delta}}}. Previous versions sent{"stock": {"warehouseId": delta}}which Holded accepted with 200 OK but silently ignored.
- Fix: stock updates were not working because the Holded
/stockendpoint is delta-based (positive = add units, negative = remove), not an absolute setter. The plugin now fetches the current Holded stock first, computes the required delta (desired − current), and sends that. If stock is already at the desired value, no API call is made.
- Fix: stock of simple products was never updated in Holded — only the general product update (
PUT /products/{id}) was called, which Holded ignores for stock. Now also callsPUT /products/{id}/stockafter every product save. - Fix: stock updates now include
warehouseIdwhen a warehouse is configured in the plugin settings. Without it, Holded was updating a different warehouse than the one visible in the UI.
- Fix: stock of variable product variations was not updated in Holded even when the payload correctly contained
stock: 0. Root cause: Holded ignores thestockfield inPUT /products/{id}. Stock is now updated via the dedicatedPUT /products/{id}/stockendpoint after every variation sync.
- Fix: when all variations of a variable product are set to stock 0 in WooCommerce, only the last variation was synced to Holded. Root cause: WooCommerce fires
woocommerce_update_productfor the parent on every variation save; the deduplication guard causedsync_variable_productto run before all variation stocks were committed to the database. Fixed by deferring the sync to theshutdownhook, ensuring all DB writes are complete before stock values are read.
- Enhancement: after a Holded → WC pull that updates at least one product, the LiteSpeed Cache is automatically purged (
litespeed_purge_all). No effect if LiteSpeed Cache is not installed.
- Feature: Orders → Holded documents — on payment confirmation (
woocommerce_payment_complete), the plugin automatically finds or creates the Holded contact (matched by NIF/CIF then email) and creates an invoice or sales order with all line items, shipping and tax. - Feature: Document type setting — choose between Invoice (factura, reduces Holded stock) and Sales order (pedido de venta, does not affect stock).
- Feature: Avoid stock duplication — after creating an invoice, re-pushes WC stock to Holded to correct the stock reduction caused by the invoice. Allows keeping WC→Holded stock push active alongside invoice creation.
- Feature: Variation name format — new "Dash" option (
Benaco – Magnum (1,5 litros)) to avoid nested parentheses when attribute values already contain parentheses. - Enhancement: Holded contact ID stored in order meta (
_cthls_contact_id) and WP user meta for reuse across orders. - Enhancement: new admin settings — NIF/CIF meta key (default
_billing_nif), email meta key (optional override). - Translations (ES, IT) updated for all new strings.
- Enhancement: Manual push, pull and single SKU sync now always run regardless of the automatic sync direction settings — disabling automatic sync no longer blocks manual operations.
- Enhancement: "Sync direction" setting renamed to "Automatic sync direction" with an explanatory note to clarify the distinction.
- Enhancement: Single SKU sync — new card in settings page to push or pull a single product/variation by SKU without running a full bulk sync.
- Debug: added
pull_price_checklog event for variations — logs Holded raw price, converted WC price, current WC price and whether an update is triggered.
- Enhancement: new "Include draft products" option — when enabled, draft products are included in both real-time and bulk push (WC → Holded). The pull direction already handles any product regardless of status.
- Enhancement: new "Variation name format" setting — choose between space ("Benaco Magnum 15 litros") or parentheses ("Benaco (Magnum 15 litros)") when pushing variation names to Holded.
- Fix: variation names pushed to Holded now use the readable attribute label instead of the taxonomy slug (e.g. "Magnum 15 litros" instead of "magnum-15-litros").
- Fix: product name in WooCommerce is never overwritten from Holded — WC is the source of truth. A
pull_name_skippedlog entry is recorded when the name in Holded differs. - Fix: price of WC variations is now correctly synced from Holded during pull (variations are pushed as simple products, so their price is fully accessible via API).
- Fix: when "Append brand to name" is enabled, the product name is no longer overwritten in WooCommerce during pull — prevents the brand from being appended repeatedly on each push/pull cycle ("Averoldi Averoldi Averoldi…").
- Change: variable products in WooCommerce are now pushed to Holded as separate simple products — one per variation — instead of a single product with variants. Holded does not handle variant-type products correctly via API.
- Change: Holded → WC pull now matches Holded products to WC variations by SKU and updates only stock and price; the product structure (parent/variation) in WooCommerce is never modified.
- Change:
_cthls_product_idis now stored on each WC variation directly (not on the parent). Stock sync for variations uses the variation's own Holded product ID. - Known limitation updated: variable product handling and price tiers.
- Fix: variable products no longer send the
variantsarray in PUT requests — the Holded API rejects PUT onkind: variantsproducts regardless of payload. After initial creation, only stock is kept in sync via the dedicated/stockendpoint. Name, price and description changes on variable products must be made manually in Holded. - Known limitation added: variable product updates not supported by Holded REST API.
- (internal — superseded by 1.2.3)
- (internal — superseded by 1.2.2)
- Fix: variant price field corrected from
pricetosubtotal(Holded API input field name). - Fix: each variant now includes a
namefield (attribute values, e.g. "75cl / 6"). - Fix: variant cost falls back to parent product cost when not set on the individual variation.
- Fix: when a product is linked by SKU match, variant IDs are fetched before the update payload is built.
- Fix: variable products already linked to Holded (via SKU match) now correctly fetch variant IDs before the first update, preventing "Cannot update product variants" errors.
- Fix: variable product variants now sync correctly — after each push the plugin fetches Holded variant IDs and stores them on WC variations, so subsequent updates target the correct variant instead of failing with "Cannot update product variants".
- Fix: variable products no longer send
price: 0to Holded — price is managed at variant level. - Fix: removed image sync — Holded API silently ignores the
imagefield; images must be set via the Holded UI. - Enhancement: added "Product images" to Known limitations.
- Fix: log table message column no longer overflows horizontally.
- Enhancement: page reloads automatically after manual push/pull so the log is visible without a manual refresh.
- Fix: removed deprecated
load_plugin_textdomain()call. - Fix: sanitize
_cost_pricePOST input beforewc_format_decimal(). - Enhancement: image sync option (WC → Holded) — sends the product featured image URL to Holded.
- Enhancement: image overwrite control — by default the image is sent only on first sync; enable "Overwrite existing image" to force resend on every sync.
- Fix: unschedule now uses both Action Scheduler and WooCommerce queue API to ensure stale jobs are reliably removed when pull sync is disabled.
- Fix: on init, if pull sync is disabled and an Action Scheduler job is still queued, it is now automatically removed.
- Fix: disabling Holded → WC sync now immediately unschedules the Action Scheduler job; re-enabling reschedules it. "Next scheduled run" is no longer shown when pull is disabled.
- Fix: sale price sync (WC→Holded) now respects sale date range — falls back to regular price outside the scheduled period.
- Fix: Holded→WC price now adds tax back when WC uses tax-inclusive prices, rounded to 2 decimals.
- Fix: WC→Holded price rounded to 2 decimal places.
- Fix: duplicate Holded product prevented via SKU lookup before create.
- Fix: Action Scheduler GROUP constant, self-healing on init,
plugins_loadedpriority raised to 20. - Enhancement: sync direction independently configurable (push / pull).
- Enhancement: manual bulk push (WC→Holded) from settings page.
- Enhancement: sync sale price option (WC→Holded), respecting scheduled dates.
- Enhancement: description source applies bidirectionally.
- Enhancement: event reference legend, next scheduled run, reschedule button in admin.
- Enhancement: known limitations card (Holded price tiers API not supported).
- Enhancement: log limited to 50 entries. Translations updated (it_IT, es_ES).
- Fix: admin buttons restore translated text after each action.
- Renamed to
carttrigger-holded-sync, PHP prefix tocthls_/CTHLS_. - Added it_IT and es_ES translations.
- Added cost price and barcode fields to product tab.
- Fix: nonce verification in product meta save.
- Redesigned settings page (card layout, icons).
- Added brand append option, Action Scheduler pull, configurable interval.
- Initial release.
Disclaimer: independent open source project, not affiliated with Holded Technologies S.L. — GPLv2 or later — developed by Poletto 1976 S.L.U.