---
version: v2.0.0
title: WordPress & WooCommerce plugin, BYOK, free file parsing, hardened security
released_at: 2026-05-21
---

## New: official WordPress & WooCommerce plugin

A first-party WordPress + WooCommerce plugin now ships with every
Pitchbar deployment. Drop it on any WordPress 6.4+ install running
PHP 7.4 or newer, paste a workspace API token, and the streaming
chat widget loads on every public page — with content sync, product
cards, coupon application, order lookup, and lead mirroring out of
the box.

**Widget embed.** One asynchronous `<script>` tag injected into
`wp_footer`, scoped to the post types you opt into. Hidden on
`wp-admin`, login, AJAX, REST, XML-RPC, and cron paths. Emits
`data-page-dir` and `data-page-locale` attributes so the bar
mirrors correctly on Arabic, Hebrew, Persian, and Urdu locales.

**Content sync.** Bulk + delta ingest of posts, pages, custom post
types, and WooCommerce products as Pitchbar knowledge sources. SHA-
256 content hash short-circuits re-indexing on unchanged content,
so re-running "Sync now" against a stable site is effectively free.

**Page-builder rendering.** Elementor, Beaver Builder, Oxygen, and
Bricks pages are rendered with the builder's own native API before
sync, so the synced HTML reflects what visitors actually see. Divi
posts route through `the_content` with `setup_postdata` primed so
third-party filter callbacks see a valid `$post` global. A
`pitchbar_post_content_html` filter lets sites post-process the
final HTML (strip nav chrome, force a custom template, redact a
section) without forking the plugin.

**Resumable sync.** Each sync pass enforces a 20-second wall-clock
budget. When a large site can't finish in one tick, the plugin
persists a resume marker as a WordPress transient and schedules a
WP-Cron continuation 30 seconds out. Shared hosting with a 30s
`max_execution_time` cap no longer fatals mid-sync. The Plugins
admin shows a soft notice while a chunked sync is in flight.

**WooCommerce shopper context.** When a logged-in WC customer
visits a page, the plugin attaches a signed `data-shopper-token`
to the widget script tag. The widget forwards it to `/widget/init`;
Pitchbar verifies it and bakes `wp_user_id` + `email_hash` (never
plaintext) claims into the chat session.

**Order lookup tool.** EcommercePreset's `order_status` capability
maps to a real tool. When the visitor asks about orders, the LLM
calls `lookup_order(limit, order_number?)`, which POSTs to
`/wp-json/pitchbar/v1/orders/lookup` over HMAC-signed callback.
Returns id, number, status, total, currency, items, tracking URL
(AfterShip / ST tracking / generic meta), and the customer's order
view URL.

**Coupon emission & apply.** The plugin's `CouponSyncer` snapshots
publish-status `shop_coupon` entries after every product sync (max
50), filters expired / over-limit, and posts them to Pitchbar. The
LLM emits `<coupon code="WELCOME10" .../>` markers in chat replies;
the widget renders them as cards with Copy and Apply buttons. Apply
stages the code in a 15-minute transient that auto-applies on the
visitor's next cart load via the `woocommerce_load_cart_from_session`
hook.

**Abandoned cart re-engagement.** A new `BehaviorRule.kind =
"abandoned_cart"` lets you re-engage a visitor whose cart has been
idle. The plugin's `cart-state.js` mirrors every WooCommerce
`added_to_cart` / `removed_from_cart` event into `localStorage`; the
widget polls every 30 seconds and fires when the cart sits past
the rule's `idle_minutes` threshold.

**Lead push-back.** Every Pitchbar lead is mirrored to WordPress as
a WC customer (when Woo is active) or WP subscriber, with
`pitchbar_lead_id` + `pitchbar_conversation_id` in user meta so the
store owner can correlate. Idempotent on email — re-pushes update,
never duplicate.

**Workspace API tokens.** New `/settings/api-tokens` admin page
issues workspace-scoped bearer tokens with granular abilities (the
plugin uses `wp:integration`). Pitchbar stores only the SHA-256
hash of the plaintext; revoke instantly invalidates without
deleting audit history. Cross-workspace revoke is forbidden.

**HMAC signed callbacks.** Pitchbar → plugin calls (order lookup,
coupon apply, lead push) are signed with HMAC-SHA256 over the raw
body using a per-token `shopper_signing_secret`. 5-minute replay
window. The plugin verifies via constant-time `hash_equals`.

**Super-admin plugin distribution.** New
`/admin/integrations/wordpress` page lets super-admins build and
download the install-ready zip directly from the Pitchbar admin.
Backed by the new `php artisan pitchbar:build-wp-plugin` Artisan
command.

## Improvements

**Plugin-load-order race fixed.** v1.x of the plugin trusted
`class_exists('WooCommerce')` at `plugins_loaded` priority 10,
which silently disabled WooCommerce features when alphabetical
plugin order put `pitchbar` before `woocommerce`. v2.0.0 defers
every WC-dependent registration to the `woocommerce_loaded` action
(with a `did_action` fallback for the priority-20+ case), so the
race is eliminated.

**Coupon enumeration via `shop_coupon` CPT.** Replaces the v1.x
reliance on `wc_get_coupons()` (not public WC API on every
release) with `get_posts(['post_type' => 'shop_coupon'])` +
`new WC_Coupon($id)`. Works on every WC version since 2.0.

**`abandoned_cart` admin trigger.** Backend already supported it;
the agent Behavior triggers admin page now exposes it in the kind
dropdown.

**Comprehensive WordPress docs.** Seven new documentation pages
under the "WordPress & WooCommerce" group: Overview, Install &
connect, Content sync, Page builders, WooCommerce deep links,
REST API reference, Troubleshooting.

## Fixes

**Page-builder content reaches the RAG store.** Pre-v2.0 the
plugin called `apply_filters('the_content', $post->post_content)`
without priming `setup_postdata`, which made plenty of
`the_content` callbacks early-return on `is_null($post)`. The
Elementor / Bricks / Oxygen path was worse — those builders don't
store layout in `post_content` at all, so the synced HTML was
empty. The fix detects each builder via postmeta and calls its
native renderer.

**Sync survives shared hosting timeouts.** The 30-second PHP
execution cap on shared hosts no longer fatals mid-sync on large
catalogs. The resumable sync pattern (transient + WP-Cron
continuation) lets a 5,000-post sync finish over multiple ticks.

**Conversation cookie lifecycle.** The widget writes
`pitchbar_conv_id` on init success so the plugin's
`CartCouponController::applyPendingCoupon` hook can locate the
correct staged coupon. Pre-v2.0 the cookie was only set when
`?pitchbar_conv` arrived as a query parameter, which left
chat→cart flows without a correlation key.

## New: BYOK (Bring Your Own Keys)

Each workspace can now pay its own Cloudflare / OpenAI / OpenRouter /
Qdrant bill. Super-admin flips a global toggle in
Settings → System; customers paste their keys at Settings → AI keys.
Per-user override (force-on / force-deny / inherit-global) lets you
grant or block individual users independent of the global flag.

Strategic for operators reselling Pitchbar as a SaaS — the more
end-customers scale, the less you owe upstream. Workspace-level
credentials are encrypted at rest via `APP_KEY`
(`workspaces.byok_keys` is an `encrypted:array` cast). Five layers
of tenant isolation guarantee workspace A's keys never leak to
workspace B's container resolution. See `/documentation/byok` for
the full matrix.

## New: file parsing routed through Cloudflare's free `toMarkdown`

PDF, DOCX, DOC, XLSX, XLS, ODT, ODS uploads now flow through
Cloudflare Workers AI's `toMarkdown` endpoint instead of the
in-process Smalot / PhpWord parsers. Cost: zero (toMarkdown for
document formats consumes 0 Neurons). Quality: structured markdown
out — headings, lists, tables preserved, which feeds the chunker
much better than Smalot's best-effort plain text on Word-exported
or scanned-with-text-layer PDFs. CSV / MD / TXT stay on the local
parsers; HTML / SVG uploads are rejected at the validator. Falls
back to Smalot when Cloudflare credentials are absent (BYOK OpenAI
customers, fresh installs) or when the Cloudflare call fails.

## New: sitemap fan-out + 500-page cap

The default `services.crawl.max_pages_per_source` bumped from 25 →
500. Buyers adding a 100-URL sitemap silently lost 75 pages.
`SitemapDiscoverer` now handles three input shapes correctly:
domain root (probes `/sitemap.xml` + `/sitemap_index.xml`), direct
sitemap URL (fetched verbatim — previous code appended a second
`/sitemap.xml`), and `<sitemapindex>` recursion. CMSes that emit a
sitemap-index by default (WordPress, Shopify, Webflow) get every
page indexed instead of just the homepage. Override the cap via
`CRAWL_MAX_PAGES_PER_SOURCE` in env.

## New: always-visible "Talk to a human" button

The widget header carries the button at all times for any agent
whose vertical includes the `ticket_escalation` capability (every
preset ships with it enabled by default). Plus
`HumanIntentDetector` matches ~40 intent phrases ("talk to a
human", "connect me to an agent", "I need to speak with someone",
…) in chat messages and short-circuits the LLM straight into the
handoff flow.

## New: 2-minute wait for human handoff + notifications

Clicking "Talk to a human" used to fall straight to "no one's
around" when no operator was actively monitoring the dashboard.
Now: the conversation ALWAYS queues and notifies, the visitor
waits up to 2 minutes (`WAIT_TIMEOUT_SECONDS = 120`), and every
workspace member with `live_chat_available=true` gets a database
+ mail notification (`HumanRequestedNotification`). Operators who
had the dashboard closed still see the queued conversation in the
sidebar on next sign-in.

## New: public KB articles

Curated answers can now be published as public knowledge-base
pages at `/kb/{workspace.slug}/{article.slug}`. Each curated
answer row carries `kb_title` + `kb_published` toggle and an
inline publish pill in the admin grid. KB pages render
operator-supplied markdown through a hardened converter
(`App\Support\SafeMarkdown`) — raw HTML stripped, `javascript:` /
`vbscript:` / `data:` URIs stripped from links.

## New: Google Sheets as a knowledge source

Pick a sheet tab, the crawler ingests every row and treats each
as a chunk seed. Mirrors the Notion / Google Docs OAuth path.

## New: multi-currency + lifetime-deal pricing

Visitors on the marketing pricing page can pick their currency;
Stripe Checkout uses the matching price. Lifetime-deal plans
render as a separate section under the subscription tiers.

## Security hardening

**Stored XSS on public KB pages — blocked.**
`Illuminate\Support\Str::markdown` defaults to `html_input='allow'`
and `allow_unsafe_links=true`, which would make `<img onerror=...>`
and `[click](javascript:...)` in a KB article execute as live JS
on every visitor browser. New `App\Support\SafeMarkdown` helper
swaps in `html_input='strip'` + `allow_unsafe_links=false`. Ten
unit tests pin every payload.

**SSRF guard on the crawler.** A workspace admin pasting
`http://169.254.169.254/...` (AWS metadata),
`http://localhost:6379/` (loopback Redis), or
`http://10.0.0.5/admin` (intranet) used to walk straight into the
server's internal network on the `PlainHttpCrawler` fallback. New
`App\Support\UrlSafetyGuard` blocks every private / loopback /
link-local CIDR, refuses non-http(s) schemes, and (on crawl jobs)
DNS-resolves hostnames to catch rebind attacks where
`attacker.com` points at `127.0.0.1`. `PlainHttpCrawler` also
re-validates every redirect hop and disables auto-follow.

**Widget Origin re-check on every privileged endpoint.** New
`VerifyWidgetOrigin` middleware re-validates the request Origin
against the JWT-bound agent's `allowed_origins` on every
`/v1/widget/*` endpoint except `init` (which enforces inline
before issuing the JWT). Defence-in-depth against stolen JWTs
replayed from an unlisted origin.

**Browser-level defence headers on every web response.** CSP
(`default-src 'self'; object-src 'none'; base-uri 'self';
form-action 'self'; frame-ancestors 'self'`), HSTS on HTTPS
requests (1 year + subdomains), `X-Content-Type-Options: nosniff`,
`Referrer-Policy: strict-origin-when-cross-origin`. Scoped to the
`web` middleware group — the widget API is excluded because
buyers embed cross-origin.

**Upload MIME allowlist.** `UploadController` requires
`mimes:pdf,docx,doc,xlsx,xls,csv,md,markdown,txt,odt,ods` on
every uploaded file. `.html` / `.exe` / `.svg` / `.zip` uploads
get a 422 at the validator before the parser sees a byte.

**Mass-assignment cleanup on `User`.** `byok_enabled` and
`default_workspace_id` removed from `User::#[Fillable]`. Both are
privilege-bearing — sanctioned admin paths use `forceFill` after
authorisation.

## Improvements

**Crawler retry policy.** `CrawlPageJob` now distinguishes
rate-limited (release-with-fresh-backoff, no retry slot burned),
permanent (DNS error, 4xx) — short-circuit via `fail()` — and
transient (5xx) failures. Stops the `MaxAttemptsExceededException`
flood that used to fill production logs whenever a customer
indexed a domain with a few dead URLs. Per-job timeout 90s,
`failOnTimeout=true` so timeouts still flip Source rows to
`failed` with customer-readable errors.

**Cloudflare Worker cron driver hardened.** `TickCommand`
`--max-time` bumped 25 → 55s (the loop exits before the 60s tick
boundary), `--job-timeout` default 120s, queues processed:
`crawl,index,default`. Resolves the
`MaxAttemptsExceededException` flood reported in production.

**PDF reindex works for uploads.** `UploadController` now
persists parsed segment text under
`storage/app/private/uploads/{source_id}/segment-N.txt`. The
Reindex button reads it back without re-uploading the original.
Pre-fix, "Reindex" was a no-op on file uploads because the
controller short-circuited on a null `document.url`.

**PDF parser sanitization.** Strips PDF stream artifacts, control
bytes, and "/Type /Page" leakage that some encoders dump into
`getText()`. Falls back to full-document text when per-page
extraction returns empty (common for Word-exported PDFs). Drops
segments with <75% printable characters as "no extractable text"
instead of indexing garbage.

**`pages_total` in source preview reads
`services.crawl.max_pages_per_source`.** The progress indicator
on the Sources preview now reflects the current cap (500 by
default) instead of the hard-coded 25 that was stale after the
bump.

**Cloudflare AI Gateway support.** Set
`CLOUDFLARE_AI_GATEWAY_URL` to route Workers AI calls through
Cloudflare's gateway — observability, caching, optional
per-account rate limits.

**Documentation refresh.** New `/documentation/byok` page;
security, knowledge, live-chat, allowed-origins, env, and
deployment pages all updated to reflect the v2.0.0 changes.

## Fixes (operator-reported during v2.0.0)

**"AI keys" link surfaced to customers.** After a buyer enrolled
BYOK from the admin dashboard, their customers had no way to find
the `/settings/byok-keys` form because no nav entry pointed at it.
Fixed by adding a conditional sidebar entry driven by the
`byok.unlocked` Inertia shared prop.

**"AI keys" link hidden from super_admin.** Super-admins were
seeing the customer-facing BYOK menu. They manage platform-wide
AI credentials at `/settings/system` instead; the workspace BYOK
form is customer-only. The controller hard-404s for super_admin
too as belt-and-braces.

**Token-count cap precision.** Replaced naive `mb_strlen / 4`
heuristics for the max-tokens budget with a Unicode-aware
estimator that distinguishes ASCII (4 chars/token) from CJK
(~1 char/token). The plan's `max_tokens` ceiling now applies
correctly to non-English visitor messages.

**Sources errors are sanitised before reaching the customer UI.**
Raw Cloudflare 401 JSON envelopes used to bleed through to the
Sources list. `SourceErrorPresenter` now rewrites them to short
friendly lines.
