Once the plugin is connected to your workspace, you need to push your WordPress content into Pitchbar so the agent can retrieve from it. The plugin syncs in two complementary modes: a manual bulk sync you trigger from the Settings page, and automatic deltas that fire on every WP hook for the post / product types you opted in.
Both modes are idempotent on external_id — re-running
them is free.
| WP entity | Pitchbar Source type | External ID format |
|---|---|---|
| Posts, pages, custom post types | wordpress | wp:{post_id} |
| WooCommerce products | woocommerce_products | wc:{product_id} |
| WC coupons | Embedded in woocommerce_products source config['coupons'] | — |
One Source row per (agent, site host). The two types coexist on the same agent — posts answer FAQ-style questions, products answer "do you have X" style shopping intent. Pitchbar auto-creates each Source on the first sync.
Open Settings → Pitchbar. Two buttons appear once the plugin is configured:
publish-status post of the post types you enabled, 50 per HTTP batch.simple, variable, grouped, and external products, 50 per batch.Click either button. The status line shows progress and final counts: "Sync complete. (124 posts, 87 queued, 37 skipped)". Skipped means the content hash matched the previous sync — no re-embedding needed, no LLM cost.
Shared hosting commonly enforces a 30-second PHP execution cap.
Pitchbar's syncers respect this: each pass enforces a
TIME_BUDGET_SECONDS = 20 wall-clock guard. When the
budget is exhausted AND more pages remain to process, the syncer:
pitchbar_post_sync_resume for posts, pitchbar_product_sync_resume for products. Shape: { "page": 7, "at": 1715472000 }.pitchbar_run_full_sync_event / pitchbar_run_product_sync_event).more: true, next_page: 7 in the response so the admin UI knows to show "large site detected, the rest is continuing in the background."Re-running "Sync now" while a resume marker exists picks up where the last pass left off. Successful completion (no more pages) clears the resume marker.
While a chunked sync is mid-flight, every WP admin page shows a soft notice: "Pitchbar is finishing a large-site sync in the background (posts). The widget already works; new content shows up once this completes."
On every save_post, wp_trash_post, or
before_delete_post for an opted-in post type with
publish status, the plugin fires a single delta call
to Pitchbar:
| Endpoint | Trigger | Body shape |
|---|---|---|
POST /api/v1/wp/posts/changed |
save_post / wp_trash_post / before_delete_post on an opted-in post type. |
One post payload + action: "upsert" or "delete". |
POST /api/v1/wp/products/changed |
woocommerce_new_product, woocommerce_update_product, woocommerce_delete_product, woocommerce_trash_product. |
One product payload + action: "upsert" or "delete". |
Bulk sync uses the parallel /posts/sync and
/products/sync endpoints with up to 50 entities per
batch. Both endpoints authenticate exactly the same way
(bearer + HMAC).
The plugin's PostContentExtractor resolves visible
HTML for every post before sending it to Pitchbar:
post_content. See Page builders.$GLOBALS['post'] is set + setup_postdata() is called so third-party the_content filters (Yoast, Jetpack, Divi, embeds) see a valid $post global.do_blocks() resolves every Gutenberg block.apply_filters('the_content', …) runs every theme/plugin hook (lazy-loading, image replacement, related posts, etc.).do_shortcode() resolves any remaining shortcodes.pitchbar_post_content_html filter, which lets you strip navigation chrome, force a custom template, or short-circuit entirely. See "Override the synced HTML" below.$GLOBALS['post'] is restored and wp_reset_postdata() is called inside a finally block, so a throwing filter callback can't leak the loop state.
Every sync (bulk or delta) sends a content_hash
field. Pitchbar compares it to the existing Document's stored
hash; on match the server returns immediately without re-chunking
or re-embedding. Re-running "Sync now" against a stable site is
effectively free — only the diff costs.
The hash is SHA-256 of the normalized concatenation:
sha256( title + "\n" + content_html + "\n" + excerpt + "\n" + taxonomy_term_names )
Whitespace is collapsed to single spaces via preg_replace('/\s+/u', ' ', …)
before hashing so trivial reflow doesn't trigger a re-index.
product_cat taxonomy)"color: blue, red" strings so the embedding captures both
Coupons ride along with the product sync. After every successful
product sync completes (resumed-to-completion, not mid-flight),
the plugin's CouponSyncer runs a snapshot:
shop_coupon custom post type via get_posts() — this works on every WooCommerce version since coupons shipped in WC 2.0. (Previously the plugin used wc_get_coupons(), which is not public WC API on every release.)new WC_Coupon($id) (a stable WC class).get_date_expires() < now) or already over their usage limit.POST /api/v1/wp/coupons/sync. Pitchbar persists them on the source's config['coupons'] array.
For each coupon the plugin sends: code (uppercased),
label (humanized: "10% off", "5 off your order", etc.),
discount, expires_at. See
WooCommerce
deep links for how the LLM uses these.
On the first successful product upsert against an agent whose
site_type is generic, Pitchbar
automatically switches the agent to
site_type = "ecommerce". This enables the
EcommercePreset system-prompt fragment and the
<product/> inline-block emission rules so the
LLM can recommend products as rich cards instead of plain text.
The switch fires only when site_type is
generic. Agents already on saas,
documentation, or explicit ecommerce are
never overwritten.
before_delete_post / woocommerce_delete_product
fire a single delete delta. Pitchbar tears down the
Document, its chunks, and the vector points associated with it.
Deleting an entity Pitchbar never saw is a silent no-op
(HTTP 200 with deleted: false) so repeated cleanup is
safe.
Apply your own filter to mutate the HTML the plugin sends to Pitchbar — strip the site header, force an Elementor template, redact a section, etc.
add_filter('pitchbar_post_content_html', function ($html, $post, $builder) {
// $builder is the detected page-builder slug, or null for
// plain Gutenberg posts. e.g. 'elementor', 'divi', 'bricks'.
if ($post->post_type === 'product') {
return $html;
}
// Strip any leftover navigation chrome our theme injects via
// the_content. Pitchbar already does its own tag-stripping,
// but doing it here reduces the chunk-overhead.
$html = preg_replace('#<nav[^>]*>.*?</nav>#is', '', $html);
return $html;
}, 10, 3);
The filter receives the fully-rendered HTML before hashing
and POSTing. Returning '' effectively disables sync
for that post (Pitchbar will see empty content and decline to
embed anything).
You can trigger a sync from your own code — useful for migrations or testing:
// Posts
$result = (new \Pitchbar\Sync\PostSyncer)->runFullSync();
// $result['ok'], $result['posts'], $result['more'], $result['next_page']
// Products (requires WooCommerce)
$result = (new \Pitchbar\Sync\ProductSyncer)->runFullSync();
// Coupons
$result = (new \Pitchbar\Sync\CouponSyncer)->run();
None of these throw — they return result arrays with an
ok key, an errors array, and (for the
chunked syncers) more + next_page
fields for resume tracking.