Hooks, Functions and Templates Reference
Sooner or later a store needs one thing done differently: a different label, a page that unlocks on a custom rule, a CRM that hears about every renewal. Editing plugin files works until the next update wipes your change.
Stackless gives you three safe places to hook in instead: a set of public PHP functions, actions and filters with the stackless_ prefix, and templates you can copy into your theme. Everything on this page comes from the plugin’s own code. Put your snippets in a child theme’s functions.php or a small custom plugin.
Public Functions
These live in includes/functions.php and load with the plugin. Every one of them is safe to call whether or not the owning module is on: with the module off, you get a neutral default (false, 0, an empty array, or the store’s base currency).
Modules:
stackless_module_active(string $id): boolis true when a module is switched on and actually running on this request. Module ids are snake_case, such assubscriptions,multi_currency,software_licensing.
Subscriptions:
stackless_get_subscription($subscription_id)returns a subscription object or null.stackless_get_customer_subscriptions(int $user_id, array $args = []): arraytakesstatus(array of status slugs) andproduct_idin$args.stackless_customer_has_active_subscription(int $user_id, int $product_id = 0): boolcounts active, trialing and pending-cancel subscriptions.$product_id0 means any product.stackless_is_subscription_product($product): booltakes aWC_Productor an ID.stackless_get_order_subscriptions(\WC_Order $order): arrayreturns subscriptions created or renewed by that order.stackless_order_contains_subscription(\WC_Order $order): boolstackless_cart_contains_subscription(): boolstackless_cart_subscription_count(): intreturns how many subscriptions the cart will create, one per billing schedule.stackless_is_renewal_order(\WC_Order $order): boolstackless_order_requires_saved_payment_method(\WC_Order $order): boolandstackless_cart_requires_saved_payment_method(): booltell a gateway it must save a reusable payment method.stackless_create_renewal_order($subscription)returns a pending renewalWC_Orderor aWP_Error.
Currency:
stackless_current_currency(): stringis the visitor’s cart currency.stackless_base_currency(): stringis the store base currency.stackless_enabled_currencies(): arraylists ISO codes enabled for checkout.stackless_convert_amount($amount, string $to = '', string $from = '')converts at the configured rate, with no charm rounding.$todefaults to the visitor’s currency and$fromto the base currency.stackless_currency_rate(string $currency = ''): floatis the effective rate from base, markup included. It’s 0 when unknown and 1 when the module is off.stackless_switch_currency(string $currency, string $source = 'api')returns true, aWP_Error, or false when the module is off.stackless_currency_signup_fee(float $fee, \WC_Product $product, string $currency = ''): float
Templates:
stackless_get_template(string $name, array $args = []): voidloads a template fromtemplates/, overridable from the theme.
Invoice and Quote Functions
These are defined only when their module is running, so wrap calls in function_exists().
Custom Invoices:
stackless_is_invoice($order): boolstackless_invoice_save_draft(array $input, int $invoice_id = 0, int $revision = 0)returns aWC_OrderorWP_Error.stackless_invoice_input(int $invoice_id): arraystackless_invoice_summary(int $invoice_id): ?arraystackless_invoice_issue(int $invoice_id, int $revision, string $signature, string $context = 'api')issues a draft. The signature must come fromstackless_invoice_summary().stackless_invoice_schedule(int $invoice_id, array $schedule)starts or replaces an issued invoice’s recurring schedule and returns the schedule row or aWP_Error.$scheduletakesunit(week, month or year),count(1 to 12),start(Y-m-d, after today in the site timezone),ends(never, after or on),ends_after,ends_onandmode(send or draft). Invalid input returns status 400; it never throws.stackless_invoice_view_url(int $invoice_id): stringreturns the signed customer link, or an empty string for drafts.stackless_invoice_admin_url(array $args = []): stringstackless_invoice_integration_key_valid($key): boolstackless_create_invoice(array $request): arrayis the keyed automation entry point. It needsintegration_keyandidempotency_key.stackless_create_invoice_draft_trusted(array $request): arrayalways makes a private draft and never emails. It needsidempotency_key.stackless_agent(string $command, array $input = []): arrayruns invoice commands (and quote commands when Request a Quote is on) as the signed-in user.
Request a Quote:
stackless_create_quote_request(array $request): arrayneedsintegration_key(from Invoices → Automation) andidempotency_key. It never sets prices.stackless_create_quote_request_trusted(array $request): arrayneedsidempotency_key.
Subscription Statuses and Contracts
Subscription objects implement Stackless\Contracts\SubscriptionData and are WooCommerce orders of type stackless_subscription, so get_id(), get_customer_id(), get_currency(), get_items() and get_meta() work as on any order. The extra methods are is_active(), get_billing_period(), get_billing_interval(), get_next_payment_date(), get_trial_end_date(), get_end_date(), has_product(), get_renewal_mode() and get_recurring_total().
Status slugs used in hooks:
pendingsub-trialsub-activesub-onholdsub-pendcancelsub-cancelledsub-expired
A payment gateway that wants to renew Stackless subscriptions automatically implements Stackless\Contracts\SubscriptionGateway: stackless_supports(string $feature), stackless_charge_renewal(\WC_Order $renewal_order, $subscription) and stackless_cancel_subscription($subscription, bool $at_period_end). Gateways that don’t still work in manual-renewal mode.
Core and Settings Actions
stackless_loadedfires after all enabled modules have started. Args: theStackless\Core\Plugininstance.stackless_settings_after_{module_id}fires inside a module’s settings form, after its fields. Use it to print extra settings UI.stackless_settings_saved_{module_id}fires after a module’s settings are saved.
Subscription Actions
These are the documented cross-module actions, fired by the Subscriptions module:
stackless_subscription_created:$subscription, \WC_Order $parent_orderstackless_subscription_status_changed:$subscription, string $from, string $tostackless_subscriptions_status_{to}:$subscription, string $from, for one target status, such asstackless_subscriptions_status_sub-cancelledstackless_subscription_renewal_order_created:\WC_Order $renewal, $subscriptionstackless_subscription_renewal_paid:$subscription, \WC_Order $renewalstackless_subscription_payment_failed:$subscription, \WC_Order $renewalstackless_subscription_switched:$subscription, int $old_product_id, int $new_product_id, \WC_Order $switch_orderstackless_subscriptions_payment_method_changed:$subscription, $token
Here’s a small example that logs every cancellation:
add_action('stackless_subscription_status_changed', function ($subscription, $from, $to) {
if ($to === 'sub-cancelled') {
error_log('Subscription #' . $subscription->get_id() . ' cancelled (was ' . $from . ')');
}
}, 10, 3);
Other Module Actions
stackless_currency_switched:$currency, $previous, $sourcestackless_checkout_enhancements_consents_recorded:$order, $recordsstackless_buy_links_processed:int $added, array $params, after a buy link filled the cart and before the redirectstackless_custom_invoices_issued:$order, $contextstackless_custom_invoices_paid:$orderstackless_custom_invoices_cancelled:$orderstackless_custom_invoices_recurring_created:$invoice, $schedulestackless_request_quotes_requested:$id, $eventstackless_request_quotes_proposal_sent:$quoteId, $offerstackless_request_quotes_accepted:$quoteId, $issuedstackless_request_quotes_declined:$quoteId, $reasonstackless_page_access_changed:$postId, $productIdsstackless_software_licensing_keys_created:int[] $license_ids, \WC_Order $orderstackless_software_licensing_site_activated:$licenseId, string $site, bool $isLocalstackless_software_licensing_site_deactivated:int $licenseId, string $siteUrlstackless_software_licensing_order_revoked:$order, $revokedstackless_software_licensing_package_download:$license, $product, $packagestackless_stripe_gateway_dispute_created:$order, $disputestackless_paypal_gateway_webhook:$type, $resource, $eventstackless_polar_gateway_webhook:$type, $data, $mode
Useful Filters
Catalog and product pages:
stackless_free_price_text_text:string $text, ?\WC_Product $product, the text shown for a zero pricestackless_sold_count_count:int $count, \WC_Product $product, defaults to WooCommerce’s total salesstackless_stock_urgency_level:string $level, \WC_Product $product, one ofnone,in-stock,low,very-low,outstackless_custom_cart_text_text:string $text, \WC_Product $product, string $context, where context islooporsinglestackless_quantity_limits_limits:array $limits, \WC_Product $product, with keysmin,max,step(max 0 means no limit)stackless_hide_price_until_login_viewer_sees_prices:bool $sees
Subscriptions:
stackless_subscriptions_customer_actions:array $actions, $subscription, the buttons customers see in My Account. Each action haslabel,url,classand an optionalconfirm.stackless_subscriptions_product_plan:$plan, \WC_Product $productstackless_subscriptions_switch_candidates:array $products, \WC_Product $current, the products a customer can switch to
Page Access:
stackless_page_access_can_view:bool $allowed, int $postId, int $userId, int[] $products, where$userIdis 0 for guestsstackless_page_access_post_types: defaults to['page']stackless_page_access_paid_statuses: defaults to WooCommerce’s paid statusesstackless_page_access_store_page_ids: shop, cart, checkout, account, terms and privacy pages, which never take a restriction
Software Licensing:
stackless_software_licensing_allow_activation:true|\WP_Error $allowed, array $license, string $site, bool $isLocal. Return aWP_Errorto refuse.stackless_software_licensing_generate_for_order:bool $generate, \WC_Order $orderstackless_software_licensing_generate_key:string $key, array $config, array $datastackless_software_licensing_local_patterns: the site patterns treated as local or stagingstackless_software_licensing_payload,stackless_software_licensing_version_payloadandstackless_software_licensing_info_payload: the license API responsesstackless_documentation_url: change where the admin Docs links point. It receives the URL and the module or screen key, so you can send your team to internal docs instead.stackless_software_licensing_email_ids: the WooCommerce emails that include license keys
Currency and payments:
stackless_multi_currency_default_currency:string $default, array $enabledstackless_multi_currency_fixed_coupon_types: defaults to['fixed_cart', 'fixed_product']stackless_multi_currency_convert_fee:bool $convert, $fee, string $currencystackless_currency_gateways_capabilities:array $mapof gateway id to currency codesstackless_currency_selector_countryandstackless_currency_selector_country_currencies: the visitor’s detected country and the country to currency mapstackless_stripe_gateway_webhook_tolerance: signature tolerance in seconds, default 300stackless_stripe_gateway_dispute_sets_on_hold:bool, \WC_Order $order, default truestackless_paypal_gateway_button_style:array $stylewithlayout,color,shape,labelstackless_paypal_gateway_currenciesandstackless_polar_gateway_currencies: currencies each gateway accepts
Sales:
stackless_buy_links_redirect:string $url, int $added, array $paramsstackless_buy_links_buy_now_supports:bool $supports, \WC_Product $product, to show Buy now on types other than simple and variablestackless_request_quotes_rate_limit: how many quote requests one IP address can send before it’s told to wait, default 5. The counter clears 10 minutes after that IP’s last request.stackless_custom_invoices_can_issue:true $allowed, \WC_Order $order, string $context. Return a string to block issuing with that message.
License & Updates:
stackless_plugin_license_serverandstackless_plugin_license_item_id: the license store URL and product ID
For example, to say “Free download” instead of “Free” on downloadable products:
add_filter('stackless_free_price_text_text', function ($text, $product) {
return ($product && $product->is_downloadable()) ? 'Free download' : $text;
}, 10, 2);
Multi-Currency Rules for Custom Code
If you add prices or fees with code while Multi-Currency Checkout is on, follow these rules:
- Prices you set with
set_price()and cart fees are base-currency amounts. Multi-Currency Checkout converts them once, so never convert them yourself. - To show a base amount in the visitor’s currency, use
stackless_convert_amount(). - If you create an order during a storefront request, wrap it in
do_action('stackless_multi_currency_pause')anddo_action('stackless_multi_currency_resume'). - An order’s currency is fixed at checkout. Subscriptions and their renewal orders inherit it and never change.
Override Templates
Stackless templates load through WooCommerce’s template system. To change one, copy it from wp-content/plugins/stackless/templates/ into your theme’s stackless/ folder, keeping the same path below it:
wp-content/plugins/stackless/templates/wishlist/button.php
→ wp-content/themes/yourtheme/stackless/wishlist/button.php
Use a child theme so a theme update doesn’t remove your copy. These templates are overridable this way:
back_in_stock/form.php,back_in_stock/email-confirm.php,back_in_stock/email-available.phpcheckout_builder/compact.phpcheckout_enhancements/consents.php,checkout_enhancements/order-consents.php,checkout_enhancements/trust.phpcurrency_selector/selector.phpcustom_invoices/invoice.php,custom_invoices/emails/invoice.php,custom_invoices/emails/plain/invoice.phpdelivery_slots/checkout-field.php,delivery_slots/order-details.php,delivery_slots/email.php,delivery_slots/email-plain.phporder_bumps/bumps.phppage_access/notice.php,page_access/my-pages.phppaypal_gateway/payment-fields.phppolar_gateway/payment-fields.phpproduct_addons/product-form.php,product_addons/checkout-fields.phprequest_quotes/form.php,request_quotes/proposal.php,request_quotes/emails/quote.php,request_quotes/emails/plain/quote.phpsaved_carts/account.php,saved_carts/cart-form.php,saved_carts/signed-out.phpservice_products/instructions.php,service_products/email.php,service_products/email-plain.phpsoftware_licensing/myaccount-licenses.php,software_licensing/order-licenses.php,software_licensing/emails/licenses-table.php,software_licensing/emails/plain/licenses-table.phpstripe_gateway/payment-fields.php,stripe_gateway/express-buttons.phpsubscriptions/cart/recurring-totals.phpsubscriptions/myaccount/subscriptions.php,subscriptions/myaccount/view-subscription.phpsubscriptions/order/related-subscriptions.phpsubscriptions/emails/andsubscriptions/emails/plain/:new-subscription.php,new-subscription-admin.php,renewal-invoice.php,renewal-paid.php,payment-failed.php,upcoming-renewal.php,trial-ending.php,switched.php,cancelled.php,expired.php,subscription-details.phpwishlist/button.php,wishlist/list.php
The license keys email follows the same rule: yourtheme/stackless/software_licensing/emails/license-keys.php and .../emails/plain/license-keys.php. A copy already in yourtheme/woocommerce/software_licensing/emails/ still works until you add one under stackless/.
custom_invoices/invoice.php is used both for the customer’s invoice page and the admin print view, so an override changes both.
Shortcodes and Blocks
[stackless_currency_selector], also a Currency Selector block (stackless/currency-selector)[stackless_request_quote], also a block (stackless/request-quote)[stackless_wishlist], also a block (stackless/wishlist)[stackless_saved_carts][stackless_my_pages][stackless_product_details][stackless_login_to_see_price]
Each one only works while its module is switched on.
Good to Know
- Hooks named here only fire while their module is running. Check with
stackless_module_active()if your code depends on one. - Two files in
templates/aren’t overrides:checkout_builder/sample.phpis the admin layout preview, andsoftware_licensing/client/class-stackless-license-client.phpis the client you ship inside your own plugin or theme. Add Licensing and Automatic Updates to Your Plugin or Theme covers it. - The license REST API is documented in License API Reference.