WP Sell Services and WP Sell Services Pro fire 454 actions and filters between them.
This page covers the behavioural surface – services, orders, delivery, money, disputes, REST, and the Pro seams – each with its parameters and the source file that fires it.
Template hooks are on their own page. The ~110 wpss_before_* / wpss_after_* markup slots, the dashboard section hooks, and the display filters are documented next to the templates that fire them, in Template Overrides. Use that page when you want to inject markup rather than change behaviour.
Between the two, 377 of the 454 hooks are documented. The rest are internal and may change without notice – if you need one, open a support request and we will promote it rather than have you bind to a moving target.
Verify before you ship. Hook names and signatures on this page are checked against the 1.3.0 source. Some names in pre-1.3.0 documentation were never fired at all – if a callback of yours has silently stopped running, search this page for the hook name before assuming a regression.
bool $allow, int $order_id, string $new_status, string $old_status
OrderService.php
wpss_order_started
int $order_id
OrdersController.php:747
wpss_order_completed
int $order_id, object $order
OrderWorkflowManager.php:685
wpss_order_cancelled
int $order_id, int $user_id, string $reason
OrderService.php:427
wpss_order_disputed
int $order_id, int $opened_by, string $reason
OrdersController.php:670
wpss_order_message_created
int $message_id, int $order_id, int $user_id
OrdersController.php:406
wpss_order_requirements_submitted
int $order_id, array $requirements
OrdersController.php:839
wpss_after_status_change_notification
int $order_id, string $new_status, string $old_status
OrderWorkflowManager.php:638
wpss_send_requirements_reminder_email
int $order_id, int $reminder_num, string $message
OrderWorkflowManager.php:338
wpss_requirements_timeout
int $order_id, bool $auto_start
OrderWorkflowManager.php:472
Removed in 1.4.0: wpss_order_accepted, wpss_order_rejected,
wpss_order_delivered. The first two went when the dead accept / reject
order verbs were removed – they never had a real transition behind them.
wpss_order_delivered went when deliver was routed through
DeliveryService, which already fires its own, better-shaped hooks.
Rebind as follows:
Was
Use instead
wpss_order_accepted
wpss_order_paid, or wpss_order_status_changed – an order is accepted by being paid
wpss_order_rejected
wpss_order_cancelled
wpss_order_delivered
wpss_delivery_submitted ($delivery_id, $order_id) or wpss_delivery_accepted ($order_id)
All three are gone from the source, so a callback still bound to them runs
never – silently. Grep your integrations.
int $withdrawal_id, string $status, object $withdrawal
EarningsService.php:489
wpss_auto_withdrawal_created
int $withdrawal_id, int $vendor_id, float $amount
EarningsService.php:866
wpss_tip_order_created
int $tip_order_id, int $parent_order_id, int $customer_id, float $amount
TippingService.php:280
wpss_tip_sent
int $tip_txn_id, int $parent_order_id, int $vendor_id, int $customer_id, float $vendor_earnings, string $vendor_notes
TippingService.php:482
wpss_tip_order_created fires when the tip checkout is started; wpss_tip_sent
fires only once the tip is actually paid and credited. The first argument of
wpss_tip_sent is the wallet transaction id, not the tip order id, and the
amount passed is the vendor’s net earnings after commission – not the gross
tip. Tips are excluded from commission by default, so the two usually match.
A milestone is a sub-order of the parent order, so every hook passes both
ids: $milestone_id is the sub-order, $order_id is the parent. See
Sub-Order Pattern.
Hook
Parameters
File
wpss_milestone_proposed
int $milestone_id, int $order_id, int $vendor_id
MilestoneService.php:262
wpss_milestone_paid
int $milestone_id, int $order_id, int $vendor_id, int $customer_id, float $vendor_earnings
MilestoneService.php:411
wpss_milestone_submitted
int $milestone_id, int $order_id, int $vendor_id, int $customer_id
MilestoneService.php:483
wpss_milestone_approved
int $milestone_id, int $order_id, int $vendor_id, int $customer_id
MilestoneService.php:536
wpss_milestone_declined
int $milestone_id, int $order_id, int $customer_id
MilestoneService.php:591
wpss_extension_request_created
int $request_id, int $order_id, array $data (requested_by, extra_days, reason)
ExtensionRequestService.php:249
wpss_extension_request_approved
int $request_id, object $request
ExtensionRequestService.php:371
wpss_extension_request_rejected
int $request_id, object $request
ExtensionRequestService.php:455
Renamed in 1.3.0. The milestone lifecycle uses proposed and declined,
not created and rejected – see Milestone terminology.
wpss_milestone_created, wpss_milestone_rejected, wpss_extension_requested
and wpss_extension_approved were listed in earlier docs but are not fired
by the plugin. Callbacks bound to those names never run. Note also that
wpss_milestone_approved passes $vendor_id as its third argument, not an
amount – if you need the money, read it from the sub-order or hook
wpss_milestone_paid.
wpss_pro_upgrade_url (added 1.3.0) controls where every “Upgrade to Pro” call-to-action points — the admin upgrade screen and the in-wizard prompts. Point it at your own landing page or an in-site URL:
wpss_catalog_price_html (added 1.3.0) is the single seam for catalog price display. Base currency is authoritative for all stored amounts; this filter is where an add-on (such as the Pro display-currency hint) injects a converted, visitor-facing price without changing the stored value.
// Add custom fields to the service meta box in wp-admin
These four hooks form the contract that lets add-ons (such as Pro’s recurring billing) inject fields into the frontend Create/Edit Service wizard without patching core files.
wpss_wizard_service_data — seed extra keys into the wizard’s edit-form data model so they pre-fill when a vendor edits an existing service:
wpss_wizard_pricing_after — render extra fields after the Pricing step. Markup runs inside the wizard’s Alpine.js scope; bind inputs with x-model="data.your_key":
wpss_wizard_save_service_meta — persist your custom meta. Fires on both save-draft and publish (unlike wpss_service_wizard_saved, which fires on publish only):
This is the one seam for “send the buyer somewhere they can pay THIS
order.” Every tip, milestone phase, paid extension and accepted proposal
resolves its pay link through it – on the order page, in the dashboard, in the
REST checkout_url field, and in the emails the plugin sends.
Never rebuild that URL inline. Code that does is correct on the standalone rail
and broken on every other one.
Default (unfiltered):<checkout page>?pay_order={id}. The standalone
checkout understands that query arg and renders the single order.
Why it needs a filter at all: a cart-based rail has no concept of “pay this
existing order.” Appending ?pay_order=N to a WooCommerce checkout URL lands
the buyer on an empty cart with no way to pay and no error message – which
is exactly what every tip, milestone and extension link did in Woo mode before
1.4.0.
Who hooks it:
Rail
Hooks it?
What the buyer gets
Standalone
n/a (the default)
?pay_order=N on the plugin’s own checkout
WooCommerce
Yes – WCPayOrderResolver (Pro)
A native WC order-pay URL
EDD
No
The unfiltered default -> empty cart
FluentCart
No
The unfiltered default -> empty cart
SureCart
No
The unfiltered default -> empty cart
Two things to know before you hook it:
It is called speculatively. Rendering the order page asks for a pay URL
for every unpaid phase, and MilestoneService::propose() asks for one when
the phase is created. A resolver that has side effects (Woo’s creates a WC
order) will have them at render time, not at click time – so make yours
idempotent, as WCPayOrderResolver is.
It is not a security gate. The milestone lock-step guard lives on the
checkout side, not here. Returning a URL for a locked phase is possible and
the plugin’s own Woo resolver does it.
As of 1.2.1, currencies are driven by a single canonical registry (code → name, symbol, decimals). Every currency surface — price formatting, the settings dropdown, manual orders, decimal handling — reads from it, so overriding one filter updates all of them consistently.
wpss_currency_registry is the preferred, single-place override — add, remove, or adjust a currency (name / symbol / decimals) and every currency surface updates. Prefer it over the older per-surface currency filters (wpss_currency_symbols, wpss_currency_format, wpss_currencies):
// Register a custom currency and change USD's symbol
wpss_zero_decimal_currencies returns the codes rendered without minor units. It is derived from the registry (decimals === 0); filter it only when you need to force a currency into or out of zero-decimal formatting independently of its registry entry.
wpss_settings_currencies and wpss_manual_order_currencies narrow (or extend) the currency choices offered in the admin settings dropdown and the manual-order screen respectively — useful for restricting a store to a subset of the registry:
// Only allow USD and EUR to be selected in settings
wpss_realtime_settings — filter the resolved real-time/WebSocket connection settings before they are used. The $settings array includes: enabled, app_id, key, secret, host, cluster, port, use_tls. The secret field is server-only; it is never sent to the browser:
wpss_use_fullwidth_template — return false to keep the active theme’s normal page template (with sidebar) on the plugin’s pages instead of the sidebar-free full-width layout:
Unlike the EDD, FluentCart, and SureCart adapters, the WooCommerce adapter does
not fire its own namespaced lifecycle hooks. It reuses the core order hooks
instead, so code written against wpss_order_created /
wpss_order_status_changed works identically whether the sale came through
WooCommerce or standalone checkout.
Hook
Parameters
File
wpss_order_created
int $order_id, string $status
WCOrderProvider.php
wpss_order_status_changed
int $order_id, string $new_status, string $old_status
WCOrderProvider.php
wpss_max_order_quantity
int $max, int $service_id
WCCheckoutProvider.php
Earlier documentation listed wpss_woocommerce_adapter_init,
wpss_service_synced_to_wc_product, wpss_after_checkout_process and
wpss_service_to_wc_status_map. None of these are fired – use the core
order hooks above. To react to product sync, hook wpss_service_updated.
int $order_id, int $external_order_id, array $order_data
FluentCartOrderProvider.php:93
wpss_fluentcart_order_detail
object $order
FluentCartAccountProvider.php:384
wpss_fluentcart_product_created was removed in 1.6.1. FluentCart is a
payment rail, not a catalogue: the plugin no longer creates FluentCart
products, so there is no creation event to fire. A service is linked to an
existing FluentCart product instead.
SureCart integration removed in 1.6.1. Its four namespaced hooks
(wpss_surecart_adapter_init, wpss_surecart_order_created,
wpss_surecart_product_created, wpss_surecart_order_detail) no longer
exist. SureCart keeps products and prices as objects in its own cloud and
settles through webhooks, so it cannot act as a payment rail the way
WooCommerce, EDD and FluentCart do – charging an arbitrary amount would mean
creating a remote price object per order.
Tips, milestone phases and extension quotes are sub-orders: a real WPSS
order of their own, created against a parent order, that the buyer pays
separately. They are the one deliberate exception to the one-rail catalog rule
— the catalog checkout belongs entirely to the active platform, but a sub-order
has no cart and cannot go through it.
The single source for “pay THIS order.” Every surface that links a buyer to
a payment — dashboard timeline, order view, emails, notifications — must call
it. Never build a ?pay_order= link by hand; it is correct only on one rail.
A cart-based rail replaces the URL entirely. Pro’s WooCommerce implementation
(WCPayOrderResolver) creates — or reuses — a real WooCommerce order for the
sub-order and returns its native order-pay URL, so the link works from an email
days later with no cart session.
These are the two rails the sub-order payment path is built and tested against.
A platform that has not implemented wpss_pay_order_url inherits the standalone
URL, which is not the checkout that rail owns — so implement the filter for any
new platform the way WCPayOrderResolver does. Do not solve it by
re-enabling WPSS gateways alongside the platform’s own, which would break the
one-rail contract.
Why wpss_get_checkout_base_url() is not the answer
It returns the active rail’s checkout — WooCommerce’s under Woo. Sending a
buyer there for a sub-order lands them on an empty cart, because a sub-order was
never added to one. That is the trap the filter above exists to avoid, and the
reason browser Pay never reaches StandaloneCheckoutProvider::render_pay_order_checkout()
on a cart rail.