D Diagent docs

WordPress & WooCommerce

REST API reference

The plugin and the Pitchbar 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

DirectionCredentialWhere it livesReplay window
Plugin → Pitchbar Bearer API token pbar_… Pitchbar stores only the SHA-256 hash. Plugin keeps plaintext in wp_options.
Plugin → Pitchbar (mutating) Bearer + HMAC body signature HMAC key is the bearer plaintext itself. 5 min
Pitchbar → Plugin HMAC body signature Per-token shopper_signing_secret (plaintext on both sides). 5 min

HMAC signature scheme

X-Pitchbar-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.

Pitchbar endpoints (plugin → Pitchbar)

Base: your Pitchbar workspace URL. All routes are POST and require Authorization: Bearer pbar_… with the wp:integration ability. Mutating routes additionally require X-Pitchbar-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 grant wp: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" }

Pitchbar 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 (Pitchbar → plugin)

Base: {wp_site_url}/wp-json/pitchbar/v1/. All routes are POST. Auth: X-Pitchbar-Signature verified against the plugin's stored shopper_signing_secret. No WordPress nonce or cookie auth — the caller is the Pitchbar server, not a logged-in browser.

Verification path

Every plugin REST controller extends Pitchbar\Rest\RestController which gates each request via verifyOrReject($request):

  1. Read the X-Pitchbar-Signature header. If missing → 401 missing_signature.
  2. Read the plugin's stored shopper_signing_secret from wp_options. If empty → 401 plugin_unconfigured.
  3. Compute the expected signature over the raw request body. If hash_equals fails → 401 signature_mismatch.
  4. If the timestamp t is > 300s away from time() → reject with signature_mismatch too (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 Pitchbar lead back into WordPress.

{
  "email": "[email protected]",
  "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 Pitchbar meta keys.
  • If no user, create a WC customer (wc_create_new_customer) when Woo is active, otherwise a WP subscriber via wp_create_user with 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_id are 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:

  1. Validates the coupon exists via new WC_Coupon($code) + get_id() ≠ 0. If not, returns 400 invalid_coupon.
  2. Stages the code in a 15-minute transient: pitchbar_pending_coupon_{conversation_id}.
  3. The plugin's woocommerce_load_cart_from_session hook reads the transient on the visitor's next cart load (located by the pitchbar_conv_id cookie the widget writes at init), calls WC()->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 Pitchbar endpoint returns errors as:

{ "message": "…", "code": "…", "errors": { "field": ["…"] } }

Every plugin endpoint returns errors as:

{ "error": { "code": "…", "message": "…" } }

The shape difference is intentional — Pitchbar follows Laravel's validator convention, plugin follows WP REST convention. Both sides parse the other transparently.

Throttling

Pitchbar'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 Pitchbar timeout (5s on OrderLookupController) bounds runtime.

Status codes summary

CodeMeaning
200Success or "ignored as no-op" (delete of unknown post).
400Validation error — body shape wrong, missing field, invalid coupon.
401Auth failed (missing token / signature / wrong secret).
403Token missing required ability.
404Resource doesn't exist on this workspace (most often the agent_id).
422Validation error from Laravel's validator (Pitchbar side).
429Throttled.