Skip to content
on all Pro plansyearly & lifetime licencesClaim nowLaunch offer

Developer hooks

The stable filter and action surface for extending B2B Suite — signatures, parameters, and working examples for every supported hook.

Everything on this page is a supported extension point. You should never need to edit a plugin file.

Before you start

Where the code goes. A small custom plugin is better than functions.php — your customisations then survive a theme change. Anything in this document can go straight into either.

Prefixes. Core hooks are smb2b_ and work whenever the free plugin is active. Pro hooks are smb2bpro_ and need B2B Suite Pro. A handful of core filters are marked Pro-aware: they live in the free plugin specifically so Pro — or your code — can extend core behaviour, and they are safe to use either way.

Stability. The hooks listed here are supported across minor versions. Undocumented apply_filters / do_action calls inside the plugin may change without notice. If you need an extension point that is not here, ask and we will consider promoting it.

Namespaces. SoftminalB2B\ for core, SoftminalB2BPro\ for Pro.

Guard for Pro if your code might run on a site without it:

if (apply_filters('smb2b_is_pro', false)) {
    // Pro-only wiring here.
}

Which hook do I need?

I want to… Use
Change the final price a buyer pays smb2b_resolved_price
Bust my own cache when pricing changes smb2b_pricing_data_changed
Tell B2B which vendor owns a product smb2b_product_vendor_id
Price a product by its vendor’s rules smb2b_pricing_level_vendor
Give one buyer their own price cache smb2b_price_user_salt
Scope admin writes to a single vendor smb2b_vendor_context
Hide prices from more people smb2b_hide_prices
Change the “log in to see prices” text smb2b_hidden_price_message
Add a field to the trade registration form smb2b_registration_fields
Sync approved buyers to a CRM smb2b_registration_approved
Add a line or note to the PDF invoice smb2b_invoice_data
Ship my own invoice design smb2b_invoice_templates
Save my own option through the settings screen smb2b_editable_settings
Run a daily job alongside the plugin’s smb2b_housekeeping
React when a buyer↔merchant message is sent smb2b_message_sent
Show a rich context card on a conversation smb2b_conversation_anchor
Hold certain orders for approval smb2bpro_approval_hold_reason
Push ledger entries to accounting software smb2bpro_ledger_posted
React to any quote transition smb2bpro_quote_*
Hide extra categories from some buyers smb2bpro_hidden_category_ids

Pricing

smb2b_resolved_price

apply_filters('smb2b_resolved_price', ResolvedPrice $resolved, array $request): ResolvedPrice

The last word on a product’s price for a given buyer, applied everywhere a price is resolved — catalogue, cart, tier tables, the Price Explainer. It runs on cache hits as well as fresh resolutions, so your override is never skipped and you never have to think about the pricing cache.

Parameter Type Notes
$resolved ResolvedPrice The resolved price plus the trail of levels considered
$request array product_id, variation_id, qty, user_id, group_id, company_id

Return the original $resolved to leave it alone, or a new one to override. Anything else is ignored and the original is kept — use withPrice() rather than constructing a ResolvedPrice yourself, so the trail stays intact.

add_filter('smb2b_resolved_price', function ($resolved, $request) {
    // An extra 5% for one strategic account, on top of whatever rule won.
    if ($request['company_id'] === 42) {
        return $resolved->withPrice($resolved->price() * 0.95);
    }

    return $resolved;
}, 10, 2);

A second example — round every trade price down to the nearest whole unit, which trade customers often prefer on invoices:

add_filter('smb2b_resolved_price', function ($resolved, $request) {
    if ($request['group_id'] === null) {
        return $resolved; // retail, leave it alone
    }

    return $resolved->withPrice(floor($resolved->price()));
}, 20, 2);

smb2b_pricing_data_changed

do_action('smb2b_pricing_data_changed'): void

Fires whenever a pricing input changes — a rule, a group, or a company membership created, updated or deleted. There is no payload; treat it as “prices may differ now”.

add_action('smb2b_pricing_data_changed', function () {
    my_cache_flush('catalog-prices');
    wp_cache_delete('my_price_table', 'my_plugin');
});

smb2b_quantity_rule_for_product — Pro-aware

apply_filters(
    'smb2b_quantity_rule_for_product',
    ?QuantityRule $rule,
    int $productId,
    ?int $variationId,
    ?int $groupId,
    int $userId
): ?QuantityRule

Supply a minimum/maximum/pack-size rule from a broader scope when no product-specific rule matched. Core resolves only the product scope; Pro uses this filter to add category rules. Precedence is preserved by convention: if $rule is already set, a product-specific rule won — return it untouched.

add_filter('smb2b_quantity_rule_for_product', function ($rule, $productId, $variationId, $groupId, $userId) {
    if ($rule) {
        return $rule;
    }

    return my_category_quantity_rule_for($productId, $groupId);
}, 10, 5);

smb2b_pricing_level_{level} — Pro-aware

Injects an entire precedence level into the resolver walk. {level} is the level key (quote, customer, company, taxonomy, vendor). This is how Pro adds its levels — and how the Dokan bridge adds vendor — without patching core, and it is the advanced seam — read Pricing\Resolver::levels() before using it. The locked precedence ladder is customer > company > product > category > tag > vendor > global; see Marketplace / multivendor for the vendor level in context.

smb2b_pricing_context_company / smb2b_pricing_context_user — Pro-aware

Supply the company (or the effective user) that pricing should resolve for. Pro’s Companies module answers the first with ['company_id' => int, 'group_id' => int|null]; view-as-customer uses the second.

// Price everyone in a legacy CRM account as if they were company 7.
add_filter('smb2b_pricing_context_company', function ($context, $userId) {
    if ($context !== null) {
        return $context;
    }

    $legacyId = get_user_meta($userId, 'legacy_account', true);

    return $legacyId ? ['company_id' => 7, 'group_id' => null] : null;
}, 10, 2);

smb2b_max_groups — Pro-aware

The cap on customer groups. Free returns a finite number; Pro filters it to 0, which means unlimited.

smb2b_rule_set_bulk_threshold

The rule count above which the resolver switches from bulk-loading rules to querying per product. Tune only if you are profiling a very large catalogue.

smb2b_skip_price_filters

Return true to leave WooCommerce’s native prices untouched for the current request — useful in an export or a REST context where you want list prices. Use sparingly: it disables all B2B pricing while active.

add_filter('smb2b_skip_price_filters', function ($skip) {
    return $skip || (defined('DOING_CRON') && DOING_CRON && my_is_feed_generation());
});

Marketplace / multivendor

These four core seams are what the B2B Suite for Dokan bridge hooks so that vendor-aware pricing, invoices and approvals work without a shred of marketplace knowledge leaking into core. On a single-merchant store they are completely inert — no vendor ever resolves, so nothing changes and there is no cost. They also mean a bridge for another marketplace (WCFM, MVX) is a matter of answering these filters, not patching the plugin.

You only need this section if you are writing a marketplace bridge or your own vendor logic; the shipped Dokan bridge already wires all four.

smb2b_product_vendor_id

apply_filters('smb2b_product_vendor_id', int $vendorId, int $productId, ?int $variationId): int

The vendor (seller) WordPress user id that owns a product. Core defaults to the product’s post_author — how Dokan and WholesaleX both identify the seller — and resolves a variation to its parent product first. Return 0 for “no vendor” (an admin-owned product on a marketplace, or any product on a normal store). A bridge for a marketplace that stores ownership elsewhere overrides this to plug in its own resolution.

Read it anywhere through Pricing\ProductVendor::for($productId, $variationId) rather than calling apply_filters yourself.

// Point B2B at WCFM's store mapping instead of post_author.
add_filter('smb2b_product_vendor_id', function ($vendorId, $productId) {
    return wcfm_get_store_id_for_product($productId) ?: $vendorId;
}, 10, 2);

smb2b_pricing_level_vendor — Pro-aware

The vendor level of smb2b_pricing_level_{level}. It sits below any product, category or tag rule and above the store-wide global band, per the locked ladder customer > company > product > category > tag > vendor > global. Core ships the level but has no rule source for it, so the level is walked only when Pro is active and simply passes through untouched otherwise — the Dokan bridge answers this filter to price a product by its owning vendor’s catalogue rules.

Return ['price' => float, 'rule_id' => int|null, 'detail' => string] to win the level, or the unchanged $match to decline it. This is the advanced seam — read Pricing\Resolver::levels() and proLevels() first.

smb2b_price_user_salt

apply_filters('smb2b_price_user_salt', int $salt, Context $context): int

Forces a per-buyer price-cache salt. Return the buyer’s user id to give that buyer their own cache entries — across both the resolver cache and WooCommerce’s variation-prices hash, so one lever isolates both. The default is 0: everyone in the same group/company segment shares a cache entry, which is what keeps pricing fast, so only reach for this when a price legitimately differs between two buyers who would otherwise share a segment.

The Dokan bridge returns the user id only when per-vendor buyer approval is switched on — so a buyer approved for some vendors but not others can never be served another buyer’s cached price. While that setting is off the salt stays 0 and there is no cache cost.

// Give a specific VIP their own price cache, whatever their group.
add_filter('smb2b_price_user_salt', function ($salt, $context) {
    return my_is_vip($context->userId()) ? $context->userId() : $salt;
}, 10, 2);

smb2b_vendor_context

apply_filters('smb2b_vendor_context', array $context): array

Who a management request belongs to — the marketplace admin managing everything, or a single vendor editing only their own catalogue. This is the write-side counterpart to smb2b_pricing_context_company (which is about the buyer being priced). Core answers “the admin, managing everything”; a bridge overrides it on a vendor-dashboard request so core scopes every write — rules, quantity rules — to that vendor.

Read it through Pricing\VendorContext::current(). The shape is ['is_vendor' => bool, 'vendor_id' => int, 'can_manage' => bool].

add_filter('smb2b_vendor_context', function ($context) {
    if (! dokan_is_seller_dashboard()) {
        return $context; // an admin screen — leave core's admin context
    }

    return [
        'is_vendor'  => true,
        'vendor_id'  => dokan_get_current_user_id(),
        'can_manage' => true,
    ];
});

Price and catalogue visibility

smb2b_hide_prices — Pro-aware

apply_filters('smb2b_hide_prices', bool $hidden): bool

Whether prices are hidden for the current visitor. Core hides from guests; Pro extends it to catalogue mode and per-group visibility.

add_filter('smb2b_hide_prices', function ($hidden) {
    // Also hide from logged-in users who are not approved trade customers.
    if (!$hidden && is_user_logged_in()) {
        return !SoftminalB2B\Registration\Applicant::isApproved(get_current_user_id());
    }

    return $hidden;
});

smb2b_hidden_price_message

The HTML shown in place of a hidden price. It is wp_kses_post-filtered before output, but return trusted markup regardless.

add_filter('smb2b_hidden_price_message', fn () => 'Trade pricing — <a href="/apply/">apply for an account</a>');

Registration and approval

smb2b_registration_fields

apply_filters('smb2b_registration_fields', array $fields): array

The business fields on the registration form. Each entry is keyed by field name:

'company_name' => [
    'label'    => 'Company name',
    'required' => true,
    'type'     => 'text', // text | tel | textarea
]

Submitted values are stored together in a single smb2b_business user-meta array and shown to the admin on the approval screen. Read them back with Applicant::business($userId) rather than looking for one meta key per field.

add_filter('smb2b_registration_fields', function ($fields) {
    $fields['reseller_number'] = [
        'label'    => 'Reseller certificate #',
        'required' => true,
        'type'     => 'text',
    ];

    // Make the phone number optional for our market.
    $fields['phone']['required'] = false;

    return $fields;
});

smb2b_registration_spam_check

Return true to reject a submission as spam before it is stored. The honeypot and minimum fill time already run; this is where a captcha or a rate limiter goes.

add_filter('smb2b_registration_spam_check', function ($isSpam, $data) {
    if ($isSpam) {
        return true;
    }

    return my_recaptcha_failed($_POST['g-recaptcha-response'] ?? '');
}, 10, 2);

smb2b_registration_submitted

Fires after a valid application is stored with status pending. Receives the user id.

smb2b_registration_approved

do_action('smb2b_registration_approved', int $userId, int $groupId): void

The pricing group is already assigned by the time this runs.

add_action('smb2b_registration_approved', function ($userId, $groupId) {
    my_crm_tag_user($userId, 'b2b-approved');
    my_slack_notify(sprintf('%s approved for trade', get_userdata($userId)->user_email));
}, 10, 2);

smb2b_registration_rejected

Receives ($userId, $reason). Any B2B pricing has already been removed by the time it fires.


Invoices and PDFs

smb2b_invoice_data

apply_filters('smb2b_invoice_data', array $data, WC_Order $order): array

The assembled invoice data, before it reaches the template. This is the right seam for extra totals rows, altered seller details or additional line-item columns — and the seam the Dokan bridge uses to swap in a per-vendor seller block on a single-vendor (sub-)order.

add_filter('smb2b_invoice_data', function ($data, $order) {
    $data['note'] = 'Thank you for your business — terms net 30.';

    if ($po = $order->get_meta('_smb2b_po_number')) {
        $data['note'] = "PO {$po}\n" . $data['note'];
    }

    return $data;
}, 10, 2);

smb2b_invoice_templates

The selectable invoice designs as slug => label. Add your own slug here and put the matching slug.php view in the templates directory — or return the HTML from smb2b_invoice_html instead.

smb2b_invoice_html — Pro-aware

Receives (null, $order, $data). Return a non-empty string to bypass the built-in templates entirely, or null to keep the default rendering.


Settings

smb2b_editable_settings — Pro-aware

apply_filters('smb2b_editable_settings', array $editable): array

The map of key => type that the settings endpoint will read and write. Types: bool, int, text, textarea, email, html.

Keys not declared here are silently ignored, so this filter doubles as the allow-list that stops the settings endpoint writing arbitrary options.

add_filter('smb2b_editable_settings', function ($editable) {
    $editable['my_addon_option'] = 'bool';
    $editable['my_addon_intro']  = 'textarea';

    return $editable;
});

Scheduled jobs

smb2b_housekeeping

Fires on the plugin’s daily maintenance run through Action Scheduler, tagged smb2b. Hook your own periodic cleanup here instead of scheduling a separate cron — it is cancelled cleanly on uninstall along with everything else.

add_action('smb2b_housekeeping', function () {
    my_prune_stale_exports();
});

smb2b_fixtures_generated / smb2b_fixtures_reset

Fire after the benchmark fixture generator creates or clears its catalogue. Test setups only.

smb2b_seed_generated / smb2b_seed_reset

Fire at the end of wp smb2b seed run and wp smb2b seed reset. seed_generated receives the run’s Manifest.

Record anything you create on the manifest — record() for post, user and term ids, recordRow($table, $id) for rows in the plugin’s own tables — and seed reset will remove it for you. Reset deletes only what the manifest lists; it never truncates a table.


Companies (Pro)

smb2bpro_company_membership_changed

Fires when a user joins, leaves, or changes role in a company. It also triggers smb2b_pricing_data_changed, because membership affects pricing.

smb2bpro_register_modules

Fires at the end of Pro’s boot (plugins_loaded priority 11), after core is populated and before rest_api_init. This is the seam for a third-party add-on to register its own modules and REST routes into the shared smb2b/v1 namespace.

add_action('smb2bpro_register_modules', function () {
    My\Addon\Module::init();
});

Order approvals and spend limits (Pro)

smb2bpro_approval_hold_reason

apply_filters('smb2bpro_approval_hold_reason', ?string $reason, WC_Order $order): ?string

Whether — and why — an order is held for approval. The default holds orders over a buyer’s per-order or monthly limit. Return a non-empty string to hold for your own reason, or null to release an order the limits would have held.

add_filter('smb2bpro_approval_hold_reason', function ($reason, $order) {
    // Always review the first order from a brand-new company.
    if ($reason === null && my_is_first_company_order($order)) {
        return 'First order from this company — please review.';
    }

    return $reason;
}, 10, 2);

smb2bpro_order_held / _approved / _rejected

  • held($orderId, $reason). Status has moved to the approval status, stock is reserved, approvers have been notified.
  • approved($orderId, $actorId). This is the seam net terms hooks to complete eligible orders on account rather than emailing a payment link.
  • rejected($orderId, $actorId). Stock has already been released.

Net terms and credit (Pro)

smb2bpro_ledger_posted

do_action('smb2bpro_ledger_posted', CreditTransaction $transaction, int $accountId): void

Fires whenever a row is appended to the ledger — a debit, payment, refund credit or adjustment. The ledger is append-only: react to entries, never mutate them. Calling delete() on a transaction is refused; record a reversing adjustment instead.

The entry carries type (debit | credit | adjustment), amount (a signed delta to what is owed — debits positive, credits and refunds negative), order_id, refund_id, due_date, note and created_by.

add_action('smb2bpro_ledger_posted', function ($transaction, $accountId) {
    my_accounting_api()->postJournalEntry([
        'account' => $accountId,
        'type'    => $transaction->type,
        'amount'  => $transaction->amount,
        'order'   => $transaction->order_id,
        'memo'    => $transaction->note,
    ]);
}, 10, 2);

smb2bpro_net_terms_order_placed

($orderId, $accountId) — fires when an order is confirmed on account. The debit is already posted and the due date stamped.

smb2bpro_invoice_overdue

($orderId, $daysLate) — fires each time an overdue reminder goes out. Use it to escalate beyond the built-in email cadence.

add_action('smb2bpro_invoice_overdue', function ($orderId, $daysLate) {
    if ($daysLate >= 30) {
        my_crm_create_task("Call about overdue order #{$orderId}");
    }
}, 10, 2);

smb2bpro_statement_sent

($accountId, $balance) — fires after a monthly statement email is sent.


Quotes / RFQ (Pro)

Every transition fires an action carrying the quote id, so you can sync a CRM or trigger automation at any point in the lifecycle.

Hook Payload Fires when
smb2bpro_quote_submitted ($quoteId) A buyer submits their basket for review
smb2bpro_quote_offered ($quoteId) You send a counter-offer with line prices and an expiry
smb2bpro_quote_accepted ($quoteId, $orderId) The buyer accepted and the order was created
smb2bpro_quote_declined ($quoteId, $byUserId) Either party declined
smb2bpro_quote_expired ($quoteId) An offer passed its expiry date on the daily sweep
smb2bpro_quote_message ($quoteId, $userId) A message was posted to the thread
smb2bpro_quote_from_conversation ($quoteId, $conversationId) A general inquiry was converted into a quote

smb2bpro_quote_accepted is the seam for attaching quote-specific data to the new order:

add_action('smb2bpro_quote_accepted', function ($quoteId, $orderId) {
    $order = wc_get_order($orderId);
    $order->update_meta_data('_my_quote_ref', "Q-{$quoteId}");
    $order->add_order_note(sprintf('Converted from quote #%d.', $quoteId));
    $order->save();
}, 10, 2);

Messaging and conversations

The buyer↔merchant message store is Free. A conversation can be anchored to an order, product or quote, and Pro layers the quote reply-and-convert behaviour on top of the same store through these seams — one set of tables, one email path, whether or not Pro is active.

smb2b_conversation_anchor — Pro-aware

apply_filters('smb2b_conversation_anchor', array $base, Conversation $conversation): array

Enrich a conversation’s anchor descriptor before it reaches the Inbox and the buyer centre. $base is ['type', 'id', 'label']; add a card payload to render a rich context card, and core shows just the label chip when none is present. Pro uses this to surface a quote’s status, line count, agreed value and expiry on the thread.

add_filter('smb2b_conversation_anchor', function ($base, $conversation) {
    if ($base['type'] !== 'order') {
        return $base;
    }

    $order = wc_get_order($base['id']);
    $base['card'] = [
        'title' => sprintf('Order #%d', $base['id']),
        'meta'  => $order ? $order->get_status() : '',
    ];

    return $base;
}, 10, 2);

smb2b_conversation_reply_handled — Pro-aware

apply_filters(
    'smb2b_conversation_reply_handled',
    bool $handled,
    Conversation $conversation,
    int $userId,
    string $side,   // 'buyer' | 'shop'
    string $body
): bool

Lets an add-on take ownership of a reply posted to an anchored thread, so the message runs through that domain’s own write and notification instead of the plain conversation path. Return true once you have stored the message and sent your own notification, and core skips its default addMessage plus email. Pro claims anchor_type === 'quote' threads and routes them through the quote negotiation — one message store, one email path.

add_filter('smb2b_conversation_reply_handled', function ($handled, $conversation, $userId, $side, $body) {
    if ($handled || $conversation->anchor_type !== 'ticket') {
        return $handled;
    }

    my_helpdesk_post_reply($conversation->anchor_id, $userId, $side, $body);

    return true; // core will not also store or email this reply
}, 10, 5);

smb2b_conversation_can_convert — Pro-aware

apply_filters('smb2b_conversation_can_convert', bool $can, Conversation $conversation): bool

Whether a conversation can be turned into a quote — this drives the Inbox “Convert to quote” button. The default is false (Free never adds the filter); Pro returns true for any thread that is not already a quote.

smb2b_message_sent

do_action('smb2b_message_sent', Conversation $conversation, Message $message): void

Fires after a reply is posted through the core reply endpoints. It does not fire when a handler claimed the reply via smb2b_conversation_reply_handled — that domain fires its own event instead (for a quote, smb2bpro_quote_message). Use it for a catch-all sync of plain buyer↔merchant messages.

add_action('smb2b_message_sent', function ($conversation, $message) {
    my_crm_log_message($conversation->id, $message->body);
}, 10, 2);

smb2b_messages_buyer_url

apply_filters('smb2b_messages_buyer_url', string $url): string

The storefront URL buyers are linked to from message-notification emails. Defaults to the My Account “Messages” endpoint — override it to point at a custom account page.

add_filter('smb2b_messages_buyer_url', fn () => home_url('/my-account/inbox/'));

Catalogue visibility (Pro)

smb2bpro_hidden_category_ids

apply_filters('smb2bpro_hidden_category_ids', array $hiddenIds, int $userId): array

The product_cat term ids hidden from a viewer, applied to every product query — shop, search, feeds and the Store API. $userId is 0 for a guest. Add ids to hide more, remove ids to reveal.

add_filter('smb2bpro_hidden_category_ids', function ($hidden, $userId) {
    if ($userId === 0) {
        $hidden[] = my_members_only_category_id();
    }

    return $hidden;
}, 10, 2);

Storefront (Pro)

smb2bpro_tier_table_rows

apply_filters('smb2bpro_tier_table_rows', array $rows, WC_Product $product): array

The quantity-tier rows before the bulk-pricing table renders. Each row is ['from' => int, 'to' => int|null, 'price' => float, 'price_html' => string]. Return [] to suppress the table for a product.

add_filter('smb2bpro_tier_table_rows', function ($rows, $product) {
    if (has_term('clearance', 'product_cat', $product->get_id())) {
        return [];
    }

    return $rows;
}, 10, 2);

Tax and EU VAT (Pro)

smb2bpro_vies_response

apply_filters('smb2bpro_vies_response', ?string $override, string $countryCode, string $number): ?string

Short-circuit the live VIES call. Return 'valid', 'invalid' or 'unavailable' to skip the network entirely — for tests, or to point at a self-hosted VIES proxy. Return null to let the real lookup happen.

add_filter('smb2bpro_vies_response', function ($override, $country, $number) {
    return my_vies_proxy_check($country, $number);
}, 10, 3);

Recipes

Give one company a contract price on a single product

Rules cover almost every case — reach for code only when the logic is genuinely dynamic:

add_filter('smb2b_resolved_price', function ($resolved, $request) {
    $isTargetLine = $request['company_id'] === 42 && $request['product_id'] === 1180;

    if ($isTargetLine && $request['qty'] >= 100) {
        return $resolved->withPrice(8.75); // negotiated pallet price
    }

    return $resolved;
}, 10, 2);

Post every new trade application to Slack

use SoftminalB2B\Registration\Applicant;

add_action('smb2b_registration_submitted', function ($userId) {
    $user    = get_userdata($userId);
    // Every business field lives in one meta array — read it through the model.
    $company = Applicant::business($userId)['company_name'] ?? '';

    wp_remote_post(MY_SLACK_WEBHOOK, [
        'body'     => wp_json_encode([
            'text' => sprintf('New trade application: %s (%s)', $company, $user->user_email),
        ]),
        'headers'  => ['Content-Type' => 'application/json'],
        'blocking' => false, // never make a visitor wait on Slack
    ]);
});

Mirror the credit ledger into an accounting system

Because the ledger is append-only and idempotent per order and refund, a one-way mirror stays correct without reconciliation:

add_action('smb2bpro_ledger_posted', function ($transaction, $accountId) {
    as_enqueue_async_action('my_push_ledger_entry', [$transaction->id, $accountId], 'my-plugin');
}, 10, 2);

add_action('my_push_ledger_entry', function ($transactionId, $accountId) {
    // Runs out of band via Action Scheduler, retried on failure — so a slow
    // accounting API never holds up a checkout.
    my_accounting_api()->post($transactionId, $accountId);
}, 10, 2);

Add a field to registration and use it to auto-assign a group

add_filter('smb2b_registration_fields', function ($fields) {
    $fields['trade_type'] = [
        'label'    => 'What do you do?',
        'required' => true,
        'type'     => 'text',
    ];

    return $fields;
});

add_action('smb2b_registration_approved', function ($userId, $groupId) {
    $business = SoftminalB2B\Registration\Applicant::business($userId);

    if (strtolower($business['trade_type'] ?? '') === 'installer') {
        my_move_user_to_group($userId, MY_INSTALLER_GROUP_ID);
    }
}, 10, 2);

Check what a customer would pay, from your own code

You do not need a hook for this — call the resolver:

use SoftminalB2B\Pricing\Resolver;

// resolve(int $productId, ?int $variationId, ?int $userId = null, int $qty = 1)
$resolved = Resolver::resolve(1180, null, 45, 10);

$resolved->price();   // what they pay
$resolved->source();  // which rule won
$resolved->trail();   // every level considered, in order

The same data is available over HTTP — see the REST API.


Debugging

  • A filter seems to have no effect. Check the argument count. add_filter('smb2b_resolved_price', $fn) without the trailing 10, 2 means your callback never receives $request, and a null argument error usually follows.
  • A price is not what you expect. Use Pricing → Check a price before reading any code — it shows the whole precedence walk including your filter’s effect, because smb2b_resolved_price runs inside the resolution the explainer reports on.
  • Your override works on the product page but not in the cart. That should not happen with smb2b_resolved_price, which runs at every resolution. If you see it, you are probably filtering a WooCommerce price hook directly — move to the B2B filter.
  • Nothing fires at all. Confirm the plugin is active and, for smb2bpro_ hooks, that Pro is too. Loading order matters: register Pro hooks on smb2bpro_register_modules or later.

Conventions

  • Filters returning HTML are escaped or wp_kses_post-filtered before output — return trusted markup regardless.
  • Money values are in the store’s base currency. Convert for display, never for storage.
  • Pro-aware core filters work whether or not Pro is installed.
  • If you need an extension point that is not here, ask — that is how most of these got added.

Was this page missing something?

Documentation gaps are treated as bugs here.Tell us →