WordPress & WooCommerce
REST API reference
The plugin and the Prime ChatFlow server talk to each other in two directions over HTTP, with two different auth schemes. This reference documents every endpoint involved — request shape, response shape, status codes, and how authentication is enforced.
Auth at a glance
| Direction | Credential | Where it lives | Replay window |
|---|---|---|---|
| Plugin → Prime ChatFlow | Bearer API token pbar_… |
Prime ChatFlow stores only the SHA-256 hash. Plugin keeps plaintext in wp_options. |
— |
| Plugin → Prime ChatFlow (mutating) | Bearer + HMAC body signature | HMAC key is the bearer plaintext itself. | 5 min |
| Prime ChatFlow → Plugin | HMAC body signature | Per-token shopper_signing_secret (plaintext on both sides). |
5 min |
HMAC signature scheme
X-Prime-ChatFlow-Signature: t=<unix_ts>,v1=<hex_sig>
sig = hmac_sha256(secret, "{t}.{raw_body}")
Both sides reject requests whose t is more than 300
seconds away from the verifying server's clock. Both sides
constant-time-compare the signature with hash_equals.
The same scheme is used for outbound webhooks documented under
Outgoing webhooks.
Prime ChatFlow endpoints (plugin → Prime ChatFlow)
Base: your Prime ChatFlow workspace URL. All routes are POST and
require Authorization: Bearer pbar_… with the
wp:integration ability. Mutating routes
additionally require X-Prime-ChatFlow-Signature.
POST /api/v1/wp/handshake
Called by the plugin's Test connection button to
discover the workspace, list available agents, and capture the
token's shopper_signing_secret. No HMAC required
(the bearer is sufficient — the handshake doesn't mutate state
beyond writing last_used_at).
Request:
{
"site_url": "https://shop.example.com",
"plugin_version": "2.0.4",
"woocommerce_active": true,
"wordpress_version": "6.6"
}
Response 200:
{
"data": {
"workspace": { "id": "01HZ…", "name": "Acme" },
"agents": [
{ "id": "01HZ…", "name": "Storefront bot", "site_type": "ecommerce", "language_default": "en", "is_published": true }
],
"token": {
"id": "01HZ…",
"name": "shop.example.com",
"abilities": ["wp:integration"],
"shopper_signing_secret": "sek_…"
},
"recommended_site_type": "ecommerce",
"echo": { "site_url": "https://shop.example.com", "plugin_version": "2.0.4" }
}
}
recommended_site_type hints at which vertical the
agent should adopt: ecommerce when WooCommerce is
active on the calling site, otherwise null.
shopper_signing_secret is plaintext — the plugin
stashes it in wp_options silently.
Errors:
401 invalid_token— bearer missing, malformed, or revoked.403 insufficient_ability— token doesn't grantwp:integration.
POST /api/v1/wp/posts/sync
Bulk post upsert. Up to 50 posts per request. Requires HMAC.
{
"agent_id": "01HZ…",
"site_url": "https://shop.example.com",
"plugin_version": "2.0.4",
"posts": [
{
"wp_id": 142,
"post_type": "page",
"permalink": "https://shop.example/pricing",
"title": "Pricing",
"content_html": "<div class=\"elementor-…\">…</div>",
"excerpt": "Three plans, two outcomes…",
"content_hash": "ab12…ef90",
"modified_at": "2026-05-09T14:30:00+00:00",
"language": "en-us",
"taxonomy_terms": ["pricing", "plans"]
}
]
}
Response 200:
{
"data": {
"queued": 1,
"skipped_unchanged": 0,
"deleted": 0
}
}
queued = number of posts whose hash differed from
the stored Document and were queued for embedding.
skipped_unchanged = number whose hash matched (no
cost). The vector store is updated asynchronously by
IndexDocumentJob; subsequent retrievals start
finding the new content within seconds.
POST /api/v1/wp/posts/changed
Single-post delta. Same shape as posts/sync but with
a posts array of length 1 and an extra
action field of "upsert" or
"delete". HMAC required.
POST /api/v1/wp/products/sync
Bulk WooCommerce product upsert. Up to 50 per batch. HMAC required.
{
"agent_id": "01HZ…",
"site_url": "https://shop.example.com",
"plugin_version": "2.0.4",
"products": [
{
"wp_id": 9001,
"sku": "T-BLU-M",
"name": "Blue tee",
"permalink": "https://shop.example/product/blue-tee",
"image_url": "https://shop.example/wp-content/uploads/2026/05/blue-tee-300x300.jpg",
"short_description": "<p>Crew-neck cotton tee.</p>",
"description": "<p>100% combed ring-spun cotton…</p>",
"price": "29.00",
"regular_price": "39.00",
"sale_price": "29.00",
"currency": "USD",
"stock_status": "instock",
"on_sale": true,
"content_hash": "cd34…12ef",
"modified_at": "2026-05-09T14:30:00+00:00",
"categories": ["tees", "summer"],
"attributes": ["color: blue", "size: S, M, L"]
}
]
}
On first upsert against an agent with site_type =
"generic", the agent is silently switched to
"ecommerce". See
Content sync for the
full rules.
POST /api/v1/wp/products/changed
Single-product delta. Same as posts/changed but for
WC products. HMAC required.
POST /api/v1/wp/coupons/sync
Snapshots the store's active coupons. Idempotent (full-replace).
{
"agent_id": "01HZ…",
"site_url": "https://shop.example.com",
"plugin_version": "2.0.4",
"coupons": [
{ "code": "WELCOME10", "label": "10% off", "discount": "10%", "expires_at": null },
{ "code": "FREESHIP", "label": "5 off your order", "discount": "5", "expires_at": "2026-12-31T00:00:00+00:00" }
]
}
The list is persisted on the agent's woocommerce_products
source under config['coupons']. Subsequent prompt
assembly includes the codes verbatim so the LLM never invents
them.
POST /api/v1/widget/coupon/apply
Called by the widget's Apply button on a
<coupon/> chat block. Auth is the widget JWT
(not a bearer token), throttle 30/min/IP.
{ "code": "WELCOME10" }
Prime ChatFlow resolves the agent's WordPress / WooCommerce source,
HMAC-signs the body with the workspace's shopper signing secret,
and forwards to /wp-json/pitchbar/v1/cart/coupon on
the WP site. The forwarded body includes
conversation_id so the plugin can stage the coupon
in a per-conversation transient.
Plugin endpoints (Prime ChatFlow → plugin)
Base: {wp_site_url}/wp-json/pitchbar/v1/. All routes
are POST. Auth: X-Prime-ChatFlow-Signature verified against
the plugin's stored shopper_signing_secret. No
WordPress nonce or cookie auth — the caller is the Prime ChatFlow
server, not a logged-in browser.
Verification path
Every plugin REST controller extends
Prime ChatFlow\Rest\RestController which gates each request
via verifyOrReject($request):
- Read the
X-Prime-ChatFlow-Signatureheader. If missing → 401missing_signature. - Read the plugin's stored
shopper_signing_secretfromwp_options. If empty → 401plugin_unconfigured. - Compute the expected signature over the raw request body. If
hash_equalsfails → 401signature_mismatch. - If the timestamp
tis > 300s away fromtime()→ reject withsignature_mismatchtoo (the secret never matched the replayed timestamp).
The error response is always JSON-wrapped:
{ "error": { "code": "signature_mismatch", "message": "HMAC signature did not verify." } }
POST /wp-json/pitchbar/v1/orders/lookup
Looks up a customer's recent WooCommerce orders.
{
"wp_user_id": 42,
"limit": 5,
"order_number": "WC-1234" // optional — filter the result set
}
Response 200:
{
"data": {
"count": 2,
"orders": [
{
"id": 9001,
"number": "9001",
"status": "completed",
"total": "49.99",
"currency": "USD",
"date_created": "2026-05-08T11:23:00+00:00",
"items": [{ "name": "Blue tee", "qty": 1, "sku": "T-BLU-M", "total": "29.00" }],
"tracking_url": "https://aftership.com/…",
"order_url": "https://shop.example/my-account/view-order/9001/"
}
]
}
}
tracking_url is best-effort: the controller checks
_aftership_tracking_url,
_tracking_url, and _st_tracking_link
order meta keys. If none match, the field is an empty string and
the LLM falls back to surfacing order_url.
When WooCommerce isn't active, the controller returns 200 with
{ "orders": [], "count": 0, "note": "woocommerce_inactive" }
so the agent can answer gracefully.
POST /wp-json/pitchbar/v1/leads
Pushes a captured Prime ChatFlow lead back into WordPress.
{
"email": "shopper@example.com",
"name": "Alex Visitor",
"phone": "+1-555-0123",
"conversation_id": "01HZ…",
"pitchbar_lead_id": "01HZ…"
}
Response 200:
{ "data": { "user_id": 199 } }
Behaviour:
- If a WP user with that email exists, update its first_name, billing_phone, and Prime ChatFlow meta keys.
- If no user, create a WC customer (
wc_create_new_customer) when Woo is active, otherwise a WP subscriber viawp_create_userwith a random 24-char password. - Username is derived from the local part of the email + a numeric suffix until unique.
pitchbar_lead_id+pitchbar_conversation_idare written to user meta so the store owner can correlate.
POST /wp-json/pitchbar/v1/cart/coupon
Stages a coupon code for the visitor's next cart load.
{
"code": "WELCOME10",
"conversation_id": "01HZ…"
}
Response 200:
{
"data": {
"applied": false,
"pending": true,
"message": "Coupon staged. It will apply when the visitor opens their cart."
}
}
Behaviour:
- Validates the coupon exists via
new WC_Coupon($code)+get_id()≠ 0. If not, returns 400invalid_coupon. - Stages the code in a 15-minute transient:
pitchbar_pending_coupon_{conversation_id}. - The plugin's
woocommerce_load_cart_from_sessionhook reads the transient on the visitor's next cart load (located by thepitchbar_conv_idcookie the widget writes at init), callsWC()->cart->apply_coupon($code), and clears the transient.
When WooCommerce isn't active, returns 400
woocommerce_inactive. The coupon-card Apply button
in the widget is hidden via a feature flag in that case.
Error envelope
Every Prime ChatFlow endpoint returns errors as:
{ "message": "…", "code": "…", "errors": { "field": ["…"] } }
Every plugin endpoint returns errors as:
{ "error": { "code": "…", "message": "…" } }
The shape difference is intentional — Prime ChatFlow follows Laravel's validator convention, plugin follows WP REST convention. Both sides parse the other transparently.
Throttling
Prime ChatFlow's /api/v1/wp/* routes throttle by API token
(default 60 req/min per token). The plugin's
/wp-json/pitchbar/v1/* routes don't enforce a quota
— the HMAC check + 5-minute replay window already prevents abuse,
and the upstream Prime ChatFlow timeout (5s on
OrderLookupController) bounds runtime.
Status codes summary
| Code | Meaning |
|---|---|
| 200 | Success or "ignored as no-op" (delete of unknown post). |
| 400 | Validation error — body shape wrong, missing field, invalid coupon. |
| 401 | Auth failed (missing token / signature / wrong secret). |
| 403 | Token missing required ability. |
| 404 | Resource doesn't exist on this workspace (most often the agent_id). |
| 422 | Validation error from Laravel's validator (Prime ChatFlow side). |
| 429 | Throttled. |