SureCart: Dynamic Updating Stock Numbers

Tech Articles | 22 January 2026 | Blog, Coding, SureCart, Wordpress

SureContact was released this week, and it has a limit of 500 lifetime spots. It has a live countdown of the available numbers, which you can see in the image below

Earlybird lifetime deal offer

I wanted to recreate this. Now, I already know that if you limit the number of stock SureCart stores, this is in a postmeta called available_stock So I already knew what I wanted to display, and I also know that all SureCart Products are effectively a custom post type, so they have a post id.

This means I had the moving parts; all I needed to do was read it and update it. I decided this was best done by creating a mini rest api point for polling this and writing some front-end JS to look for our wrapped shortcode span and update it dynamically. I settled on updating every 5 seconds, but in reality, you could probably set this to 30 seconds, and you could even make it dynamic so it speeds up as they get closer to being “sold out”.

I added a second code here, which hides the output if there is none left, so our countdown isn’t fake, and could be used to hide a section like the SureContact one above.

Example Page:

Our SALES page offer text

The below gif shows this in action. We start with it hidden because its Zero, we increase to 10,0 then down to 50, and all the updates happen on the page you

sales count down in action

The Snippet, edit the post id and your poll interval to your setup

<?php

define( 'SC_STOCK_POST_ID', 24 );                  // your post id
define( 'SC_STOCK_META_KEY', 'available_stock' );  // meta key
define( 'SC_STOCK_POLL_SECONDS', 5 );              // <-- poll interval in SECONDS

function sc_get_available_stock( $post_id = null ) {
	$post_id = (int) ( $post_id ?: SC_STOCK_POST_ID );
	if ( $post_id <= 0 ) return 0;

	$val = get_post_meta( $post_id, SC_STOCK_META_KEY, true );

	// If missing, initialize to 0 so everything "just works"
	if ( $val === '' ) {
		add_post_meta( $post_id, SC_STOCK_META_KEY, 0, true );
		$val = 0;
	}

	return max( 0, (int) $val );
}

/**
 * REST endpoint for stock polling
 * GET /wp-json/sc-stock/v1/available
 */
add_action( 'rest_api_init', function() {
	register_rest_route( 'sc-stock/v1', '/available', [
		'methods'             => 'GET',
		'callback'            => function() {
			return [
				'post_id' => SC_STOCK_POST_ID,
				'stock'   => sc_get_available_stock( SC_STOCK_POST_ID ),
			];
		},
		'permission_callback' => '__return_true', // public read (only exposes the number)
	] );
} );

/**
 * [sc_stock_left] shortcode
 * Wrap output in a span so JS can update it live.
 */
add_shortcode( 'sc_stock_left', function( $atts ) {
	$atts = shortcode_atts(
		[
			'prefix'  => '',
			'suffix'  => '',
			'post_id' => 0, // optional override (not used by polling unless you extend it)
		],
		$atts,
		'sc_stock_left'
	);

	$post_id = (int) ( $atts['post_id'] ?: SC_STOCK_POST_ID );
	$stock   = sc_get_available_stock( $post_id );

	$prefix = esc_html( $atts['prefix'] );
	$suffix = esc_html( $atts['suffix'] );

	// data-sc-stock is used by JS to find/update these nodes
	return sprintf(
		'<span data-sc-stock="1" data-sc-prefix="%s" data-sc-suffix="%s">%s%s%s</span>',
		esc_attr( $atts['prefix'] ),
		esc_attr( $atts['suffix'] ),
		$prefix,
		$stock,
		$suffix
	);
} );

/**
 * [sc_if_in_stock] shortcode
 * Wrap content in a container JS can hide/show live.
 */
add_shortcode( 'sc_if_in_stock', function( $atts, $content = '' ) {
	$atts = shortcode_atts(
		[
			'post_id' => 0,
			'min'     => 1,
		],
		$atts,
		'sc_if_in_stock'
	);

	$post_id = (int) ( $atts['post_id'] ?: SC_STOCK_POST_ID );
	$stock   = sc_get_available_stock( $post_id );
	$min     = (int) $atts['min'];

	$inner = do_shortcode( $content );

	// Always render a wrapper so JS can toggle it without a refresh
	$visible = ( $stock >= $min );

	return sprintf(
		'<div data-sc-in-stock="1" data-sc-min="%d" style="%s">%s</div>',
		(int) $min,
		$visible ? '' : 'display:none;',
		$inner
	);
} );

/**
 * Enqueue inline polling JS on the front-end
 */
add_action( 'wp_enqueue_scripts', function() {
	// Only run on the front-end
	if ( is_admin() ) return;

	wp_register_script( 'sc-stock-poll', '', [], null, true );
	wp_enqueue_script( 'sc-stock-poll' );

	$poll_ms  = max( 1000, (int) SC_STOCK_POLL_SECONDS * 1000 );
	$rest_url = esc_url_raw( rest_url( 'sc-stock/v1/available' ) );

	$js = <<<JS
(function(){
  var pollMs = {$poll_ms};
  var endpoint = "{$rest_url}";
  var lastStock = null;

  function updateStockNodes(stock){
    // Update any [sc_stock_left] outputs
    document.querySelectorAll('[data-sc-stock="1"]').forEach(function(el){
      var prefix = el.getAttribute('data-sc-prefix') || '';
      var suffix = el.getAttribute('data-sc-suffix') || '';
      // Use textContent so it stays safe/plain
      el.textContent = prefix + String(stock) + suffix;
    });

    // Toggle any [sc_if_in_stock] wrappers
    document.querySelectorAll('[data-sc-in-stock="1"]').forEach(function(wrap){
      var min = parseInt(wrap.getAttribute('data-sc-min') || '1', 10);
      if (stock >= min) {
        wrap.style.display = '';
      } else {
        wrap.style.display = 'none';
      }
    });
  }

  function tick(){
    fetch(endpoint, { credentials: 'same-origin' })
      .then(function(r){ return r.json(); })
      .then(function(data){
        if (!data || typeof data.stock === 'undefined') return;
        var stock = parseInt(data.stock, 10);
        if (isNaN(stock)) return;

        if (lastStock === null || stock !== lastStock) {
          lastStock = stock;
          updateStockNodes(stock);
        }
      })
      .catch(function(){ /* silent */ });
  }

  // initial + interval
  tick();
  setInterval(tick, pollMs);
})();
JS;

	wp_add_inline_script( 'sc-stock-poll', $js );
} );
PHP

Support the Author

Support my work
Really Useful Plugin Logo
Wpvideobank temp
Appoligies for any spelling and grammer issue. As a dyslexic i need to rely on tools for this they like me are not perfect but I do try my best