Developers

Consuming ssap_get_prices from a Custom / Headless Front End

Published · Last updated

A worked example of calling the plugin’s pricing endpoint yourself — for a headless build, a custom theme component, or a queueing layer — instead of relying on the bundled script. For the formal contract, see Developer Reference → AJAX request / response contract.

When you’d do this

  • A headless front end (React/Vue/Next) that renders catalogue cards from cached data and wants live, per-visitor prices.
  • A custom component (e.g. a bespoke product carousel) that the default .ajax-price placeholders don’t cover.
  • A batching/queueing layer that fetches prices for many products on a schedule.

If you just want the normal behaviour on a standard WooCommerce theme, you don’t need any of this — the bundled script already does it.

The essentials

  • Endpoint: admin-ajax.php, action ssap_get_prices (works for logged-in and logged-out visitors).
  • Send: action and product_ids[]. There is no nonce.
  • Get back: { success, data: { prices, stock? } }, keyed by product ID.
  • Same-origin & cookies: send the request with credentials so WooCommerce resolves the visitor’s session (geolocation, currency, cart, VAT). A cross-origin headless app must proxy through the same domain or otherwise carry the Woo session — otherwise prices resolve for an anonymous default location.

There is no nonce, deliberately

The endpoint takes no nonce, and the bundled script sends none. That is not an oversight: a nonce is per-user and expires after roughly 12 to 24 hours, but this plugin’s markup is baked into a full-page cache and replayed to other visitors, so any nonce in that HTML is either stale or belongs to somebody else. WordPress would then reject the whole request, which is the exact failure this plugin exists to avoid.

What makes that safe is the endpoint itself: it is read-only, it returns only published-product price and stock HTML that is already visible on any shop page, it changes nothing, and it caps how many products one request may ask for. The visitor is identified from their own login cookie instead, so members still get member pricing.

Two practical consequences for your own code:

  • Send no nonce at all. On the REST route in particular, an absent X-WP-Nonce header is treated as anonymous and then re-resolved from the cookie, but a header that is present and empty is a failed check and returns 403. Omit it entirely.
  • Never add state-changing behaviour behind this endpoint. It is safe because it only reads. Anything that writes needs its own properly authenticated route.

If your surface loads on a normal WordPress page, the plugin localizes an ssap_params object you can read the endpoint URL from:

const { ajax_url, transport, rest_url } = window.ssap_params;

Example: fetch and swap

async function refreshPrices(productIds) {
  const { ajax_url } = window.ssap_params;

  const body = new URLSearchParams();
  body.set('action', 'ssap_get_prices');
  productIds.forEach(id => body.append('product_ids[]', id));

  const res = await fetch(ajax_url, {
    method: 'POST',
    credentials: 'same-origin',   // critical: carries the Woo session
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });

  const json = await res.json();
  if (!json.success) return;                     // {success:false} → leave cached prices

  for (const [id, data] of Object.entries(json.data.prices)) {
    const el = document.querySelector(`[data-product-id="${id}"] .price-target`);
    if (el) el.innerHTML = data.price_html;       // server-rendered, safe HTML
  }
}
Common mistake

Calling the endpoint cross-origin without carrying the WooCommerce session. Fetch then resolves prices for an anonymous, default-location visitor, so geolocation, currency, VAT and member pricing all come out wrong. Send the request same-origin with credentials, or proxy it through the store domain, so Woo can resolve the real visitor.

Example: with stock

If Refresh stock via AJAX is enabled, each data.stock[id] carries availability_html, is_in_stock, is_purchasable, add_to_cart_html, and any adapter fields (bundled_items, components). A product whose own page you are rendering can also return single_regions, the freshly-rendered Add to Cart area, if you ask for it with single_regions[<id>][]=classic:

if (json.data.stock) {
  for (const [id, s] of Object.entries(json.data.stock)) {
    // s.is_in_stock, s.availability_html, s.add_to_cart_html, s.bundled_items, …
  }
}

Practical notes

  • Batch, don’t loop. Send all IDs in one request — that’s the whole performance point. The server caps a single request at ssap_max_products_per_request (default 200); for more products, chunk into batches of ≤200.
  • Only published products return data. Draft/private/scheduled IDs are silently omitted — don’t expect every requested ID back.
  • De-dupe your IDs before sending; the server does too, but it saves payload.
  • Failure handling: on a non-success body or a network error, keep whatever price you already showed (that’s what the bundled script does via its cached-HTML fallback).
  • React to updates: on a standard page you can also just listen for the ssap_prices_updated / ssap_stock_updated events the bundled script fires, rather than calling the endpoint yourself (Developer Reference → JavaScript events).
Performance Tip

Send every product ID in one request, not one request per product. The single batched call is the whole performance point; the server dedupes and caps at ssap_max_products_per_request (default 200), so for very large grids chunk into batches of 200.

Server-side alternative

If you’re rendering server-side (PHP) rather than via the browser, you don’t need the endpoint at all — call WooCommerce directly: wc_get_product($id)->get_price_html() runs the same pricing stack the endpoint uses. The AJAX endpoint exists specifically to move that computation to after a cached page has been served to the browser.

Further reading

the formal request/response contract, filters, and JS events behind this example.
add your own fields (bundled_items, components, custom) to the stock payload you consume here.
how the endpoint fits the render-placeholder-then-swap flow.

Comments

▲
▼
1/1