# Pitchbar for WordPress — Documentation

Welcome. This document covers everything you need to install, configure, and operate the Pitchbar WordPress & WooCommerce plugin. The plugin connects your WordPress site to a Pitchbar workspace and adds a streaming AI sales chat widget, full content sync, and deep WooCommerce integration.

You need a Pitchbar workspace to use this plugin. The plugin is the bridge between WordPress and your Pitchbar agent — it does not contain the AI itself. Create a workspace in your Pitchbar deployment (or purchase the main Pitchbar SaaS item) before installing.

## Overview

Pitchbar for WordPress is a companion plugin that does six things:

1. Embeds the Pitchbar widget on every public page via `wp_footer`, scoped to the post types you opt into.
2. Syncs your content — posts, pages, custom post types, and the entire WooCommerce catalog — into the agent's knowledge base.
3. Renders page-builder pages properly (Elementor, Divi, Beaver Builder, Oxygen, Bricks) so their visible HTML actually reaches the agent.
4. Integrates with WooCommerce — logged-in shopper context, order lookup, coupon emission + apply, abandoned-cart trigger, and lead mirroring back into WP.
5. Shows sync status per post + product in the WP admin list tables with green / yellow / gray pills.
6. Authenticates securely in both directions — Bearer token outbound, HMAC-signed callbacks inbound.

The plugin's source lives in the Pitchbar monorepo at `wp-plugin/pitchbar/` and ships as a versioned zip you install from Plugins → Add New → Upload Plugin.

## Requirements

Component | Minimum | Tested through
--- | --- | ---
WordPress | 6.4 | 6.6
PHP | 7.4 | 8.4
WooCommerce | 8.0 (optional) | 9.x
Multisite | Supported per-site | —
Page builders | Elementor (Free + Pro), Divi, Beaver Builder, Oxygen, Bricks | —
RTL locales | Yes (Arabic, Hebrew, Persian, Urdu, ...) | —

The plugin core (widget embed + post sync) runs on a non-Woo site. Every WooCommerce-specific feature is wrapped behind a `class_exists('WooCommerce')` guard, so the plugin never breaks anything when WooCommerce isn't installed.

## Quick start

1. Upload `pitchbar.zip` via Plugins → Add New → Upload Plugin.
2. Activate the plugin.
3. Open Settings → Pitchbar.
4. Paste your Pitchbar workspace base URL and an API token (created at `Settings → API tokens` in your Pitchbar admin).
5. Click Test connection. The plugin lists agents in your workspace.
6. Pick an agent and Save changes.
7. Click Sync posts now (and, on Woo sites, Sync products now) so the agent has knowledge to answer from.
8. Visit any public page on the site. The widget loads in the bottom-right corner.

## Install & activate

### From CodeCanyon

1. Download `pitchbar.zip` from your CodeCanyon downloads.
2. Sign in to WordPress as a user with `manage_options` (usually administrator).
3. Go to Plugins → Add New → Upload Plugin.
4. Choose `pitchbar.zip`. Click Install Now.
5. Click Activate Plugin.

### From your Pitchbar admin (self-built)

Super-admins of a Pitchbar deployment can produce an install-ready zip from the platform admin without ssh access:

1. Sign in to your Pitchbar admin as a super_admin.
2. Open /admin/integrations/wordpress.
3. Click Build latest. The server runs `php artisan pitchbar:build-wp-plugin` against the bundled source and writes a versioned archive to `storage/app/private/wp-plugin-builds/pitchbar-{version}.zip`.
4. Click Download next to the resulting row.

Multisite: activate per site (not network-wide). Each subsite connects to its own Pitchbar workspace and can attach a different agent. Network-wide activation is not supported in v2.x.

## Connect to your workspace

After activation, you'll see a “Pitchbar is installed but not configured yet” notice in the admin. Open Settings → Pitchbar to wire up the connection.

### Create an API token in Pitchbar

1. Sign in to your Pitchbar workspace.
2. Open Settings → API tokens.
3. Click Create token. Name it after your WordPress site (e.g. `my-site.com`).
4. Select the `wp:integration` scope.
5. Copy the plaintext value shown once — it's never displayed again.

### Paste into the plugin settings

Field | Example | Notes
--- | --- | ---
Pitchbar base URL | `https://app.pitchbar.example` | No trailing slash. Just the workspace host.
API token | `pbar_…` | The plaintext you copied above. Stored in `wp_options`.

Click Test connection. On success the panel shows the workspace name and populates the Attached agent dropdown with every agent in that workspace. Pick one. Save changes.

### Diagnostics on failure

Test connection surfaces detailed errors instead of a generic “Connection failed”:

- HTTP status (401, 403, 404, 5xx) mapped to plain English advice.
- The URL the plugin tried — copy/paste into a browser to verify reachability.
- Transport code on network failure (cURL error code, DNS resolution failure, SSL handshake error).
- First 800 chars of the upstream response body — useful when a reverse proxy returns an HTML error page instead of JSON.

Click Show details to reveal the full diagnostic block. Common status hints:

Status | Likely cause | Fix
--- | --- | ---
401 | API token invalid or revoked | Reissue in Pitchbar → Settings → API tokens
403 | Token missing `wp:integration` ability | Recreate the token with that scope
404 | Pitchbar app not fully deployed | Verify the base URL points at the Pitchbar host, not a marketing redirect
419 | CSRF mismatch | Base URL should point at the API host, not a redirect chain
429 | Token rate-limited | Wait a minute and retry
5xx | Pitchbar server error | Tail `storage/logs/laravel.log` on the Pitchbar host

### Widget display options

Setting | Default | Effect
--- | --- | ---
Enabled | On | Injects the widget on the front end. Uncheck to disable without disconnecting.
Post types | Posts, Pages | Tick more to load the widget on custom post types' singular pages.

## Content sync — posts & pages

Knowledge sync is what makes the agent useful: the AI can only answer from content it has been given. The plugin pushes posts, pages, and the custom post types you enabled to your Pitchbar agent so the chat can answer from your actual site copy.

### Bulk sync

On the Settings page (when configured), the Knowledge sync panel exposes a Sync posts now button. Click it once after the initial connect. The plugin paginates `WP_Query` in batches of 50, posts each batch to `POST /api/v1/wp/posts/sync` on Pitchbar, and stamps a sync timestamp + content hash on every successfully-synced row.

Idempotent. Pitchbar's server-side syncer short-circuits unchanged posts via the content hash. Re-running “Sync now” back-to-back is safe and only sends what's actually changed.

### Delta hooks (automatic)

Once configured, the plugin keeps the agent up to date automatically:

- `save_post` (priority 20) — pushes the post to `POST /api/v1/wp/posts/changed` with `action: "upsert"`. Drafts and revisions are skipped.
- `wp_trash_post` / `before_delete_post` — push `action: "delete"` so the agent removes the entry from its knowledge base.

### Resumable on shared hosting

Many shared hosts cap `max_execution_time` at 30 seconds. A site with thousands of posts can't finish in one pass — the plugin handles this gracefully:

1. Hard 20-second budget per pass.
2. If the budget is exceeded with pages still queued, the plugin persists a resume marker in a transient (`pitchbar_post_sync_resume`, 1 hour TTL).
3. It schedules a single WP-Cron event 30 seconds out (`pitchbar_run_full_sync_event`) which resumes from the same page.
4. A soft info notice appears on the Plugins screen while the background tick is finishing.
5. Re-running “Sync now” manually resumes from the same point — every batch is idempotent on `external_id`.

### Empty-body fallback

Some posts have empty `post_content` — featured-image-only posts, builder-owned stubs, etc. The plugin synthesises a body from `title + excerpt + taxonomy terms` before sending so the agent always has indexable text to work with.

## Page builders

Most page builders stash the actual layout in postmeta and leave `post_content` either empty or as a shortcode stub. A naive content sync would skip every page on a builder site. The plugin invokes each builder's native renderer to produce the visible HTML before sending.

Builder | Detection | Renderer
--- | --- | ---
Elementor (Free + Pro) | `_elementor_edit_mode` postmeta | `\Elementor\Plugin::frontend()->get_builder_content_for_display()`
Divi | `_et_pb_use_builder` postmeta | `setup_postdata()` + `the_content` filter
Beaver Builder | `_fl_builder_enabled` postmeta | `FLBuilder::render_content_by_id()`
Oxygen | `ct_builder_shortcodes` postmeta | `ct_template_output` filter
Bricks | `_bricks_page_content_2` postmeta | `Bricks\Frontend::render_content()`

Detection is first-match-wins based on each builder's own postmeta keys, so a site that switched builders mid-project never double-renders. If none match, the plugin falls back to the standard `the_content` filter pipeline.

### Override the rendered HTML

The `pitchbar_post_content_html` filter lets you post-process the rendered HTML before it's sent to Pitchbar — strip navigation menus, force a custom Elementor template, redact a block, whatever you need:

    add_filter('pitchbar_post_content_html', function ($html, $post) {
        // Strip everything between <nav>...</nav> tags
        $html = preg_replace('#<nav\b[^>]*>.*?</nav>#is', '', $html);
        return $html;
    }, 10, 2);

## WooCommerce products

When WooCommerce is active, a second panel appears on the Settings page: WooCommerce products. Click Sync products now to push the catalog. Same architecture as posts — batched, resumable, idempotent on `external_id`.

### Every product type is included

The product syncer does NOT filter by type — simple, variable, grouped, external, plus subscription, bundle, membership, booking, and any custom product type extensions register. A type filter was the cause of a real symptom on subscription / multilingual stores where “Sync now” returned `0 products, 0 queued` against a catalog of 10 products.

### Fallback WP_Query

Some hosts and extensions hook `wc_get_products` in ways that hide the entire catalog (subscription gates, multilingual scope filters, custom-types-of-the-month). When the first call returns 0 products and we're on page 1, the plugin retries with a raw `WP_Query` against `post_type=product`. A `Logger::warn` records this so admins can debug.

### What gets synced per product

Field | Source
--- | ---
SKU | `get_sku()`
Name | `get_name()`
Permalink | `get_permalink()`
Image URL | First gallery image (raw URL, so the LLM can emit a product card with a real thumbnail)
Short + long description | `get_short_description()` + `get_description()`
Price + sale price + currency | `get_price()`, `get_sale_price()`, `get_woocommerce_currency()`
Stock status | `get_stock_status()` (in stock / out of stock / on backorder)
On sale | `is_on_sale()`
Categories | Product category term names
Attributes | All product attribute slugs + values

### Coupon snapshot rides along

After every successful full product sync (not on resumed-out partial runs), the plugin calls `CouponSyncer->run()` which posts the store's currently-valid `shop_coupon` CPT entries to `POST /api/v1/wp/coupons/sync`. Expired and exhausted codes are filtered out. The agent's system-prompt fragment lists these so it never invents codes that don't exist.

## “Indexed” admin badges

Every successful sync stamps two values on each post / product: `_pitchbar_synced_at` (timestamp) and `_pitchbar_synced_hash` (content hash sent to the server). A new Pitchbar column on the Posts, Pages, and Products admin list tables turns those into a visible pill per row:

Pill | Condition
--- | ---
Indexed | The post has a sync timestamp AND `post_modified_gmt` has NOT advanced past it.
Out of date | The post has a sync timestamp but has been edited since (post_modified_gmt > synced_at).
Not indexed | No `_pitchbar_synced_at` meta — never synced.

The column has a forced 110px width via `admin_head` CSS so the header text never wraps to one letter per line on crowded WooCommerce screens.

## WooCommerce — logged-in shopper context

When a logged-in WooCommerce customer browses the site, the plugin issues a short-lived signed token attached to the widget loader as `data-shopper-token`. The Pitchbar server verifies the signature on `/widget/init` and lifts the claims into the issued widget JWT so the chat session is bound to the visitor.

### Claim shape

    {
        "wp_user_id": "42",
        "email_hash": "sha256_hex_of_lowercased_trimmed_email",
        "source": "wordpress"
    }

The plaintext email is never sent. Only the SHA-256 hash rides in the claims, used server-side for dedupe and correlation. The signature is HMAC-SHA256 with the workspace's `shopper_signing_secret` (received in the handshake response and stored in `wp_options`).

### Wire format

The token is delivered as `base64url(claims_json).hmac_sha256_hex`. The Pitchbar server mirrors the verification path in `App\Services\Widget\ShopperToken`.

## WooCommerce — order lookup tool

The agent can answer “Where's my order?” with real order data when the visitor is signed in. Enable the `lookup_order` tool on the agent in the Pitchbar admin; the LLM emits a tool-call which the Pitchbar server routes back to the plugin's REST endpoint.

POST /wp-json/pitchbar/v1/orders/lookup

Returns the last N WooCommerce orders for the signed-in `wp_user_id`.

### Request

Header / Field | Value
--- | ---
`X-Pitchbar-Signature` | `t={unix_ts},v1={hmac_sha256_hex}` using `shopper_signing_secret`
`wp_user_id` (required) | Integer. Lifted from the signed shopper token on the chat side.
`limit` (optional) | 1–10, default 5
`order_number` (optional) | Filter to a specific order number

### Response

    {
        "data": {
            "count": 2,
            "orders": [
                {
                    "id": 1234,
                    "number": "1234",
                    "status": "processing",
                    "total": "49.99",
                    "currency": "USD",
                    "date_created": "2026-05-12T18:42:00+00:00",
                    "items": [
                        { "name": "Aurora Hoodie", "qty": 1, "sku": "AH-001", "total": "49.99" }
                    ],
                    "tracking_url": "https://aftership.com/track/...",
                    "order_url": "https://shop.example.com/my-account/view-order/1234/"
                }
            ]
        }
    }

The tracking URL is resolved from common postmeta keys — `_aftership_tracking_url`, `_tracking_url`, `_st_tracking_link` — in that order. The first non-empty value wins.

## WooCommerce — coupons

Pitchbar handles coupons in two halves: emit + apply.

### Emit

The plugin's `CouponSyncer` pushes the store's currently-valid coupons (up to 50, expired and exhausted dropped) to Pitchbar after every product sync. The agent's prompt fragment lists them so the LLM only ever offers codes that actually work.

When the agent decides to offer a coupon, it emits a `<coupon/>` block in chat. The widget renders a card with the code, a human-readable label (“15% off”, “$10 off your order”), and two buttons: Copy and Apply.

### Apply

The Apply button hits the plugin's REST endpoint:

POST /wp-json/pitchbar/v1/cart/coupon

Validates the code with `WC_Coupon`; on success stages it in a 15-minute transient keyed by conversation id.

The plugin can't call `$cart->apply_coupon()` directly from a REST context because WC's cart machinery expects the cart-page session. Instead the plugin:

1. Validates the code with `new WC_Coupon($code)` — invalid / expired codes return 400 immediately.
2. Stages the code in a 15-minute transient `pitchbar_pending_coupon_{conversation_id}`.
3. Sets a first-party `pitchbar_conv_id` cookie on the visitor's domain so the plugin can correlate the next page load.
4. On the visitor's next cart load (`woocommerce_load_cart_from_session`), the plugin reads the cookie, looks up the staged code, and fires `WC()->cart->apply_coupon($code)`. The transient is deleted after apply.

Why a transient? A cart-load hook is the only place WC's session, the cart object, and the visitor's identity line up. Staging the code and applying on the next cart load is how the visitor sees the discount actually take effect on their next checkout step.

## WooCommerce — abandoned cart

The plugin enqueues a tiny front-end script (`assets/cart-state.js`) that mirrors WooCommerce's jQuery cart events into `localStorage` under the key `pitchbar_cart_state`:

    {
        "items": 2,
        "timestamp": 1731600000000
    }

- `added_to_cart` increments items + bumps timestamp.
- `removed_from_cart` decrements items.
- Any visit to a checkout / order-received / order-pay / thank-you path clears the state entirely.
- Fallback DOM click handler on `.add_to_cart_button` + `.single_add_to_cart_button` for sites with no jQuery on the front end.

The widget reads this state on every page load. When a cart sits idle longer than the threshold configured in the agent's behavior rules (admin UI exposes `abandoned_cart` as a trigger kind), the widget engages proactively with the configured message.

## WooCommerce — lead mirroring

When the chat captures a lead (visitor fills the in-bar form), the Pitchbar server calls back to the plugin so the contact lands in WordPress without you leaving the admin.

POST /wp-json/pitchbar/v1/leads

Creates or updates a WP user mirroring the captured lead. Idempotent on email.

### Behaviour

- If WooCommerce is active, the plugin calls `wc_create_new_customer($email, '', '', ['first_name' => $name])` so the lead is a real WC customer immediately.
- If WooCommerce is absent, the plugin creates a regular WP user with role `subscriber` via `wp_create_user`.
- Existing users (matched by email) are not duplicated — only their meta is refreshed.
- `pitchbar_lead_id` and `pitchbar_conversation_id` are stamped in user meta for correlation.
- Phone (when provided) lands in `billing_phone` user meta.

## REST API reference

Two surfaces — Plugin → Pitchbar (outbound) and Pitchbar → Plugin (inbound). Each endpoint is authenticated separately.

### Plugin → Pitchbar (outbound)

All outbound calls carry `Authorization: Bearer {api_token}` plus an HMAC `X-Pitchbar-Signature` header (using the same token as the shared secret). The signature payload is `{unix_ts}.{body}`; the header format is `t={ts},v1={hmac_sha256_hex}`. 5-minute timestamp window.

POST/api/v1/wp/handshake

Validates the API token, returns the workspace name, the agents in the workspace, the recommended `site_type`, and the `shopper_signing_secret` for callback auth.

POST/api/v1/wp/posts/sync

Bulk push of up to 50 posts per batch. Server returns counts of queued / skipped-unchanged rows.

POST/api/v1/wp/posts/changed

Single-post delta — `action: "upsert" | "delete"`. Fires from `save_post` / trash hooks.

POST/api/v1/wp/products/sync

Bulk push of up to 50 WooCommerce products per batch.

POST/api/v1/wp/products/changed

Single-product delta from WC's `woocommerce_new_product` / `update_product` / `delete_product` / `trash_product` hooks.

POST/api/v1/wp/coupons/sync

Snapshot of currently-valid `shop_coupon` entries (up to 50). Posted automatically after every successful full product sync.

### Pitchbar → Plugin (inbound)

Every inbound endpoint verifies an `X-Pitchbar-Signature` header against the locally-stored `shopper_signing_secret`. A missing or mismatched signature returns 401 with a clear error code (`missing_signature`, `plugin_unconfigured`, or `signature_mismatch`).

POST/wp-json/pitchbar/v1/leads

Creates / updates a WP user from a captured lead. Idempotent on email.

POST/wp-json/pitchbar/v1/orders/lookup

Returns the last N WooCommerce orders for `wp_user_id` with status, items, tracking URL, and view-order link.

POST/wp-json/pitchbar/v1/cart/coupon

Validates a coupon code with `WC_Coupon`; on success stages it in a 15-minute transient that applies on the visitor's next cart load.

## Security model

The plugin and the Pitchbar server authenticate each other with two distinct credentials, in opposite directions:

Direction | Credential | Notes
--- | --- | ---
Plugin → Pitchbar | Bearer API token + HMAC body signature | Token created in `Settings → API tokens`, scope `wp:integration`. Pitchbar stores only the SHA-256 hash of the plaintext. The plugin keeps the plaintext in `wp_options` — treat as a `wp-config.php`-level secret.
Pitchbar → Plugin | HMAC-SHA256 signature using `shopper_signing_secret` | The plugin receives this per-token secret in the handshake response and stores it locally. Pitchbar uses it to sign every callback (orders, coupons, leads, shopper token) so the WordPress REST endpoints verify the caller without ever holding the bearer plaintext.

### Replay window

Both directions enforce a 5-minute timestamp window. Signatures older than 300 seconds are rejected. The window is short enough to stop replay attacks and wide enough to absorb clock drift between hosts.

### Data minimization

- The shopper token carries `wp_user_id` + the SHA-256 hash of the email — never the plaintext email.
- The page-context payload carries only public taxonomies, post type, permalink, and Woo public product fields.
- The plugin never sends customer order data to the AI provider — order lookup is on-demand only, server-to-server.

### WordPress capability gates

- The Settings page and all AJAX endpoints require `manage_options`.
- Every AJAX POST verifies a WordPress nonce (`pitchbar_ajax_action`).
- REST endpoints use `permission_callback => '__return_true'` because the auth is HMAC, not WP nonce — the signature verification happens inside the controller.

## Filters & hooks reference

Hook | Type | Purpose
--- | --- | ---
`pitchbar_post_content_html` | filter | Post-process the rendered HTML before sync. Receives `($html, $post)`. Use to strip nav, redact blocks, force a builder template, etc.
`pitchbar_run_full_sync_event` | action | WP-Cron continuation for resumable post sync. Fires automatically; you don't call this directly.
`pitchbar_run_product_sync_event` | action | WP-Cron continuation for resumable product sync.

## Troubleshooting

### The widget doesn't appear on my site

1. Open Settings → Pitchbar. Confirm Enabled is ticked.
2. Click Test connection. If the agent dropdown is empty, the workspace has no agents — create one in your Pitchbar admin first.
3. Confirm the current post type is in Post types. The widget is suppressed on singular pages of post types you haven't ticked.
4. Disable any aggressive caching plugin temporarily (WP Rocket, W3 Total Cache) and reload. Cached HTML may not contain the `<script>` tag yet.
5. View page source. Search for `pitchbar` — if the `<script async>` tag is present but the widget isn't, the issue is on the Pitchbar server side (open the network tab, look for the widget.js fetch).

### “Connection failed” on Test connection

Click Show details to reveal the diagnostic block. See the status-code table in Connect to your workspace. Most common cause is a stale or wrong-scope API token (401 / 403) — reissue with the `wp:integration` scope.

### “0 products, 0 queued” on product sync

A multilingual scope filter or subscription gate may be hiding products from `wc_get_products`. The plugin includes a fallback `WP_Query` path that fires automatically on page 1; check `wp-content/debug.log` for the warning `ProductSyncer used fallback WP_Query`. If the fallback also returns 0, the store has no published products.

### Sync stuck mid-run

The plugin shows a “is finishing in the background” notice while a chunked sync is mid-run — this is expected on large sites. The WP-Cron continuation runs every 30 seconds until the catalog is fully synced. If the notice persists for more than 10 minutes:

1. Confirm WP-Cron is firing on your host. If you've disabled it (`DISABLE_WP_CRON` in `wp-config.php`), you must set up a real cron hitting `wp-cron.php`.
2. Run `wp transient delete pitchbar_post_sync_resume` (or the product variant) to clear the resume marker.
3. Re-run “Sync now” manually.

### HMAC signature mismatch

Inbound callbacks (orders / leads / coupons) check the signature against the local `shopper_signing_secret`. If this is out of sync (e.g. you regenerated the API token in Pitchbar without re-running Test connection), the plugin returns 401 with code `signature_mismatch`. Re-run Test connection from the Settings page — the new secret is delivered in the response and stored locally.

### Widget mirrors the wrong direction on RTL

The plugin passes `data-page-dir="rtl"` when `is_rtl()` returns true. If you've set the WordPress locale to an RTL language but the theme overrides this with its own LTR rules, you may need to force RTL in WordPress general settings.

### Posts with empty content fail with “content_html field is required”

Solved as of v2.0.3 — the plugin synthesises a body from `title + excerpt + taxonomy terms` when `the_content` collapses to empty. If you're seeing this on an older plugin version, upgrade to 2.0.3 or later.

### WooCommerce column header renders one letter per line

Solved as of v2.0.4 — the Pitchbar admin column has a forced 110px width via `admin_head` CSS. Hard refresh the Products list page if you're still seeing the old behaviour.

### Where do I see plugin logs?

Enable `WP_DEBUG` + `WP_DEBUG_LOG` in `wp-config.php`:

    define('WP_DEBUG', true);
    define('WP_DEBUG_LOG', true);
    define('WP_DEBUG_DISPLAY', false);

Plugin warnings and informational messages land in `wp-content/debug.log`. Tail it after running a sync to see the per-batch diagnostics.

## Changelog

### 2.0.4

- Pitchbar admin column width. On the WooCommerce Products list (and other crowded list tables) the “Pitchbar” column was rendering one letter per line because WP's auto-width algorithm starved it. Forced to 110px via an `admin_head` style block.

### 2.0.3

- “Indexed” badge in the Posts, Pages, and Products admin lists. The plugin stamps a sync timestamp and content-hash on every post / product it ships to Pitchbar. The admin list table now renders a green “Indexed” pill, a yellow “Out of date” pill (post edited after the last sync), or a gray “Not indexed” pill for entries that have never been synced.
- Empty-body posts no longer require a server-side validation deploy. If `the_content` collapses to an empty string, the plugin synthesises a body from `title + excerpt + taxonomy terms` before sending.

### 2.0.2

- Posts with empty content sync cleanly. Pre-fix a batch died with `posts.N.content_html field is required` the first time any post had an empty body.
- WooCommerce: every product type is synced. Pre-fix the syncer filtered to `[simple, variable, grouped, external]` which silently dropped subscriptions, bundles, memberships, bookings, and custom types. Type filter removed; fallback `WP_Query` added for hosts whose `wc_get_products` hook chain hides everything.

### 2.0.1

- Test connection now reports exactly what broke. The generic “Connection failed” message is gone. The plugin surfaces HTTP status, the URL it tried, the transport-level cURL / DNS code on a network failure, and the first 800 chars of the upstream response body. A new “Show details” toggle reveals the full diagnostic block.
- HTTP-status-aware advice. 401 hints to reissue the API token, 403 hints at the missing `wp:integration` ability, 404 hints that the Pitchbar app isn't fully deployed, 5xx hints at the server log.
- JS catch block now prints the underlying error message instead of swallowing it.

### 2.0.0

- Page builder support. Elementor, Beaver Builder, Oxygen, and Bricks pages now sync their fully-rendered HTML. Divi posts route through `the_content` correctly now that `setup_postdata` primes the global `$post` before the filter chain runs.
- WooCommerce load-order race fixed. The plugin no longer trusts `class_exists('WooCommerce')` at `plugins_loaded` priority 10. It listens to the `woocommerce_loaded` action and re-checks if WC already fired, so alphabetical plugin-load order no longer silently disables product sync, coupon sync, and the cart-coupon REST endpoint.
- Resumable sync. Both post and product sync now enforce a 20-second time budget per pass. When a large site can't finish in one tick, the plugin persists a resume marker and schedules a WP-Cron continuation 30 seconds out.
- Coupon enumeration via `shop_coupon` CPT. Replaces `wc_get_coupons()` (not public WC API on every release).
- `abandoned_cart` admin trigger. The widget evaluator already knew how to engage on cart staleness; the plugin admin UI now exposes `abandoned_cart` in the trigger-kind dropdown.
- RTL widget mirroring. Embed loader now emits `data-page-dir` and `data-page-locale` so the widget mirrors correctly on Arabic, Hebrew, Persian, and Urdu sites.
- Background-sync admin notice. While a chunked sync is mid-run the Plugins admin shows a soft info notice.
- `pitchbar_post_content_html` filter added so themes / sites can post-process the synced HTML without forking the plugin.

### 1.2.0

- Mirrors captured Pitchbar leads back into WordPress as WooCommerce customers (or WP subscribers on non-Woo sites).
- `<coupon/>` chat blocks with Copy + Apply. The Apply button stages the code on the WP cart so it kicks in on the next cart load.
- CouponSyncer pushes the store's currently-valid WC coupons to Pitchbar after every product sync.
- `abandoned_cart` widget trigger kind. Cart-state script mirrors WC events into `localStorage`; the widget engages when the cart sits idle.
- `/wp-json/pitchbar/v1/leads` + `/wp-json/pitchbar/v1/cart/coupon` REST endpoints.

### 1.1.0

- Logged-in shopper context. The widget surfaces the visitor's identity to the agent (signed token, never raw email).
- `lookup_order` tool. The agent can fetch the visitor's recent orders, status, items, and tracking via a new plugin REST endpoint.
- Test connection picks up the per-token shopper signing secret automatically.

### 1.0.1

- WordPress post / page sync as a Pitchbar knowledge source.
- WooCommerce product sync with automatic ecommerce vertical switch and product card emission in chat replies.
- Sync Now buttons added under Settings → Pitchbar with delta hooks on `save_post` / `woocommerce_update_product` / delete events.

### 1.0.0

- Initial release. Widget embed + connection settings.

Pitchbar for WordPress — built by the team behind Pitchbar.
GPLv2 or later. Documentation last updated for plugin v2.0.4.