Hooks, Functions and Templates
Sooner or later a store needs one thing done differently: a CRM that hears about every renewal, a help desk button next to a license, a template with your own markup. Editing plugin files works until the next update wipes the change.
Stackless for EDD gives you safe places to hook in instead: public PHP functions, actions and filters with the stackless_edd_ prefix, two PHP interfaces and templates you can copy into your theme. Every signature on this page comes from the plugin’s 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. Each one is safe to call whether or not its module is on. With the module off you get a neutral default: false, 0, an empty array, null, the base currency or a WP_Error.
Modules
stackless_edd_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,software_licensing,multi_currency.
Subscriptions
stackless_edd_get_subscription($subscription_id)returns aSubscriptionDataobject or null.stackless_edd_get_customer_subscriptions(int $user_id, array $args = []): arraytakesstatus(array of status slugs) anddownload_idin$args.stackless_edd_customer_has_active_subscription(int $user_id, int $download_id = 0): boolcounts active, trialing and pending-cancel subscriptions. 0 means any download.stackless_edd_is_subscription_product(int $download_id, ?int $price_id = null): boolchecks a download or one of its price options.stackless_edd_get_order_subscriptions(int $order_id): arrayreturns subscriptions created or renewed by an order.stackless_edd_order_contains_subscription(int $order_id): boolstackless_edd_cart_contains_subscription(): boolstackless_edd_cart_subscription_count(): intis how many subscriptions the cart will create, one per billing schedule.stackless_edd_is_renewal_order(int $order_id): boolstackless_edd_order_requires_saved_payment_method(int $order_id): boolandstackless_edd_cart_requires_saved_payment_method(): booltell a gateway it must save a reusable payment method.stackless_edd_create_renewal_order($subscription)returns a pending renewal order ID or aWP_Error.stackless_edd_query_subscriptions(array $args = []): arraypages through subscriptions.$args:status,customer_id,user_id,download_id,page,per_page(1 to 100). Returns['items' => SubscriptionData[], 'total' => int].stackless_edd_get_subscription_orders(int $subscription_id): arrayreturns the parent, renewal and switch orders, newest first.stackless_edd_cancel_subscription(int $subscription_id, bool $at_period_end = true, string $by = 'admin')returns true or aWP_Error.$byisadmin,customerorsystemand shows in the notes.stackless_edd_reactivate_subscription(int $subscription_id, string $by = 'admin')withdraws a pending cancellation or resumes an on-hold subscription.stackless_edd_update_subscription_status(int $subscription_id, string $status, string $note = '')moves a subscription to aSubscriptionData::STATUS_*status. Use the cancel and reactivate functions for those two changes.stackless_edd_add_subscription_note(int $subscription_id, string $note): void
Licenses, Invoices and Quotes
stackless_edd_get_license(int $license_id): ?arrayreturnsid,license_key,download_id,price_id,order_id,subscription_id,user_id,customer_id,customer_email,status(stored),effective_status(expiry applied),activation_limit,activations,expires_at,created_atandupdated_at(UTC,Y-m-d H:i:s).stackless_edd_get_customer_licenses(int $user_id, string $email = ''): arrayincludes guest purchases made with the same email, newest first.stackless_edd_get_customer_invoices(int $user_id, string $email = '', array $args = []): arrayreturns issued invoices, never drafts. See Contracts for Other Plugins below for the fields.stackless_edd_get_customer_quotes(int $user_id, string $email = '', array $args = []): arrayreturns quote requests, newest first.
Currency
stackless_edd_current_currency(): stringis the visitor’s cart currency.stackless_edd_base_currency(): stringis the store currency from Downloads → Settings → General.stackless_edd_price_decimals(string $currency = ''): intandstackless_edd_base_price_decimals(): intfollow EDD’s decimal rules, such as 0 for JPY.stackless_edd_enabled_currencies(): arraylists ISO codes enabled for checkout.stackless_edd_convert_amount($amount, string $to = '', string $from = '')converts at the configured rate with no charm rounding.$todefaults to the visitor’s currency,$fromto the base currency.stackless_edd_currency_rate(string $currency = ''): floatis the rate from base, markup included. It’s 0 when unknown and 1 when the module is off.stackless_edd_switch_currency(string $currency, string $source = 'api')returns true, aWP_Erroror false when the module is off.stackless_edd_currency_signup_fee(float $fee, int $download_id, ?int $price_id = null, string $currency = ''): float
Orders, Gateways and Payments
stackless_edd_get_gateway(string $gateway_id): ?\StacklessEDD\Core\Gatewayis null for EDD’s own and third-party gateways.stackless_edd_complete_order(int $order_id, string $transaction_id = '', string $note = ''): boolrecords the transaction and moves the order to complete. Safe to call twice.stackless_edd_add_order_note(int $order_id, string $note): voidstackless_edd_order_pay_url(int $order_id): stringis EDD’s order recovery link, where a customer pays a pending order.stackless_edd_get_saved_payment_methods(int $user_id, string $gateway_id = ''): arrayreturnsid,gateway,label,is_defaultandexpiresper method.stackless_edd_add_payment_method_url(string $gateway_id, int $subscription_id = 0): stringis empty when the gateway can’t do it.stackless_edd_cart_item_is_locked(int $download_id, array $options): boolandstackless_edd_cart_has_locked_items(): bool. Invoice lines and renewal or switch payments are locked: leave their price, quantity and fees alone.
Money, Notices, Account and Agents
stackless_edd_price($amount, string $currency = ''): stringformats a price with its symbol, like EDD does on the storefront.stackless_edd_decimal($amount, ?int $decimals = null, string $currency = ''): stringreturns a rounded decimal string.stackless_edd_add_notice(string $message, string $type = 'success'): voidshows a shopper message on the next page view. Types:success,error,info.stackless_edd_account_url(string $tab = '', array $args = []): stringlinks to a tab of the[stackless_edd_account]page, such asstackless_edd_account_url('subscriptions', ['id' => 4]).stackless_edd_account_item_actions_html(string $type, int $id, $data): stringreturns what plugins printed onstackless_edd_account_item_actions.stackless_edd_agent(string $command, array $input = []): arrayruns an agent command as the signed-in user. See AI Agents.stackless_edd_get_template(string $name, array $args = []): voidandstackless_edd_get_template_html(string $name, array $args = []): stringload a template (see Override Templates).
Subscription Statuses and Contracts
Subscriptions live in their own tables and implement StacklessEDD\Contracts\SubscriptionData. Renewal and switch orders are ordinary EDD orders linked through order meta. Dates are UTC DateTimeImmutable objects, or null.
Methods: get_id(), get_status(), get_customer_id() (EDD customer), get_user_id() (0 for guests), get_email(), get_currency(), get_gateway(), get_parent_order_id(), get_items(), get_meta(), update_meta(), is_active(), get_billing_period(), get_billing_interval(), get_next_payment_date(), get_trial_end_date(), get_end_date(), set_next_payment_date(), has_product(int $download_id), get_renewal_mode() (auto or manual) and get_recurring_total().
Hooks pass the internal status slugs. The REST API and webhooks show the public names.
| Constant | Slug in hooks | Public name |
|---|---|---|
STATUS_PENDING | pending | pending |
STATUS_TRIAL | trialling | trial |
STATUS_ACTIVE | active | active |
STATUS_ON_HOLD | on_hold | on-hold |
STATUS_PENDING_CANCEL | pending_cancel | pending-cancel |
STATUS_CANCELLED | cancelled | cancelled |
STATUS_EXPIRED | expired | expired |
Order meta constants on the same interface: META_TOKEN_ID (_stackless_edd_payment_token_id), META_GATEWAY_CUSTOMER (_stackless_edd_gateway_customer_id) and META_GATEWAY_SUBSCRIPTION (_stackless_edd_gateway_subscription_id) on the parent order, and META_RENEWAL_OF (_stackless_edd_renewal_of) and META_SWITCH_OF (_stackless_edd_switch_subscription_id) on renewal and switch orders.
Gateways That Renew Automatically
A gateway built on StacklessEDD\Core\Gateway renews subscriptions by implementing StacklessEDD\Contracts\SubscriptionGateway:
stackless_edd_supports(string $feature): boolforauto_renewal,multiple_subscriptions,zero_total_setup,payment_method_change,amount_change,date_changeandgateway_managed.stackless_edd_charge_renewal(\EDD\Orders\Order $renewal_order, SubscriptionData $subscription): arrayreturnsresult(success,pendingorfailed) and an optionalmessage. It must be idempotent per renewal order, and on success it callsstackless_edd_complete_order()itself.stackless_edd_cancel_subscription(SubscriptionData $subscription, bool $at_period_end): boolcancels at the provider for gateway-managed billing. Others return true.
Third-party EDD gateways have no gateway object. Subscriptions bought with them renew manually: a pending renewal order plus a payment link.
Core and Settings Hooks
| Hook | Type | Arguments |
|---|---|---|
stackless_edd_loaded | action | StacklessEDD\Core\Plugin $plugin, after enabled modules have started |
stackless_edd_settings_after_{module_id} | action | none; prints inside a module’s settings form, after its fields |
stackless_edd_settings_saved_{module_id} | action | none; runs after a module’s settings are saved |
stackless_edd_admin_field_{type} | action | array $field; prints a custom settings field row |
stackless_edd_settings_sanitize_option and stackless_edd_settings_sanitize_option_{id} | filter | $value, array $field, $raw; return null to skip saving |
stackless_edd_account_tabs | filter | array $tabs; each tab has title, icon, priority, callback |
stackless_edd_account_page_id | filter | int $page_id |
stackless_edd_emails | filter | array $emails listed on Stackless → Emails |
stackless_edd_defer_script | filter | bool $defer, string $handle |
stackless_edd_live_store | filter | bool $live, string $state |
stackless_edd_documentation_url | filter | string $url, string $key |
Here’s a tab added to the account page. The callback receives the tab’s query arguments:
add_filter('stackless_edd_account_tabs', function (array $tabs) {
$tabs['support'] = [
'title' => 'Support',
'icon' => 'sparkles',
'priority' => 80,
'callback' => function (array $query) {
echo '<p>Need help? Reply to any receipt email.</p>';
},
];
return $tabs;
});
stackless_edd_documentation_url changes where the Documentation and “How to set up” links in the Stackless admin point, for example to an internal wiki. $key is the module ID or a screen key such as dashboard, emails or account.
Subscription Actions
The Subscriptions module fires these cross-module actions:
| Action | Arguments |
|---|---|
stackless_edd_subscription_created | SubscriptionData $subscription, int $parent_order_id |
stackless_edd_subscription_status_changed | SubscriptionData $subscription, string $from, string $to |
stackless_edd_subscriptions_status_{to} | SubscriptionData $subscription, string $from, for one target status |
stackless_edd_subscription_renewal_order_created | int $renewal_order_id, SubscriptionData $subscription |
stackless_edd_subscription_renewal_paid | SubscriptionData $subscription, int $renewal_order_id |
stackless_edd_subscription_payment_failed | SubscriptionData $subscription, int $renewal_order_id |
stackless_edd_subscription_switched | SubscriptionData $subscription, int $old_download_id, int $new_download_id, int $switch_order_id |
stackless_edd_subscription_updated | int $subscription_id, after an admin, customer or gateway edit |
stackless_edd_subscription_deleted | int $subscription_id |
stackless_edd_subscriptions_payment_method_changed | SubscriptionData $subscription, string $gateway_id, $token_id |
A small example that logs every cancellation:
add_action('stackless_edd_subscription_status_changed', function ($subscription, $from, $to) {
if ($to === 'cancelled') {
error_log('Subscription #' . $subscription->get_id() . ' cancelled (was ' . $from . ')');
}
}, 10, 3);
To show a download’s subscription terms, ask apply_filters('stackless_edd_subscription_product_terms', null, int $download_id, ?int $price_id). Subscriptions answers period, interval, trial_length, trial_period, signup_fee (in the visitor’s currency) and times, or null for a price that isn’t a subscription.
Other Module Actions
| Action | Arguments |
|---|---|
stackless_edd_software_licensing_keys_created | int[] $license_ids, \EDD\Orders\Order $order |
stackless_edd_software_licensing_license_updated | int $license_id, array $data |
stackless_edd_software_licensing_site_activated | int $license_id, string $site, bool $is_local |
stackless_edd_software_licensing_site_deactivated | int $license_id, string $site_url |
stackless_edd_software_licensing_order_revoked | $order, $revoked |
stackless_edd_software_licensing_package_download | $license, int $download_id, $package, $version |
stackless_edd_custom_invoices_paid | \EDD\Orders\Order $order |
stackless_edd_request_quotes_accepted | int $quote_id, $issued (the issued invoice order) |
stackless_edd_payment_method_saved | string $gateway_id, string $token_id, int $user_id |
Contracts for Other Plugins
Help desks, CRMs and agents read Stackless through these. They’re kept stable: keys get added, never renamed or removed. Each returns its neutral default when the owning module is off.
| Contract | Owner | Returns |
|---|---|---|
stackless_edd_get_customer_invoices(int $user_id, string $email = '', array $args = []) | Custom Invoices | id, number, title, status (sent, overdue, paid, cancelled, refunded), status_label, open, total, currency, due_date, issued_at, admin_url, customer_url, pay_url |
stackless_edd_get_customer_quotes(int $user_id, string $email = '', array $args = []) | Request a Quote | id, number, title, status (requested, proposal, offered, expired, accepted, declined, withdrawn), status_label, open, total, currency, due_date, admin_url, customer_url |
apply_filters('stackless_edd_subscription_admin_url', '', $id) | Subscriptions | the subscription’s edit screen; $id is an ID or a SubscriptionData object |
apply_filters('stackless_edd_license_admin_url', '', int $license_id) | Software Licensing | the license’s screen under Stackless → Licenses |
apply_filters('stackless_edd_invoice_admin_url', '', int $invoice_id) | Custom Invoices | the invoice’s screen under Stackless → Invoices |
apply_filters('stackless_edd_license_product_terms', null, int $download_id, ?int $price_id) | Software Licensing | activation_limit (0 = unlimited), validity (lifetime, subscription, days), validity_days, version, requires_wp, tested_wp, requires_php, software_type (plugin, theme), or null |
do_action('stackless_edd_account_item_actions', string $type, int $id, $data) | Subscriptions and Software Licensing templates | fires inside each item’s actions area on the account page; $type is subscription (with the SubscriptionData object) or license (with the license array) |
Invoice and quote $args take open (bool, only open items) and number (1 to 100, default 20). Here’s a help desk button beside each license:
add_action('stackless_edd_account_item_actions', function ($type, $id, $data) {
if ($type === 'license') {
printf('<a class="button" href="%s">Get help</a>', esc_url(home_url('/support/?license=' . $id)));
}
}, 10, 3);
Multi-Currency Rules for Custom Code
- An order’s currency is fixed at checkout. Subscriptions and their renewal orders keep it.
- Runtime cart prices and cart fees are base-currency amounts. Multi-Currency converts them once, so never pre-convert them.
- If your code adds a surcharge to a cart item’s stored price, also return that base-currency surcharge from
stackless_edd_runtime_price_surcharge($surcharge, int $download_id, array $options). It starts as null. Return it unchanged for items you didn’t touch. - If your code scales the stored price, return the factor from
stackless_edd_runtime_price_ratio($ratio, int $download_id, array $options), such as 0.6 for 40% off. - Labels that show a base amount convert it with
stackless_edd_convert_amount(). - Wrap order creation during a storefront request in
do_action('stackless_edd_multi_currency_pause')anddo_action('stackless_edd_multi_currency_resume'). - CLI or staging scripts that need storefront prices:
add_filter('stackless_edd_multi_currency_is_storefront', '__return_true'), thendo_action('stackless_edd_multi_currency_reset_context').
Gateway Copies and Integrations
Polar, Paddle and Dodo Payments can run as several copies, one per provider account: the original ID plus <id>_copyN, where N is 2 to 10. Each copy has its own settings, webhook URL and orders.
| Hook | Type | Arguments |
|---|---|---|
stackless_edd_gateway_copy_families | filter | array $families; a gateway module declares itself copyable |
stackless_edd_gateway_copies_panel | filter | string $html, string $gateway_id |
stackless_edd_gateway_copy_added | action | string $copy_id, string $family_id |
stackless_edd_gateway_copy_removed | action | string $copy_id, string $family_id |
Before you sync a customer or order to an outside service, ask apply_filters('stackless_edd_skip_integrations', false, string $email). True means the address is on the store’s internal and test list from Extra Enhancements. Orders from those addresses carry the order meta _stackless_edd_internal_email set to yes.
Override Templates
Front-end and email markup comes from the plugin’s templates/ folder, one subfolder per module. stackless_edd_get_template() looks in your theme first, so copy a file to the same path under a stackless-edd folder in your theme or child theme:
wp-content/plugins/stackless-edd/templates/subscriptions/account/view-subscription.php
wp-content/themes/your-child-theme/stackless-edd/subscriptions/account/view-subscription.php
Some templates you might override:
account/account.php: the tabbed account pagesubscriptions/account/subscriptions.phpandsubscriptions/account/view-subscription.phpsubscriptions/emails/*.php: subscription email bodies, such asrenewal-invoice.phpandpayment-failed.phpsoftware_licensing/account-licenses.phpandsoftware_licensing/emails/licenses-table.phpcustom_invoices/invoice.phpandcustom_invoices/emails/invoice.phprequest_quotes/form.phpandrequest_quotes/proposal.phpwishlist/button.phpandwishlist/list.phpcurrency_selector/selector.phpandcheckout_builder/compact.php
To load a file from somewhere else entirely, use the stackless_edd_template filter (string $file, string $name, array $args). Overrides of view-subscription.php and account-licenses.php must keep their stackless_edd_account_item_actions_html() call, or other plugins’ buttons disappear.
Good to Know
- Other modules are reached only through these functions, hooks and contracts. Don’t create another module’s classes directly. Their internals can change between releases.
- The webhook and REST filters are listed in Webhooks and REST API, and the agent command filters in AI Agents.
- Checkbox settings save
yesorno, and options are namedstackless_edd_{module_id}_{key}. - Template overrides don’t update with the plugin. After an update, compare your copy with the plugin’s file, and check the changelog for what changed.
- The licensing client in
templates/software_licensing/client/is a file you copy into your own plugin, not a theme template.