Skip to content

Hooks and Filters Reference

WP Sell Services overviewFree + ProGet WP Sell Services →

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.

// Actions execute code at specific points
add_action( 'wpss_order_status_changed', 'my_func', 10, 3 );
function my_func( $order_id, $new_status, $old_status ) {
// Your code here
}
// Filters modify data before it is used
add_filter( 'wpss_review_window_days', fn( $days ) => 14 );
Hook Parameters File
wpss_loaded Plugin $plugin src/Core/Plugin.php:303
wpss_adapter_initialized EcommerceAdapterInterface $adapter src/Integrations/IntegrationManager.php:136

wpss_loaded is the primary extension hook. All Pro features register here:

add_action( 'wpss_loaded', function( $plugin ) {
// Plugin is ready - register extensions
}, 10, 1 );
Hook Parameters File
wpss_service_created int $post_id, array $data src/Services/ServiceManager.php:188
wpss_service_updated int $service_id, array $data src/Services/ServiceManager.php:305
Filter Parameters File
wpss_pre_create_service array $data ServiceManager.php
wpss_pre_update_service array $data, int $service_id ServiceManager.php
wpss_before_service_deleted int $service_id src/Services/ServiceManager.php:342
wpss_service_meta_saved int $post_id, WP_Post $post src/Admin/Metaboxes/ServiceMetabox.php:913
wpss_rest_service_created int $service_id, WP_REST_Request $request src/API/ServicesController.php:751
wpss_rest_service_updated int $service_id, WP_REST_Request $request src/API/ServicesController.php:916
wpss_rest_service_deleted int $service_id, bool $force src/API/ServicesController.php:965
Hook Parameters File
wpss_service_approved int $service_id, string $notes src/Services/ModerationService.php:196
wpss_service_rejected int $service_id, string $reason src/Services/ModerationService.php:246
wpss_service_pending_moderation int $service_id src/Frontend/ServiceWizard.php:1970
Hook Parameters File
wpss_order_status_changed int $order_id, string $new_status, string $old_status src/Admin/Pages/ManualOrderPage.php:743
wpss_order_status_{status} int $order_id, string $old_status OrderService.php:197
wpss_order_created int $order_id, string $status src/functions/orders.php:1196
Filter Parameters File
wpss_pre_create_order array $order_data StandaloneOrderProvider.php
wpss_pre_order_status_change bool $allow, int $order_id, string $new_status, string $old_status OrderService.php
wpss_order_started int $order_id src/API/OrdersController.php:944
wpss_order_completed int $order_id, object $order src/Services/OrderWorkflowManager.php:705
wpss_order_cancelled int $order_id, int $user_id, string $reason src/Services/OrderWorkflowManager.php:804
wpss_order_disputed int $order_id, int $opened_by, string $reason src/API/OrdersController.php:1148
wpss_order_message_created int $message_id, int $order_id, int $user_id src/API/OrdersController.php:685
wpss_requirement_field_label string $label, string $key src/functions/orders.php:356
wpss_after_status_change_notification int $order_id, string $new_status, string $old_status src/Services/OrderWorkflowManager.php:666
wpss_send_requirements_reminder_email int $order_id, int $reminder_num, string $message src/Services/OrderWorkflowManager.php:493
wpss_requirements_timeout int $order_id, bool $auto_start src/Services/OrderWorkflowManager.php:628

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.

Filter Parameters File
wpss_pre_submit_delivery array $delivery_data, int $order_id DeliveryService.php
Hook Parameters File
wpss_delivery_submitted int $delivery_id, int $order_id src/Services/DeliveryService.php:151
wpss_delivery_accepted int $order_id src/Services/DeliveryService.php:206
wpss_revision_requested int $order_id, string $reason src/Services/DeliveryService.php:276
wpss_requirements_submitted int $order_id, array $field_data, array $attachments src/Services/RequirementsService.php:521
wpss_cancellation_requested int $order_id, int $user_id, string $reason, string $note src/Services/OrderService.php:1389
wpss_order_auto_refunded int $order_id, object $order, mixed $refund_result src/Services/OrderWorkflowManager.php:1591
wpss_new_order_message int $order_id, int $sender_id, string $content src/Services/ConversationService.php:351

These hooks fire during payment processing, gateway interactions, and checkout flow.

Hook Parameters File
wpss_standalone_adapter_init StandaloneAdapter $adapter src/Integrations/Standalone/StandaloneAdapter.php:156
wpss_standalone_checkout_processed int $order_id, array $order_data StandaloneCheckoutProvider.php:134
wpss_standalone_order_complete object $order src/Integrations/Standalone/StandaloneOrderProvider.php:741
wpss_order_paid int $order_id, string $transaction_id src/Integrations/Standalone/StandaloneOrderProvider.php:437
wpss_order_status_pending_requirements int $order_id, string $old_status src/Integrations/Standalone/StandaloneOrderProvider.php:428
wpss_payment_callback string $gateway_id src/Integrations/Standalone/StandaloneAdapter.php:335
Hook Parameters File
wpss_offline_multi_orders_created array $order_ids, int $customer_id src/Integrations/Gateways/OfflineGateway.php:740
wpss_offline_order_created int $order_id, object $order src/Integrations/Gateways/OfflineGateway.php:664
wpss_offline_order_paid int $order_id, string $transaction_id src/Integrations/Gateways/OfflineGateway.php:925
Hook Parameters File
wpss_stripe_webhook_received string $event_type, object $data, string $payload src/Integrations/Stripe/StripeGateway.php:660
wpss_stripe_refund_processed string $payment_intent_id, object $charge src/Integrations/Stripe/StripeGateway.php:1623
Hook Parameters File
wpss_paypal_refund_processed string $url, array $resource src/Integrations/PayPal/PayPalGateway.php:1054
Hook Parameters File
wpss_rest_offline_order_created int $order_id, object $order, string $gateway_id src/API/PaymentController.php:444
Filter Parameters File
wpss_stripe_payment_intent_args array $params, int $order_id, int $vendor_id src/Integrations/Stripe/StripeGateway.php:299
wpss_rest_create_payment_intent null, object $gateway, float $amount, string $currency, int $service_id, int $package_id, object $pay_order src/API/PaymentController.php:270
wpss_rest_confirm_payment null, object $gateway, string $payment_id, int $service_id, int $package_id, object $pay_order src/API/PaymentController.php:319
wpss_checkout_tax_rate float $tax_rate, int $vendor_id, int $service_id src/functions/money.php:831

wpss_stripe_payment_intent_args lets you modify Stripe PaymentIntent parameters before creation:

add_filter( 'wpss_stripe_payment_intent_args', function( $params, $order_id, $vendor_id ) {
$params['metadata']['custom_field'] = 'value';
return $params;
}, 10, 3 );

wpss_checkout_tax_rate lets you apply different tax rates per vendor or service:

add_filter( 'wpss_checkout_tax_rate', function( $rate, $vendor_id, $service_id ) {
// Apply 15% tax for services in a specific category
if ( has_term( 'consulting', 'wpss_service_category', $service_id ) ) {
return 15.0;
}
return $rate;
}, 10, 3 );

These hooks fire when services, requests, or users are deleted and related data is cleaned up. Use them for custom cleanup logic.

Hook Parameters File
wpss_before_cascade_delete_service int $service_id DataCascadeHandler.php:101
wpss_after_cascade_delete_service int $service_id src/Services/DataCascadeHandler.php:135
wpss_before_cascade_delete_request int $request_id src/Services/DataCascadeHandler.php:151
wpss_after_cascade_delete_request int $request_id src/Services/DataCascadeHandler.php:162
wpss_before_cascade_delete_user int $user_id src/Services/DataCascadeHandler.php:178
wpss_after_cascade_delete_user int $user_id src/Services/DataCascadeHandler.php:257
// Clean up custom data when a service is deleted
add_action( 'wpss_before_cascade_delete_service', function( $service_id ) {
global $wpdb;
$wpdb->delete( $wpdb->prefix . 'my_custom_table', [ 'service_id' => $service_id ] );
} );
Filter Parameters File
wpss_pre_vendor_register array $profile_data, int $user_id VendorService.php
wpss_vendor_profile_allowed_fields array $allowed_fields VendorService.php
wpss_vendor_pitch_stats array $stats src/functions/vendors.php:838
wpss_vendor_pitch_steps array $steps src/Frontend/Shortcodes.php:1349
Hook Parameters File
wpss_vendor_registered int $user_id, array $profile_data src/Services/VendorService.php:182
wpss_vendor_profile_updated int $user_id, array $filtered_data src/Services/VendorService.php:561
wpss_vendor_vacation_mode_changed int $user_id, bool $enabled, string $message src/Services/VendorService.php:612
wpss_vendor_tier_changed int $user_id, string $tier src/Services/VendorService.php:653
wpss_vendor_level_promoted int $user_id, string $new_level, string $current_level src/Services/SellerLevelService.php:377
wpss_vendor_level_updated int $user_id, string $level src/Services/SellerLevelService.php:274
wpss_vendor_status_updated int $vendor_id, string $status src/Services/VendorService.php:377
wpss_vendor_commission_updated int $vendor_id, float $rate src/Admin/Pages/VendorsPage.php:1405
wpss_vendor_contacted int $vendor_id, int $user_id, int $service_id, string $message, array $attachments src/Frontend/AjaxHandlers.php:2095
wpss_vendor_access_granted int $user_id src/Services/VendorService.php:442
wpss_vendor_access_revoked int $user_id src/Services/VendorService.php:494
Filter Parameters File
wpss_commission_base_amount float $base_amount, int $order_id, int $vendor_id CommissionService.php
Hook Parameters File
wpss_commission_recorded int $order_id, array $commission, int $vendor_id src/Services/CommissionService.php:297
wpss_withdrawal_requested int $withdrawal_id, int $vendor_id, float $amount src/Services/EarningsService.php:788
wpss_withdrawal_processed int $withdrawal_id, string $status, object $withdrawal src/Services/EarningsService.php:524
wpss_auto_withdrawal_created int $withdrawal_id, int $vendor_id, float $amount src/Services/EarningsService.php:1634
wpss_tip_order_created int $tip_order_id, int $parent_order_id, int $customer_id, float $amount TippingService.php:290
wpss_tip_sent int $tip_txn_id, int $parent_order_id, int $vendor_id, int $customer_id, float $vendor_earnings, string $vendor_notes src/Services/TippingService.php:490

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.

Filter Parameters File
wpss_pre_open_dispute array $dispute_data, int $order_id DisputeService.php
Hook Parameters File
wpss_dispute_opened int $dispute_id, int $order_id, int $opened_by, array $data src/Services/DisputeService.php:407
wpss_dispute_evidence_added int $dispute_id, int $user_id src/Services/DisputeService.php:546
wpss_dispute_status_changed int $dispute_id, string $status, string $old_status src/Services/DisputeService.php:842
wpss_dispute_resolved int $dispute_id, string $resolution, object $dispute, float $refund_amount src/Services/DisputeService.php:1043
wpss_dispute_response_submitted int $message_id, int $dispute_id, int $user_id src/Services/DisputeWorkflowManager.php:208
wpss_dispute_escalated int $dispute_id, string $reason, int $escalated_by src/Services/DisputeWorkflowManager.php:358
wpss_dispute_cancelled int $dispute_id, int $user_id, string $reason src/Services/DisputeWorkflowManager.php:524
Filter Parameters File
wpss_pre_create_review array $review_data, int $order_id ReviewService.php
Hook Parameters File
wpss_review_created int $review_id, int $order_id src/API/ReviewsController.php:462
wpss_review_reply_created int $review_id src/API/ReviewsController.php:621
wpss_buyer_request_created int $post_id, array $data src/Services/BuyerRequestService.php:121
wpss_buyer_request_updated int $request_id, array $data src/Services/BuyerRequestService.php:174
wpss_buyer_request_status_changed int $request_id, string $status, string $old_status src/Services/BuyerRequestService.php:645
wpss_request_converted_to_order int $order_id, int $request_id, int $proposal_id, object $request, object $proposal src/Services/BuyerRequestService.php:1182
wpss_proposal_submitted int $proposal_id, int $request_id, int $vendor_id, array $proposal_data src/Services/ProposalService.php:187
wpss_proposal_updated int $proposal_id, array $update_data src/Services/ProposalService.php:365
wpss_proposal_accepted int $proposal_id, object $proposal, object $request src/Services/BuyerRequestService.php:1124
wpss_proposal_rejected int $proposal_id, object $proposal, string $reason src/Services/ProposalService.php:458
wpss_proposal_withdrawn int $proposal_id, object $proposal src/Services/ProposalService.php:503
wpss_proposal_deleted int $proposal_id, object $proposal src/Services/ProposalService.php:895
wpss_proposal_status_updated int $proposal_id, string $status src/Services/ProposalService.php:593
wpss_buyer_request_deleted int $request_id src/Services/BuyerRequestService.php:1351
wpss_buyer_request_meta_saved int $post_id, WP_Post $post src/Admin/Metaboxes/BuyerRequestMetabox.php:349

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 src/Services/MilestoneService.php:262
wpss_milestone_paid int $milestone_id, int $order_id, int $vendor_id, int $customer_id, float $vendor_earnings src/Services/MilestoneService.php:337
wpss_milestone_submitted int $milestone_id, int $order_id, int $vendor_id, int $customer_id src/Services/MilestoneService.php:511
wpss_milestone_approved int $milestone_id, int $order_id, int $vendor_id, int $customer_id src/Services/MilestoneService.php:567
wpss_milestone_declined int $milestone_id, int $order_id, int $customer_id src/Services/MilestoneService.php:741
wpss_milestone_revision_requested int $milestone_id, int $parent_id, int $vendor_id, int $customer_id, string $reason src/Services/MilestoneService.php:686
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
wpss_extension_approved int $pay_order_id, int $parent_order_id, int $vendor_id, int $customer_id, float $vendor_earnings, int $extra_days, int $request_id Paid extension approved and the vendor credited. NET earnings, not gross. ExtensionOrderService.php

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 and wpss_extension_requested were listed in earlier docs but are not fired by the plugin. Callbacks bound to those names never run.

wpss_extension_approved is a different case: it does fire, with seven arguments, when a paid extension is approved and the vendor is credited. It was wrongly grouped with the three above. See the extensions table. 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.

These hooks fire in the WordPress admin area for order management, service meta, and settings pages.

Hook Parameters File
wpss_admin_order_actions object $order, string $status src/Admin/Admin.php:2805
wpss_gateway_cards Settings $settings src/Admin/Settings.php:2052
Filter Parameters File
wpss_service_meta_fields array $fields, int $post_id src/Admin/Metaboxes/ServiceMetabox.php:189
wpss_pro_upgrade_url string $url (default https://wpsellservices.com/) UpgradePage.php, ServiceWizard.php:961
wpss_docs_url string $url (default https://wpsellservices.com/docs/) src/Admin/Pages/UpgradePage.php:347

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:

add_filter( 'wpss_pro_upgrade_url', function( $url ) {
return home_url( '/go-pro/' );
} );

Added in 1.3.0. Role-based menu visibility lets you show or hide dashboard sections per user role. Gate a section programmatically with:

Filter Parameters File
wpss_can_access_dashboard_section bool $can_access, string $section, int $user_id MenuVisibility.php
// Hide the "Earnings" section from a custom role
add_filter( 'wpss_can_access_dashboard_section', function( $can, $section, $user_id ) {
if ( 'earnings' === $section && user_can( $user_id, 'my_limited_role' ) ) {
return false;
}
return $can;
}, 10, 3 );
Filter Parameters File
wpss_catalog_price_html string $html, float $amount, string $context src/functions/money.php:165

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
add_filter( 'wpss_service_meta_fields', function( $fields, $post_id ) {
$fields['custom_field'] = [
'label' => 'Custom Field',
'type' => 'text',
'value' => get_post_meta( $post_id, '_wpss_custom_field', true ),
];
return $fields;
}, 10, 2 );
Hook Parameters File
wpss_service_wizard_saved int $service_id, array $sanitized_data src/Frontend/ServiceWizard.php:1943
wpss_wizard_pricing_after WP_Post|null $service ServiceWizard.php
wpss_wizard_save_service_meta int $service_id, array $data ServiceWizard.php
Filter Parameters File
wpss_vendor_can_create_service bool $can_create, int $user_id src/API/ServicesController.php:1383
wpss_services_per_page int $per_page (default 12) src/Frontend/ServiceArchiveView.php:640
wpss_wizard_service_data array $data, int $service_id ServiceWizard.php
wpss_wizard_sanitize_service_data array $sanitized, array $raw ServiceWizard.php
// Prevent unverified vendors from creating services
add_filter( 'wpss_vendor_can_create_service', function( $allowed, $user_id ) {
if ( ! get_user_meta( $user_id, '_wpss_identity_verified', true ) ) {
return false;
}
return $allowed;
}, 10, 2 );

Wizard Extension Surface (Add-on Integration — 1.2.0)

Section titled “Wizard Extension Surface (Add-on Integration — 1.2.0)”

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:

add_filter( 'wpss_wizard_service_data', function( $data, $service_id ) {
$data['my_billing_cycle'] = get_post_meta( $service_id, '_wpss_billing_cycle', true ) ?: 'one_time';
return $data;
}, 10, 2 );

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":

add_action( 'wpss_wizard_pricing_after', function( $service ) {
?>
<div class="wpss-field-group">
<label><?php esc_html_e( 'Billing cycle', 'my-addon' ); ?></label>
<select x-model="data.my_billing_cycle">
<option value="one_time"><?php esc_html_e( 'One-time', 'my-addon' ); ?></option>
<option value="monthly"><?php esc_html_e( 'Monthly', 'my-addon' ); ?></option>
</select>
</div>
<?php
} );

wpss_wizard_sanitize_service_data — sanitize your injected keys from the untrusted client JSON payload before save:

add_filter( 'wpss_wizard_sanitize_service_data', function( $sanitized, $raw ) {
$allowed = [ 'one_time', 'monthly', 'yearly' ];
$sanitized['my_billing_cycle'] = in_array( $raw['my_billing_cycle'] ?? '', $allowed, true )
? $raw['my_billing_cycle']
: 'one_time';
return $sanitized;
}, 10, 2 );

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):

add_action( 'wpss_wizard_save_service_meta', function( $service_id, $data ) {
if ( isset( $data['my_billing_cycle'] ) ) {
update_post_meta( $service_id, '_wpss_billing_cycle', $data['my_billing_cycle'] );
}
}, 10, 2 );
Hook Parameters File
wpss_dashboard_section_before_content string $section, int $user_id src/Frontend/UnifiedDashboard.php:957
Filter Parameters File
wpss_dashboard_default_section string $section, int $user_id UnifiedDashboard.php
// Add a notice at the top of the earnings dashboard section
add_action( 'wpss_dashboard_section_before_content', function( $section, $user_id ) {
if ( 'earnings' === $section ) {
echo '<div class="wpss-notice">Minimum withdrawal is $50.</div>';
}
}, 10, 2 );

wpss_dashboard_default_section — the landing section when no section is in the URL. Defaults to sales for active vendors and orders for buyers:

// Always land on the messages section regardless of role
add_filter( 'wpss_dashboard_default_section', function( $section, $user_id ) {
return 'messages';
}, 10, 2 );
Filter Parameters File
wpss_add_service_to_cart bool $added, array $cart_item, object $adapter src/Frontend/AjaxHandlers.php:2331
wpss_pay_order_url_lookup string $url, int $order_id, ?object $order src/functions/urls.php
wpss_ensure_pay_order string $url, int $order_id src/functions/urls.php

The pay-order seam: wpss_pay_order_url_lookup + wpss_ensure_pay_order

Section titled “The pay-order seam: wpss_pay_order_url_lookup + wpss_ensure_pay_order”

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.

// Read: where the buyer will pay. Safe once per row on any list or page.
$url = wpss_get_pay_order_url( $order_id ); // src/functions/urls.php
// Write: the buyer is paying now. The rail creates its store order here.
$url = wpss_ensure_pay_order( $order_id ); // src/functions/urls.php

Default (unfiltered): /<checkout page>/pay/{id}/ on the standalone rail (?pay_order={id} without pretty permalinks). On a store rail the read helper returns <WPSS checkout page>?pay_order={id}, a dormant page that resolves through wpss_ensure_pay_order() on template_redirect - so the store order is created when the buyer clicks, not when a list is rendered.

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.

This table is the single source of truth for which rails can pay one existing order. Every other page links here rather than restating it – the copies that used to live in the REST, WooCommerce and coverage pages each drifted separately, and three of them were still saying FluentCart had no pay-order rail after it gained one.

Rail Can pay one existing order? Resolver Caveats
Standalone Yes, natively none needed – /pay/N/ on the plugin’s own checkout Lock-step milestone order is enforced here
WooCommerce Yes WCPayOrderResolver [PRO] Creates or reuses a wc-pending WC order; lock-step is not enforced on the WC order-pay URL
FluentCart Yes FluentCartPayOrderResolver [PRO] Beta rail, off until wpss_pro_beta_rails returns true; mints a FluentCart order carrying one fee line; lock-step is not enforced on its checkout URL
EDD No none No Pay button at all – wpss_can_pay_single_order() is false, so tips, phases and extensions cannot be charged on this rail

Both Pro resolvers hook the read and the write half: wpss_pay_order_url_lookup rebuilds a URL from what the WPSS row already knows, wpss_ensure_pay_order creates the store order when the buyer clicks.

Two things to know before you hook it:

  1. The read filter must not write. wpss_get_pay_order_url() runs once per unpaid row on every order list, order page, REST shape and email. wpss_pay_order_url_lookup may only return a URL the rail already knows (Pro stores the WC order id and key on the WPSS row when it creates the order). Creating the store order belongs in wpss_ensure_pay_order, which runs on click and on the writes that create a tip, phase or extension. Make it idempotent, as WCPayOrderResolver is.
  2. It is not a security gate. The milestone lock-step guard lives on the checkout side, not here. wpss_can_pay_single_order() is true when a rail hooks either filter.
// Point a custom rail at its own pay page.
add_filter( 'wpss_pay_order_url_lookup', function ( string $url, int $order_id, ?object $order ): string {
$known = $order->meta['my_gateway_invoice'] ?? '';
return $known ? my_gateway_pay_page_url( $known ) : $url; // read only
}, 10, 3 );
add_filter( 'wpss_ensure_pay_order', function ( string $url, int $order_id ): string {
$order = wpss_get_order( $order_id );
if ( ! $order || 'paid' === ( $order->payment_status ?? '' ) ) {
return $url;
}
return my_gateway_create_or_reuse_invoice( $order ); // the write
}, 10, 2 );

See WooCommerce Checkout for the full WC implementation.

These filters let you customize outgoing email content without modifying templates.

Filter Parameters File
wpss_email_from_name string $from_name src/Services/EmailService.php:2106
wpss_email_header_vars array $template_vars, string $type src/Services/EmailService.php:2082
wpss_vendor_pending_email_content string $content, object $user, string $platform_name src/Services/NotificationService.php:1570
// Change the "From" name on all marketplace emails
add_filter( 'wpss_email_from_name', function( $name ) {
return 'DesignHub Marketplace';
} );
// Customize the "vendor application received" email content
add_filter( 'wpss_vendor_pending_email_content', function( $content, $user, $platform ) {
$content .= '<p>We review applications within two business days.</p>';
return $content;
}, 10, 3 );
Filter Parameters File
wpss_pre_send_message array $message_data, int $conversation_id ConversationService.php
wpss_validate_add_to_cart bool $valid, int $service_id, int $package_id, int $user_id CartController.php
wpss_cart_item_data array $cart_item, int $service_id, int $package_id CartController.php
wpss_email_subject string $subject, string $type, string $to EmailService.php
wpss_search_query_args array $query_args, string $query SearchService.php
wpss_template_args array $args, string $template_name functions.php
Hook Parameters File
wpss_message_sent object $message, object $conversation src/Services/ConversationService.php:342
wpss_notification_created int $notification_id, int $user_id, string $type, array $data src/Services/NotificationService.php:112
wpss_portfolio_item_created int $item_id, int $vendor_id, array $data src/Services/PortfolioService.php:199
wpss_portfolio_item_updated int $item_id, array $data src/Services/PortfolioService.php:294
wpss_portfolio_item_deleted int $item_id, object $item src/Services/PortfolioService.php:344
wpss_settings_tab_{tab} (none) Settings.php:985
wpss_advanced_settings_sections (none) src/Admin/Settings.php:2173
Filter File Default
wpss_ecommerce_adapters IntegrationManager.php:67 Standalone only (Pro adds WooCommerce, EDD, FluentCart)
wpss_payment_gateways Plugin.php:813 Test gateway (debug)
wpss_wallet_providers [PRO] Plugin.php:825 Empty
wpss_storage_providers [PRO] Plugin.php:837 Empty
wpss_email_providers [PRO] Plugin.php:849 Empty
wpss_analytics_widgets [PRO] Plugin.php:861 Empty
Filter File Free Default
wpss_service_max_packages ServiceWizard.php:116 3
wpss_service_max_gallery ServiceWizard.php:126 4
wpss_service_max_videos ServiceWizard.php:136 1
wpss_service_max_extras ServiceWizard.php:146 3
wpss_service_max_faq ServiceWizard.php:156 5
wpss_service_max_requirements ServiceWizard.php:166 5
Filter Parameters File
wpss_format_price $formatted, $price, $currency src/functions/money.php:57
wpss_currency $currency src/functions/money.php:1089
wpss_platform_name $platform_name src/functions/misc.php:36
wpss_is_vendor $is_vendor, $user_id src/functions/vendors.php:147
wpss_order_number_prefix $prefix (default 'WPSS-') src/Database/Repositories/OrderRepository.php:92
wpss_currency_symbols $symbols src/functions/money.php:1124
wpss_currency_format $format, $symbol, $currency src/functions/money.php:1151
wpss_currencies $currencies src/functions/money.php:1946
wpss_order_statuses $statuses src/functions/orders.php:153
wpss_max_upload_size $upload_max src/functions/misc.php:126
wpss_allow_late_requirements_submission $allow_late src/functions/orders.php:820
wpss_wallet_manager null src/functions/money.php:1968

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.

Filter Parameters File
wpss_currency_registry array<string, array{name:string, symbol:string, decimals:int}> $registry src/functions/money.php:1925
wpss_currency_decimals int $decimals, string $currency src/functions/money.php:205
wpss_zero_decimal_currencies string[] $codes src/functions/money.php:1051
wpss_settings_currencies array $currencies src/Admin/Settings.php:3998
wpss_manual_order_currencies array $currencies src/Admin/Pages/ManualOrderPage.php:948

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
add_filter( 'wpss_currency_registry', function( $registry ) {
$registry['XCD'] = [
'name' => 'East Caribbean Dollar',
'symbol' => 'EC$',
'decimals' => 2,
];
$registry['USD']['symbol'] = 'US$';
return $registry;
} );

wpss_currency_decimals overrides the decimal places for a specific currency at format time (for example, to render USD without minor units):

add_filter( 'wpss_currency_decimals', function( $decimals, $currency ) {
return 'USD' === $currency ? 0 : $decimals;
}, 10, 2 );

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
add_filter( 'wpss_settings_currencies', function( $currencies ) {
return array_intersect_key( $currencies, array_flip( [ 'USD', 'EUR' ] ) );
} );
Filter Parameters File
wpss_get_template_part $template, $slug, $name src/functions/templates.php:125
wpss_get_template $template, $template_name, $args src/functions/templates.php:176
wpss_locate_template $template, $template_name, $template_path src/Frontend/TemplateLoader.php:568
wpss_dashboard_section_template $template_path, $section src/functions/urls.php:415
Filter Parameters File
wpss_vendor_slug $slug (default 'provider') Plugin.php, functions.php
wpss_service_order_slug $slug (default 'service-order') Plugin.php, functions.php
wpss_checkout_slug $slug (default 'service-checkout') StandaloneAdapter.php
wpss_service_slug $slug (default 'service') src/PostTypes/ServicePostType.php:424
wpss_buyer_request_slug $slug (default 'buyer-request') src/PostTypes/BuyerRequestPostType.php:114
wpss_service_post_type_args $args src/PostTypes/ServicePostType.php:346
wpss_service_tag_args $args src/PostTypes/ServicePostType.php:408
wpss_service_category_taxonomy_args $args ServiceCategoryTaxonomy.php:118
wpss_service_tag_taxonomy_args $args ServiceTagTaxonomy.php:103
wpss_buyer_request_post_type_args $args src/PostTypes/BuyerRequestPostType.php:98
Filter Parameters File
wpss_order_status_transitions $transitions, $from, $to src/Services/OrderService.php:1025
wpss_commission_rate $rate, $order, $vendor_id, $service_id src/Services/CommissionService.php:362
wpss_proposal_order_revisions $revisions, $proposal, $request src/Services/BuyerRequestService.php:1006
wpss_max_order_quantity $max src/Frontend/SingleServiceView.php:960
wpss_api_controllers $controllers src/API/API.php:183
wpss_api_public_settings $settings src/API/API.php:659
wpss_batch_max_requests $max (default 25) src/API/API.php:1363
wpss_api_cors_origins $origins src/API/API.php:1436
wpss_settings_tabs $tabs src/Admin/Settings.php:239
wpss_blocks $blocks src/Blocks/BlocksManager.php:95
wpss_rate_limits $limits, $action src/Core/RateLimiter.php:289
Filter Parameters File
wpss_realtime_settings array $settings RealtimeService.php
wpss_review_window_days $days src/Services/ReviewService.php:519
wpss_auto_approve_reviews $auto_approve (default true) src/API/ReviewsController.php:426
wpss_vendor_registration_open $open (default true) src/API/VendorsController.php:599
wpss_auto_approve_vendors $auto_approve (default true) src/Services/VendorService.php:112
wpss_delivery_allowed_file_types $allowed src/functions/files.php:373
wpss_requirements_allowed_file_types $allowed src/functions/files.php:383
wpss_withdrawal_methods $methods src/API/EarningsController.php:705
wpss_search_results $results, $query, $args SearchService.php:121
wpss_search_suggestions $suggestions, $query src/Services/SearchService.php:482
wpss_related_services_args $args, $service src/Frontend/SingleServiceView.php:794
wpss_cart_checkout $result, $cart, $user_id, $payment_method src/API/CartController.php:435
wpss_seller_levels $levels src/API/SellerLevelsController.php:263
wpss_rest_service_data $data, $service, $request src/API/ServicesController.php:1499
wpss_rest_order_data $data, $order, $request OrdersController.php
wpss_rest_review_data $data, $review, $request ReviewsController.php
wpss_rest_vendor_data $data, $vendor, $request VendorsController.php
wpss_can_access_dashboard_section $allowed, $section, $user_id src/Frontend/UnifiedDashboard.php:478
wpss_dashboard_sections $sections, $user_id, $is_vendor src/Frontend/UnifiedDashboard.php:577
wpss_dashboard_section_titles $titles src/Frontend/UnifiedDashboard.php:896

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:

add_filter( 'wpss_realtime_settings', function( $settings ) {
// Force a specific cluster at runtime
$settings['cluster'] = 'eu';
return $settings;
} );
Filter Parameters File
wpss_use_fullwidth_template bool $use src/Frontend/TemplateLoader.php:292 (and :332)
wpss_fullwidth_page_keys string[] $page_keys src/Frontend/TemplateLoader.php:338

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:

add_filter( 'wpss_use_fullwidth_template', '__return_false' );

wpss_fullwidth_page_keys — control which mapped plugin pages render full-width. Default: ['dashboard', 'cart', 'checkout', 'become_vendor', 'registration']:

// Remove the cart page from full-width treatment
add_filter( 'wpss_fullwidth_page_keys', function( $keys ) {
return array_diff( $keys, [ 'cart' ] );
} );
Filter Parameters File
wpss_service_schema $schema, $service_id src/SEO/SchemaMarkup.php:202
wpss_service_list_schema $schema src/SEO/SchemaMarkup.php:242
wpss_category_schema $schema, $term src/SEO/SchemaMarkup.php:301
wpss_person_schema $schema, $user_id src/SEO/SchemaMarkup.php:354
wpss_vendor_page_schema $schema, $user_id src/SEO/SchemaMarkup.php:401
wpss_organization_schema $schema src/SEO/SchemaMarkup.php:432
wpss_open_graph_data $data, $service_id src/SEO/SEO.php:270
wpss_sitemap_post_types $post_types src/SEO/SEO.php:334
wpss_breadcrumbs $breadcrumbs, $service_id src/SEO/SEO.php:402
wpss_notification_email_content $content, $subject, $user_id, $data src/Services/NotificationService.php:2013
wpss_vendor_welcome_email_content $content, $user, $platform_name src/Services/NotificationService.php:1521
wpss_admin_vendor_notification_content $content, $user src/Services/NotificationService.php:1623

These hooks are fired exclusively by the Pro plugin and require an active Pro license.

Unlike the EDD and FluentCart 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.

Hook Parameters File
wpss_edd_adapter_init EDDAdapter $adapter src/Integrations/EDD/EDDAdapter.php:166
wpss_edd_service_purchased ServiceItem $item, int $order_id src/Integrations/EDD/EDDOrderProvider.php:119
wpss_edd_services_processed int $order_id, ServiceItem[] $items src/Integrations/EDD/EDDOrderProvider.php:149
wpss_edd_order_record_created int $record_id, ServiceItem $item, int $order_id src/Integrations/EDD/EDDOrderProvider.php:410
wpss_edd_service_meta_saved int $product_id src/Integrations/EDD/EDDProductProvider.php:237
wpss_edd_service_checkout_processed int $order_id, int $download_id, array $service_data, int $index EDDCheckoutProvider.php:222
Hook Parameters File
wpss_fluentcart_adapter_init FluentCartAdapter $adapter src/Integrations/FluentCart/FluentCartAdapter.php:213
wpss_fluentcart_order_created int $order_id, int $external_order_id, array $order_data src/Integrations/FluentCart/FluentCartOrderProvider.php:110
wpss_fluentcart_order_detail object $order src/Integrations/FluentCart/FluentCartAccountProvider.php:366

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.0. 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.

Hook Parameters File
wpss_terawallet_recharged int $transaction_id, float $amount TeraWalletProvider.php:203
wpss_mycred_balance_changed int $user_id, float $amount, string $reference MyCredProvider.php:253
Hook Parameters File
wpss_razorpay_refund_processed string $payment_id, array $refund src/Integrations/Razorpay/RazorpayGateway.php:885
Hook Parameters File
wpss_pro_connect_payout_paid string $payout_id, string $account_id, float $amount, string $currency src/StripeConnect/ConnectWebhookHandler.php:253
wpss_pro_connect_payout_failed string $payout_id, string $account_id, string $failure_code, string $failure_message src/StripeConnect/ConnectWebhookHandler.php:294
wpss_pro_connect_transfer_created string $transfer_id, string $account_id, float $amount, string $currency src/StripeConnect/ConnectWebhookHandler.php:335
Hook Parameters File
wpss_recurring_renewal_order_created int $new_order_id, int $subscription_id, object $subscription src/RecurringServices/RecurringOrderFactory.php:139
wpss_recurring_payment_failed int $subscription_id, object $subscription RecurringWebhookHandler.php:191
wpss_recurring_subscription_cancelled int $subscription_id, object $subscription RecurringWebhookHandler.php:229
Hook Parameters File
wpss_analytics_init AnalyticsManager $manager src/Analytics/AnalyticsManager.php:103
Hook Parameters File
wpss_gateway_settings_{$gateway_id} (none) Pro.php:1057
Filter Parameters File
wpss_edd_cart_item_data $cart_item_data, $product_id, $variation_id EDDCheckoutProvider.php:56
wpss_edd_validate_add_to_cart $valid, $product_id, $quantity EDDCheckoutProvider.php:97
wpss_edd_thankyou_redirect $redirect, $order_id EDDCheckoutProvider.php:251
wpss_edd_can_access_vendor_dashboard $can_access, $user_id src/Integrations/EDD/EDDAccountProvider.php:483
Filter Parameters File

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.

wpss_get_pay_order_url( int $order_id ): string

Section titled “wpss_get_pay_order_url( int $order_id ): string”

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.

$url = wpss_get_pay_order_url( $order_id );

wpss_pay_order_url_lookup and wpss_ensure_pay_order (filters)

Section titled “wpss_pay_order_url_lookup and wpss_ensure_pay_order (filters)”
apply_filters( 'wpss_pay_order_url_lookup', string $url, int $order_id, ?object $order ); // read
apply_filters( 'wpss_ensure_pay_order', string $url, int $order_id ); // write

A cart-based rail replaces the URL entirely. Pro’s WooCommerce implementation (WCPayOrderResolver) creates — or reuses — a real WooCommerce order for the sub-order in wpss_ensure_pay_order and returns its native order-pay URL, so the link works from an email days later with no cart session. Its wpss_pay_order_url_lookup rebuilds that URL from the id and key stored on the WPSS row, so lists and pages never load or create a WooCommerce order.

See Pay-order support by rail above – the one table, kept beside the filters it describes.

A platform that has not implemented wpss_ensure_pay_order gets no Pay button at all (wpss_can_pay_single_order() is false) — so implement the filters 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

Section titled “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.

Filter Parameters File
wpss_app_token_lifetime array{idle:int, absolute:int} $lifetime functions/misc.php
wpss_token_recovery_routes array<int,string> $routes API/AppTokenGuard.php

App tokens expire 30 days after last use or 90 days after issue, whichever comes first. Returning 0 for either key disables that limit; disabling both restores the pre-1.6.0 behaviour of tokens that never expire, which is what that release was filed to end.

// A 7-day idle window for a high-security marketplace.
add_filter( 'wpss_app_token_lifetime', function ( array $lifetime ): array {
$lifetime['idle'] = 7 * DAY_IN_SECONDS;
return $lifetime;
} );

wpss_token_recovery_routes lists the routes reachable with an expired token in the Authorization header - by default /auth/login, /auth/register and /auth/forgot-password.

Do not add a route that reads the current user. WordPress 401s the whole request on a failed application password, so without this carve-out an app that attaches its stored token to every request could never reach the login route to replace it. These three take their credentials from the request body and grant nothing on their own, which is what makes them safe to open.

Filter Parameters File
wpss_web_login_lock bool $enabled Core/Plugin.php

Five wrong passwords for one account inside 15 minutes lock it for 15 minutes, on the website sign-in form and on POST /auth/login alike - one counter, keyed on the resolved account rather than on the address the attempts came from. A successful sign-in clears it, and the 15 minutes always runs down, so an administrator cannot be held out indefinitely.

Return false to leave wp-login.php alone on a site whose security plugin already limits failed sign-ins. The counter keeps running either way, so the app’s own lockout - 423 wpss_account_locked - is unaffected.

// A dedicated security plugin owns the login form on this site.
add_filter( 'wpss_web_login_lock', '__return_false' );
Filter Parameters File
wpss_member_bypasses_limits bool $bypasses, int $user_id functions/vendors.php
wpss_vendor_can_create_service bool $can_create, int $vendor_id two gates, see below

Administrators bypass vendor selling limits by default, so a site owner seeding demo content or building services for a client does not meet their own paywall.

// Meter administrators too.
add_filter( 'wpss_member_bypasses_limits', '__return_false' );

wpss_vendor_can_create_service has TWO gates on it, and this catches people out: this plugin enforces a per-profile maximum at priority 10, and Pro’s plan enforcer runs at priority 20. If you are testing whether a member may create a service, run the filter - calling either class directly can return true while the filter answers false.

Filter Parameters File
wpss_skip_message_email_when_online bool $skip, int $recipient_id, bool $enabled functions/notifications.php
wpss_presence_window int $seconds functions/notifications.php
wpss_messages_per_page int $per_page templates/dashboard/sections/messages.php

There is no settings screen for the presence behaviour - it is on by default and adjusted here.

Filter Parameters File
wpss_video_thumbnail_cache_ttl int $ttl, string $video_url functions/services.php
wpss_gallery_image_size string $size, int $service_id partials/service-gallery.php
wpss_sticky_top_offset int $offset Frontend/Frontend.php

A video thumb uses the embed provider’s own poster frame, fetched through oEmbed and cached for a week. That call is an HTTP request to the provider, so if you shorten the TTL, shorten it deliberately - an uncached lookup puts a third-party round trip in front of every visitor.

Use wpss_sticky_top_offset when a theme has its own sticky header that the plugin’s measurement cannot see.

Filter Parameters File
wpss_category_terms_limit int $limit functions/services.php

Category choosers cap at 200 terms. The helper wpss_group_category_terms() turns a flat term list into parents each carrying their children, and is what every single-dropdown chooser in the plugin uses - reuse it rather than grouping by hand, or the two will drift as they did before 1.6.0.

Orphans - a child whose parent is missing because hide_empty dropped it - are promoted to top level rather than discarded, so a category with services in it always stays reachable.