Developer guide

Read and change the wishlist from JavaScript, move or hide the button, add data to wishlist rows, override templates and hook into Pro. Every example is checked against the code.

FreePro

Architecture

JavaScript API

window.SiterynWishlist (script handle siteryn-wishlist, loaded on every page while the wishlist is enabled):

MethodReturns
getItems()Product IDs (numbers)
has( id ) · count()Boolean · number
add( id ) · remove( id ) · toggle( id )Updated IDs. Pass numbers, not strings.
getAddedAt( id )ISO date string or null
STORAGE_KEY · UPDATED_EVENTsiteryn_wishlist_items · siteryn_wishlist:updated

Storage format: localStorage['siteryn_wishlist_items'] = [{"id":123,"addedAt":"2026-10-18T09:00:00.000Z"}]. Use default-language product IDs; the buttons' data-product-id already holds them.

document.addEventListener( 'DOMContentLoaded', function () {
  if ( ! window.SiterynWishlist ) { return; }
  var prev = SiterynWishlist.getItems();
  document.addEventListener( SiterynWishlist.UPDATED_EVENT, function ( e ) {
    var now = e.detail.items; // detail: { items, count, source: 'local' | 'storage' }
    now.filter( function ( id ) { return prev.indexOf( id ) === -1; } )
      .forEach( function ( id ) { console.log( 'added', id ); } );
    prev.filter( function ( id ) { return now.indexOf( id ) === -1; } )
      .forEach( function ( id ) { console.log( 'removed', id ); } );
    prev = now.slice();
  } );
} );
// New [data-siteryn-wishlist-toggle] buttons pick up their state on this event:
document.dispatchEvent( new CustomEvent( 'siteryn_wishlist:updated', {
  detail: { items: SiterynWishlist.getItems(), count: SiterynWishlist.count() }
} ) );

Settings passed to the script: window.siterynWishlist (restUrl, storageKey, wishlistPageUrl, lang, tooltip, i18n), and on the wishlist page window.siterynWishlistPage (showPrice, showAddedDate, showStock, removeControlStyle, buttonStyle).

Other event: siteryn_wishlist:item-rendered (detail { element, product, context: 'own'|'shared' }) for each wishlist page row.

Moving or hiding the button

add_action( 'siteryn_wishlist_loaded', function () {
	remove_action( 'woocommerce_single_product_summary',
		array( \Siteryn\SiterynWishlist\Frontend::class, 'render_single_product_button' ), 35 );
} );
add_action( 'woocommerce_product_meta_end', function () {
	if ( function_exists( 'siteryn_wishlist_button' ) ) {
		siteryn_wishlist_button( get_the_ID() );
	}
} );

Loop buttons: render_loop_button on woocommerce_after_shop_loop_item (15), append_loop_button_to_product_image_block on render_block_woocommerce/product-image (10), render_card_hook_button on each action in siteryn_wishlist_card_hooks (default siteryn_product_card_after_body, 10), and the theme-card fallback mark_product_card on woocommerce_post_class (10) with render_card_buttons on wp_footer (5). The script then moves each button to the chosen Position. There's no filter to hide one product's button; wrap the template:

add_filter( 'siteryn_wishlist_locate_template', function ( $path, $name ) {
	if ( 'button.php' !== $name || '' === $path ) { return $path; }
	$GLOBALS['my_wl_button_tpl'] = $path;
	return __DIR__ . '/button-guard.php';
}, 10, 2 );

// button-guard.php
// $product_id = isset( $product_id ) ? absint( $product_id ) : 0;
// if ( $product_id && ! has_term( 'gift-cards', 'product_cat', $product_id ) ) {
//     include $GLOBALS['my_wl_button_tpl'];
// }

Adding data to wishlist rows

add_filter( 'siteryn_wishlist_product_data', function ( $data, $product ) {
	$data['sku'] = $product->get_sku();
	return $data;
}, 10, 2 );
document.addEventListener( 'siteryn_wishlist:item-rendered', function ( e ) {
  var body = e.detail.element.querySelector( '.siteryn-wishlist-page__item-body' );
  if ( ! body || ! e.detail.product.sku ) { return; }
  var span = document.createElement( 'span' );
  span.className = 'my-wl-sku';
  span.textContent = 'SKU: ' + e.detail.product.sku;
  body.appendChild( span );
} );

Load that script with siteryn-wishlist-page as a dependency.

Templates

Copy a template to your-theme/siteryn-wishlist/:

TemplateVersion · variables
button.php1.0.2 · $product_id, $label_add, $label_remove, $icon, $icon_custom_url, $active_icon, $active_icon_custom_url, $icon_position, $display_type, $style, $size, $context, $alignment, $position, $loop_position, $loop_corner, $loop_reveal_on_hover, $loop_match_add_to_cart…
wishlist-page.php1.0.1 · $layout, $width, $empty_message, $page_title, $button_style
sign-in-required.php1.0.0

An admin notice lists theme copies with an older @version. Keep the markup contract:

REST routes

RouteAccess
GET /siteryn-wishlist/v1/products?include=1,2,3Public. Up to 100 visible products: id, display_id, name, permalink, image, price_html, in_stock, stock_status, purchasable. Optional lang (WPML/Polylang) returns each product in that language.
GET /siteryn-wishlist-pro/v1/lists · POST /lists/{id}/items · DELETE /lists/{id}/items/{product} ProLogged in, licensed, with sync or multiple wishlists on. Send X-WP-Nonce. The default list's id is default. There is no route to read a single list; use GET /lists.
POST /lists · PATCH|DELETE /lists/{id}Logged in, licensed, Multiple wishlists on. Up to 20 lists of 100 items; the default list can't be deleted.
POST /lists/{id}/add-to-cartLogged in, licensed, Add all to cart on. Optional product_ids (otherwise the stored list). Returns { added, skipped }.
GET /shared/{token}Public; public lists only.
POST /estimate-requests · POST /eventsEstimate form (nonce) · activity counters.

Pro

$lists = get_user_meta( $user_id, '_siteryn_wishlist_pro_lists', true );
$ids   = array();
foreach ( (array) $lists as $list ) {
	if ( 'default' === ( $list['id'] ?? '' ) ) {
		$ids = wp_list_pluck( $list['items'], 'product_id' );
	}
}
$products = \Siteryn\SiterynWishlist\ProductRepository::get_products_data( $ids );

Wishlist page buttons are styled with CSS variables --siteryn-wishlist-page-btn-bg, -border, -text, -hover-bg, -hover-border, -hover-text and -radius (they replace the old --siteryn-wishlist-cart-* variables). window.SiterynWishlistProSyncing is true while sync copies a customer's saved items. The admin bar node id is siteryn-wishlist.

add_filter( 'siteryn_wishlist_pro_back_in_stock_email', function ( $email, $product, $user ) {
	$email['subject'] = sprintf( 'Good news, %s: %s is back!', $user->display_name, $product->get_name() );
	return $email; // array( 'subject' => …, 'message' => … ), plain text
}, 10, 3 );
<?php
if ( function_exists( '\SiterynPro\SiterynWishlist\siteryn_wishlist_pro_header_icon' ) ) {
	\SiterynPro\SiterynWishlist\siteryn_wishlist_pro_header_icon(); // namespaced
}
?>
add_action( 'siteryn_wishlist_pro_loaded', function () {
	class My_Acme_Provider extends \SiterynPro\SiterynWishlist\AI\Providers\OpenAICompatibleProvider {
		public function get_id() { return 'acme'; }
		public function get_label() { return 'Acme AI'; }
		public function get_default_model() { return 'acme-chat-1'; }
		public function get_suggested_models() { return array( 'acme-chat-1' ); }
		protected function base_url( \SiterynPro\SiterynWishlist\AI\ProviderConfig $c ) {
			return 'https://api.acme.ai/v1';
		}
	}
	add_filter( 'siteryn_wishlist_pro_ai_providers', function ( $p ) {
		$p['acme'] = new My_Acme_Provider();
		return $p;
	} );
} );
// wp-config.php: define( 'SITERYN_WISHLIST_PRO_AI_ACME_API_KEY', '…' );

Behind a proxy or CDN, map the real visitor IP for rate limits with the siteryn_wishlist_pro_client_ip filter.

See the Hooks reference for every action and filter.