# Wordpress and WooCommerce - Developer Guide > 1. Plugin purpose and data ownershipOrderLemon is a sales channel, WooCommerce is the system of record DataDirectionBehaviorCategoriesWooCommerce -> OrderLemonSynchronized before productsProducts and… - Page: https://cloud.orderlemon.com/knowledge_base/view/261-wordpress-and-woocommerce-developer-guide - Part of: [Knowledge base](https://cloud.orderlemon.com/knowledge_base/llms.txt) on OrderLemon Cloud — https://cloud.orderlemon.com/llms.txt - Updated: 2026-10-26T11:01:20Z ### 1. Plugin purpose and data ownership OrderLemon is a sales channel, WooCommerce is the system of record | Data | Direction | Behavior | | --- | --- | --- | | Categories | WooCommerce -> OrderLemon | Synchronized before products | | Products and variations | WooCommerce -> OrderLemon | One-way only. OrderLemon never sends product updates back to WooCommerce | | Orders | OrderLemon -> WooCommerce | Imported by webhook or hourly pull | | Order Status | Bidirectional | Status changes use webhooks, loop prevention, and retry handling | | Customers | OrderLemon -> WooCommerce | Found or created during order import | **Business rules:** - WooCommerce stock is reduced only when an OrderLemon order is imported - Products are never hard-deleted in OrderLemon, they are disabled with enabled = 0 - OrderLemon requires a category before a product can be created - A product synchronized by this plugin must have exactly one WooCommerce product category - There is no product reverse-sync and no product.stock_updated webhook flow ### 2. Requirements and Local Setup (only Dev environment) **Runtime requirements:** - PHP 8.3+ - Wordpress 6.9+ - WooCommerce 10.4.3+ - WooCommerce HPOS enabled The plugin declares HPOS compatibility and remains inactive when HPOS is disabled. Activation checks WordPress, WooCommerce, and WooCommerce versions. Existing stores with legacy orders are sent to WooCommerce's Features screen for migration. HPOS must be enabled. The plugin remains inactive and shows an admin notice when HPOS is disabled. WooCommerce deactivation is handled by WordPress Core through Requires Plugins. **Local setup:** ``` scripts/setup.sh localdocker compose up -d ``` **Local URLs:** - Store: http://localhost:8000 - Admin: http://localhost:8000/wp-admin - OrderLemon settings: http://localhost:8000/wp-admin/admin.php?page=orderlemon-settings - phpMyAdmin: http://localhost:8081 **Default local credentials:** - WordPress admin: admin`/ admin123 - phpMyAdmin: wordpress / wordpress These credentials are for local development only. The local Docker environment is not connected to OrderLemon, so it cannot verify live API calls, webhook delivery, archive transitions, or bidirectional end-to-end behavior ### 3. Architecture **Main entry point:** - `orderlemon-woocommerce/orderlemon-woocommerce.php` | Component | Path | Responsibility | | --- | --- | --- | | Bootstrap | `orderlemon-woocommerce.php` | Requirements, HPOS, activation, tables, initialization, and deactivation | | Config | `src/Core/Config.php` | Environment URL, connection state, options, and config-file override | | API client | `src/Core/ApiClient.php` | Native OrderLemon API communication, response validation, and request retries | | Constants | `src/Core/Constants.php` | Shared HTTP, environment, stock, and order-status constants | | ErrorCodes | `src/Core/ErrorCodes.php` | Standardized error codes | | EntityLocator | `src/Core/EntityLocator.php` | WooCommerce order/product lookup and request-level caching | | TaxSetup | `src/Core/TaxSetup.php` | Tax helpers used by imported orders | | AtomicLock | src/Core/AtomicOptionLock.php | Database-backed mutex for imports, webhooks, and full sync | | QR auth | `src/Auth/Auth.php` | Device registration, verification, shop selection, and webhook registration | | Product sync | `src/Sync/ProductSync.php` | WooCommerce category, product, variation, modifier, and stock sync | | Order orchestrator | `src/Sync/OrderSync.php` | Delegates order import, status sync, and customer statistics | | Order import | `src/Sync/Orders/OrderImportHandler.php` | Creates WooCommerce orders from normalized OrderLemon data | | Order status sync | `src/Sync/Orders/OrderStatusSyncHandler.php` | Sends WooCommerce status changes to OrderLemon | | Customer stats | `src/Sync/Orders/OrderCustomerStatsHandler.php` | Updates customer statistics after order status changes | | Payment status | `src/Sync/Orders/PaymentStatusTrait.php` | Shared payment-status resolution | | Customer manager | `src/Sync/CustomerManager.php` | Customer lookup, creation, privacy hooks, and statistics | | Webhook endpoint | `src/Webhook/WebhookHandler.php` | REST route, authentication, validation, idempotency, and dispatch | | Order webhook handler | `src/Webhook/Handlers/OrderWebhookHandler.php` | Order create/update/status/cancel/refund handling | | Logger | `src/Core/Logger.php` | WooCommerce logs and structured sync logs with redaction | | Retry Queue | `src/Core/RetryQueue.php` | Persistent asynchronous retry jobs | | Admin | src/Admin/* and admin/views/* | Settings, logs, notices, meta boxes, status, and rate limiting | **Initialization:** - Database upgrade check: init priority 5 - Main plugin initialization: init priority 10, after WooCommerce - Admin services load only in wp-admin - HPOS compatibility is declared on before_woocommerce_init - Public-facing cart/checkout assets are not currently registered by a Frontend service ### 4. Runtime Data and Storage ##### **4.1 Environments** ###### The environment selector is available in advanced settings and defaults to production | Environment | API URL | | --- | --- | | dev | https://api.orderlemon.dev/v1/ | | test | https://test.api.orderlemon.dev/v1/ | | stage | https://stg.api.orderlemon.com/v1/ | | production | https://api.orderlemon.com/v1/ | sandbox is unsupported and resolves to production. HTTPS is required for test, stage, and production; plain HTTP is allowed only for dev. **Config precedence:** - Mutable connection state stored in WordPress takes precedence over the config file. This includes auth.token, api.token, company.id, store.id, whatsapp.number, and webhook.secret - Other values are read from the config file first and the WordPress options - Config::set() writes to Wordpress options - api.url is read from the config file only. The orderlemon_api_url WordPress is ignored - An invalid custom URL falls back to the selected environment URL ### ##### **4.2 Important options** **General settings:** - orderlemon_sync_mode - orderlemon_auto_sync_products - orderlemon_auto_sync_orders - orderlemon_debug_mode - orderlemon_category_sync_mode - orderlemon_sync_categories - orderlemon_excluded_products - orderlemon_customer_email_domain - orderlemon_log_retention_days **Connection state:** - orderlemon_auth_token - orderlemon_api_token (legacy fallback) - orderlemon_company_id - orderlemon_store_id - orderlemon_shop_name - orderlemon_webhook_secret - orderlemon_webhook_id - orderlemon_webhook_url - orderlemon_connected_at **Immediate API request retry tuning:** - orderlemon_max_retries, default 3 - orderlemon_initial_backoff, default 1 second - orderlemon_max_backoff, default 60 seconds orderlemon_excluded_products is only the settings form value. The product post meta orderlemon_sync_enabled is the source of truth for exclusion ##### **4.3 Custom database tables** ##### **Table names must use $wpdb->prefix** | Table | Purpose | | --- | --- | | {$wpdb->prefix}orderlemon_sync_log | Structured sync and webhook logs | | {$wpdb->prefix}orderlemon_retry_queue | Persistent retry jobs | The sync-log table contains id, sync_type, entity_type, entity_id, direction, status, message, request_data, response_data, environment, error_code, log_level, http_status, and created_at. The retry table contains id, action_type, action_data, attempts, max_attempts, next_retry_at, status, claim_token, dedup_hash, rerun_requested, error_message, created_at, and updated_at. Tables are created on activation and reconciled with dbDelta() when the database version changes. For orders, always use WooCommerce CRUD: wc_get_order(), wc_get_orders(), order meta methods, and WC order objects. Do not query wp_posts or wc_orders directly for orders. ##### **4.4 Important metadata** | Key | Purpose | | --- | --- | | _orderlemon_product_id | Linked OrderLemon product or modifier ID | | _orderlemon_last_sync | Last sync timestamp | | _orderlemon_sync_enabled | Per-product sync toggle | | _orderlemon_disabled_in_ol | The mapped OL row is disabled | | _orderlemon_payload_hash | Bulk-sync change-detection hash | | _orderlemon_relation_key | Confirmed parent/modifier relation | | _orderlemon_had_variations | Parent variation cleanup marker | | _orderlemon_synced_category_id | Category used by the last sync | **Category term meta:** - _orderlemon_category_id - _orderlemon_category_enabled - _orderlemon_is_modifier **Order meta:** - _orderlemon_order_id - _orderlemon_logistics_type - _orderlemon_logistics_label - _orderlemon_pickup_moment - _orderlemon_delivery_moment - _orderlemon_payment_type **User meta:** - _orderlemon_bsuid - _orderlemon_phone_normalized - _orderlemon_customer_id - _orderlemon_whatsapp_customer ### 5. Authentication And Connection ##### The plugin uses QR/device authentication rather than manual token entry ###### 1. generate_qr_code() sends a login newdevice request with Authorization: Bearer newdevice. 2. The pending device UUID is stored server-side. 3. The administrator confirms the QR session in WhatsApp. 4. verify_device() polls with the device UUID and the login verify action. 5. On success, the device UUID becomes the API bearer token and company ID is stored. 6. Selecting a shop stores store.id and the shop name. 7. Shop selection registers or re-enables the OrderLemon webhook. The plugin can be authenticated but not usable until a shop is selected, because native shop-scoped API requests require both company ID and shop ID. Webhook registration reuses an existing webhook when possible. It verifies a stored webhook ID, can adopt a webhook with the exact current URL if the local ID was lost, and creates a new webhook only when no matching webhook exists. The registered events are payments.completed and order.updated. Disconnect disables the remote webhook when possible, clears authentication/company/shop/session state, and preserves the stored webhook ID for reconnect recovery. A failed remote disable produces an admin warning. ### 6. OrderLemon API protocol ###### Native API calls use one POST request to the environment base URL: ```     Module: products    Authorization: Bearer     Content-Type: application/json    {"a":"update","data":{"company_id":1,"shop_id":2,"product_id":123}} ``` The Module value identifies the domain, for example products, categories, orders, company, login, or webhook. The action is in the a field. ApiClient injects company_id and shop_id when absent and rejects requests when either is missing. Important product actions: - products:list — reference-based recovery and duplicate handling. - products:new — create; category_id is required. - products:update — update using product_id. - products:available_modifiers — modifier lookup. Important order actions: - orders:list — hourly order pull. - orders:update — outbound status update. ApiClient validates JSON, uses SSL verification, handles legal-signature HTTP 420, retries rate limits and retryable 5xx responses, and returns WP_Error for business or transport failures. ### 7. Product and category synchronization ##### **7.1 Triggers and gates** **ProductSync listens to:** - woocommerce_new_product - woocommerce_update_product - woocommerce_product_set_stock - woocommerce_variation_set_stock - woocommerce_variation_set_stock_status - woocommerce_new_product_variation - woocommerce_update_product_variation - woocommerce_before_delete_product_variation - before_delete_post - wp_trash_post - transition_post_status for future → publish The orderlemon_auto_sync_products option is checked first. When it is disabled, product save, variation, stock, and deletion hooks do not send anything to OrderLemon. Save hooks are coalesced during the request and enqueued at shutdown. API work runs in the persistent retry queue rather than delaying the product-edit request. ##### **7.2 Eligibility** **A product is eligible only when:** - it loads as WC_Product; - its status is publish; - it is not password protected; - _orderlemon_sync_enabled is not no; - its product type is allowed by orderlemon_allowed_product_types (default: simple and variable); - it passes the category sync mode. **Category modes:** - all — all categories are allowed. - include — only selected categories are allowed. - exclude — selected categories are excluded. The full-sync report separates transient skipped results from excluded_product, excluded_category, unsupported_type, generic excluded, disabled, success, and failed results. ##### **7.3 Categories** **The plugin resolves the first WooCommerce product category before sending a product:** 1. use _orderlemon_category_id when valid; 2. refresh the OrderLemon category list; 3. match by name where possible; 4. create the category when no match exists. Category names are truncated to 24 characters. Category enabled state is recalculated from whether an eligible WooCommerce product still uses the category. Modifier categories are shared by attribute name. Do not disable a shared modifier category while another eligible parent still uses it. ##### **7.4 Product payload** ##### | WooCommerce | OrderLemon | | --- | --- | | SKU, or WC-{id} fallback | reference | | Product ID | metadata.woocommerce.product_id | | Category ID | metadata.woocommerce.category_id | | Name | name | | Description | description | | Featured image | picture_base64 | | Regular price | unit_price | | Sale price or null | sale_price | | Sale dates | sale_price_start, sale_price_end | | WC tax rate | vat_percentage | | Published state | enabled | | First category | category_id and category_name | | Stock quantity | stock | | Manage stock | isstock | | Menu order | position | | Featured flag | isfav | ###### The reference is limited to 30 characters and passes through orderlemon_product_reference. This filter must be used consistently by sync, disable, duplicate, recovery, and incoming cart-resolution logic. Product images are limited to 20 MiB before Base64 encoding. Oversized local or remote images are skipped with a warning, and product synchronization continues without the image. ###### When _orderlemon_product_id is missing, ProductSync uses products:list to recover a product by reference before creating a new row. After a successful sync, other enabled OL rows with the same reference are disabled. Full sync additionally reports enabled duplicate groups by case-insensitive name and category for manual review. Modifier/variation products are excluded from that name/category scan. ##### ##### **7.5 Variable products, modifiers, and stock** ##### **Variable products are synchronized as modifier products in OrderLemon:** Variable products are represented by an OL parent product and modifier products for variations. ProductSync creates modifier categories, variation products, parent/modifier relations, and disables stale modifiers. Variation metadata contains WooCommerce variation and parent IDs so incoming cart items can be resolved. Stock changes normally use a stock-only products:update containing stock and isstock. A full sync is used when the mapping is unknown, the parent is disabled, or parent availability must be recomputed. A variation modifier may receive enabled from its stock state; a base product's enabled state is not derived from stock in a stock-only update. Products and modifiers are disabled, not deleted, when they are trashed, deleted, become ineligible, or disappear from a clean full-sync source. ##### **7.6 Full sync** The admin Sync All Products flow first checks for missing SKUs and then starts a background run. State is stored in orderlemon_full_sync_state. The background process uses: - one-minute start cooldown; - start and batch AtomicOptionLock locks; - 30-minute stale-run takeover protection; - a stable published-product count and maximum ID snapshot; - ascending keyset pages of 40 product IDs; - durable page checkpointing; - a 20-second batch budget, or 300 seconds when set_time_limit(0) succeeds; - up to three exception attempts per product; - run IDs and compare-and-swap state checkpoints; - stale-product disabling only after a clean product phase. Products published after the snapshot are processed by a later run. Bulk payload hashes are an optimization only; they do not detect manual changes made directly in OrderLemon. A product Update or orderlemon_full_sync_force_resend can be used to repair remote drift. ### 8. Webhooks and Order Import ##### **8.1 Endpoint and registered events** **Endpoint:** ``` POST /wp-json/orderlemon/v1/webhook ``` The URL is generated with rest_url(). Plain permalinks can work through the rest_route form. If the site URL or permalink structure changes after connection, the remote registered URL may become invalid; the plugin stores the registered URL and warns about mismatches.**Only these events are registered with OrderLemon:**- payments.completed - order.updated**The handler retains compatibility dispatch for:**- order.created - order.status_changed - order.cancelled - order.refunded**Compatibility routes must not be mistaken for the current remote event subscription. Accepted payload shapes:**- Flat: {"event":"order.created","payload":{...}} - Native: {"orderlemon":{...},"event":{"type":"payments.completed","id":"..."}} ##### **8.2 Webhook Security** ##### **The endpoint requires Authorization: Bearer and compares the token with hash_equals(). It also:** - rejects payloads over 1 MiB; - validates JSON before processing; - rate-limits failed attempts by REMOTE_ADDR; - supports admin whitelist and block controls; - fails closed when the secret is missing; - logs only whitelisted identifiers and counts. For native events with an event ID, the deduplication key is scoped by company ID, shop ID, event type, and event ID. A database-backed lock serializes concurrent delivery. A concurrent request receives HTTP 503 and Retry-After: 5. The one-day completion marker is written only after successful processing, and duplicates return HTTP 200. Order creation has a separate lock by OrderLemon order ID. Refunds use an order-level lock and a durable external refund ID stored as _orderlemon_refund_id before the refund's first save. ##### **8.3 Order import** **OrderImportHandler::create_from_orderlemon() normalizes the payload and:** 1. checks for an existing _orderlemon_order_id; 2. acquires and rechecks an import lock; 3. creates an HPOS-compatible order with created_via=orderlemon; 4. finds or creates the customer; 5. adds product, modifier, shipping, fee, and discount items; 6. applies payment, status, logistics, attribution, and dates; 7. preserves OrderLemon custom prices and VAT totals; 8. saves the order; 9. reduces stock unless the status is cancelled or failed; 10. stores the entity mapping and logs success. The hourly orderlemon_pull_orders job uses the same import handler and advances orderlemon_last_order_pull only when the pull has no failures. **Product resolution priority:** 1. metadata.woocommerce.product_id; 2. product_reference matched to WC SKU; 3. WC-{id} fallback when the current product has no own SKU; 4. explicit wc_product_id. Unresolved products are retained as OL-only line items rather than silently discarded. Cart item classification supports regular products, modifiers, campaign products, positive explicit fee lines, and negative discount lines. A fee requires campaign_id to be explicitly present with value 0, no meaningful parent_id, and a positive amount. ##### ##### **8.4 Customer resolution** **Customer lookup priority:** 1. BSUID from user_id; 2. normalized phone; 3. email. BSUID is required for WhatsApp users who adopt usernames and may have an empty phone field. When a phone match receives a new non-conflicting BSUID, the existing customer is linked to it. **New or updated customers use metadata such as:** - _orderlemon_bsuid - _orderlemon_p (…cut; the full text is on the page.)