Repository: saleor/saleor
Pull request: Optimize webhooks calls in bulk mutations
I want to merge this change because it reduces the number of webhook queries for bulk mutations

resolves: https://github.com/saleor/saleor/issues/14112

# Impact

- [ ] New migrations
- [ ] New/Updated API fields or mutations
- [ ] Deprecated API fields or mutations
- [ ] Removed API types, fields, or mutations

# Docs

<!-- Docs are stored in a separate repository: https://github.com/saleor/saleor-docs/. -->
<!-- Please provide a link to the PR that updates documentation for your changes. -->
<!-- If changes in docs are not required, please mention that in the description. -->

- [ ] Link to documentation:

# Pull Request Checklist

<!-- Please keep this section. It will make the maintainer's life easier. -->

- [ ] Privileged queries and mutations are either absent or guarded by proper permission checks
- [ ] Database queries are optimized and the number of queries is constant
- [ ] Database migrations are either absent or optimized for zero downtime
- [ ] The changes are covered by test cases


Problem statement:
Optimize loading webhooks for event
The `get_webhooks_for_event` should be rewritten into a dataloader. Currently, there is a performance issue visible in bulk mutations where webhooks are triggered in a loop for each instance modified in the bulk mutation, and for each instance, it's running `get_webhooks_for_event`. Each call triggers a database query:

```
SELECT * FROM webhook_webhook INNER JOIN app_app ON ( webhook_webhook . app_id = ? . id ) WHERE ( webhook_webhook . app_id IN ( SELECT U0. id FROM app_app U0 INNER JOIN app_app_permissions U1 ON ( U0. id = U1. app_id ) INNER JOIN permission_permission U2 ON ( U1. permission_id = U2. id ) INNER JOIN django_content_type U3 ON ( U2. content_type_id = U3. id ) WHERE ( U0. is_active AND U2. codename = ? AND U3. app_label = ? ) ) AND webhook_webhook . is_active AND EXISTS ( SELECT ( ? ) FROM webhook_webhookevent U0 WHERE ( U0. event_type IN ( ? ) AND U0. webhook_id = ? . id ) LIMIT ? ) ) ORDER BY webhook_webhook . id ASC
```

Hints from issue discussion:
Example trace: https://app.datadoghq.com/apm/trace/15459061078129668117?graphType=flamegraph&shouldShowLegend=true&sort=time&spanID=4396032202316836865&spanViewType=metadata

Commit message: optimize webhooks calls in bulk mutations

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index faa3268e52f..f86fa317e81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -90,6 +90,33 @@ All notable, unreleased changes to this project will be documented in this file.
 
 ### Breaking changes
 - Deprecate `external_url` on `Invoice` GraphQL type in favour of `url`. No matter if the invoice is stored on Saleor or is a link to an external invoice it will get returned in the `url` field.
+- Extend methods of `BasePlugin` to accept optional `webhooks` parameter to reduce number of queries executed in bulk mutations.
+Affected methods:
+  - `channel_updated`
+  - `category_deleted`
+  - `gift_card_created`
+  - `gift_card_deleted`
+  - `gift_card_status_changed`
+  - `menu_deleted`
+  - `menu_item_deleted`
+  - `order_updated`
+  - `order_cancelled`
+  - `customer_updated`
+  - `customer_deleted`
+  - `customer_metadata_updated`
+  - `collection_deleted`
+  - `product_created`
+  - `product_updated`
+  - `product_deleted`
+  - `product_variant_created`
+  - `product_variant_updated`
+  - `product_variant_deleted`
+  - `product_variant_out_of_stock`
+  - `product_variant_back_in_stock`
+  - `product_variant_stock_updated`
+  - `shipping_price_deleted`
+  - `shipping_zone_deleted`
+  - `voucher_deleted`
 
 ### GraphQL API
 
diff --git a/export_files/temp_file.csv b/export_files/temp_file.csv
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/export_files/temp_file_IWAZRRh.csv b/export_files/temp_file_IWAZRRh.csv
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/saleor/graphql/account/bulk_mutations/customer_bulk_delete.py b/saleor/graphql/account/bulk_mutations/customer_bulk_delete.py
index 4c637ba806e..0fc28fcacf4 100644
--- a/saleor/graphql/account/bulk_mutations/customer_bulk_delete.py
+++ b/saleor/graphql/account/bulk_mutations/customer_bulk_delete.py
@@ -3,6 +3,7 @@
 from ....account import models
 from ....permission.enums import AccountPermissions
 from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core import ResolveInfo
 from ...core.doc_category import DOC_CATEGORY_USERS
 from ...core.mutations import ModelBulkDeleteMutation
@@ -49,6 +50,7 @@ def perform_mutation(cls, root, info: ResolveInfo, /, **data):
     def bulk_action(cls, info: ResolveInfo, queryset, /):
         instances = list(queryset)
         queryset.delete()
-        manager = get_plugin_manager_promise(info.context).get()
-        for instance in instances:
-            manager.customer_deleted(instance)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.CUSTOMER_DELETED):
+            manager = get_plugin_manager_promise(info.context).get()
+            for instance in instances:
+                manager.customer_deleted(instance, webhooks=webhooks)
diff --git a/saleor/graphql/account/bulk_mutations/customer_bulk_update.py b/saleor/graphql/account/bulk_mutations/customer_bulk_update.py
index b59e2e3c7f7..f1743fe1df2 100644
--- a/saleor/graphql/account/bulk_mutations/customer_bulk_update.py
+++ b/saleor/graphql/account/bulk_mutations/customer_bulk_update.py
@@ -17,6 +17,7 @@
 from ....order.utils import match_orders_with_new_user
 from ....permission.enums import AccountPermissions
 from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core.descriptions import ADDED_IN_313, PREVIEW_FEATURE
 from ...core.doc_category import DOC_CATEGORY_USERS
 from ...core.enums import CustomerBulkUpdateErrorCode, ErrorPolicyEnum
@@ -556,8 +557,16 @@ def post_save_actions(cls, info, manager, instances, old_instances):
         app = get_app_promise(info.context).get()
         staff_user = info.context.user
         users_with_name_or_email_updated = []
+        webhooks_meta = get_webhooks_for_event(
+            WebhookEventAsyncType.CUSTOMER_METADATA_UPDATED
+        )
+        webhooks_updated = get_webhooks_for_event(
+            WebhookEventAsyncType.CUSTOMER_UPDATED
+        )
         for updated_instance, old_instance in zip(instances, old_instances):
-            cls.call_event(manager.customer_updated, updated_instance)
+            cls.call_event(
+                manager.customer_updated, updated_instance, webhooks=webhooks_updated
+            )
             new_email = updated_instance.email
             new_fullname = updated_instance.get_full_name()
 
@@ -620,7 +629,11 @@ def post_save_actions(cls, info, manager, instances, old_instances):
                 )
 
             if metadata_update:
-                cls.call_event(manager.customer_metadata_updated, updated_instance)
+                cls.call_event(
+                    manager.customer_metadata_updated,
+                    updated_instance,
+                    webhooks=webhooks_meta,
+                )
 
         models.CustomerEvent.objects.bulk_create(customer_events)
         mark_gift_cards_search_index_as_dirty_by_users(users_with_name_or_email_updated)
diff --git a/saleor/graphql/account/tests/bulk_mutations/test_customer_bulk_delete.py b/saleor/graphql/account/tests/bulk_mutations/test_customer_bulk_delete.py
index 29dc2f76350..1911c05748f 100644
--- a/saleor/graphql/account/tests/bulk_mutations/test_customer_bulk_delete.py
+++ b/saleor/graphql/account/tests/bulk_mutations/test_customer_bulk_delete.py
@@ -55,7 +55,9 @@ def test_delete_customers(
     )
 
 
-@patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@patch(
+    "saleor.graphql.account.bulk_mutations.customer_bulk_delete.get_webhooks_for_event"
+)
 @patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_customers_trigger_webhooks(
     mocked_webhook_trigger,
diff --git a/saleor/graphql/core/mutations.py b/saleor/graphql/core/mutations.py
index 79ee7264898..1b24f25513e 100644
--- a/saleor/graphql/core/mutations.py
+++ b/saleor/graphql/core/mutations.py
@@ -561,8 +561,8 @@ def handle_typed_errors(cls, errors: list, **extra):
         return cls(errors=errors, **extra)
 
     @staticmethod
-    def call_event(func_obj, *func_args):
-        return call_event(func_obj, *func_args)
+    def call_event(func_obj, *func_args, **kwargs):
+        return call_event(func_obj, *func_args, **kwargs)
 
     @classmethod
     def update_metadata(cls, instance, meta_data_list: List, is_private: bool = False):
diff --git a/saleor/graphql/discount/mutations/bulk_mutations.py b/saleor/graphql/discount/mutations/bulk_mutations.py
index 1f5f3aa5772..29e7677b0b2 100644
--- a/saleor/graphql/discount/mutations/bulk_mutations.py
+++ b/saleor/graphql/discount/mutations/bulk_mutations.py
@@ -10,6 +10,7 @@
 from ....product import models as product_models
 from ....product.tasks import update_products_discounted_prices_for_promotion_task
 from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core import ResolveInfo
 from ...core.doc_category import DOC_CATEGORY_DISCOUNTS
 from ...core.mutations import ModelBulkDeleteMutation
@@ -93,9 +94,10 @@ def bulk_action(cls, info: ResolveInfo, queryset, /):
 
         queryset.delete()
 
-        manager = get_plugin_manager_promise(info.context).get()
-        for sale, catalogue_info in sales_and_catalogue_infos:
-            manager.sale_deleted(sale, catalogue_info)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.SALE_DELETED):
+            manager = get_plugin_manager_promise(info.context).get()
+            for sale, catalogue_info in sales_and_catalogue_infos:
+                manager.sale_deleted(sale, catalogue_info, webhooks=webhooks)
 
         update_products_discounted_prices_for_promotion_task.delay(list(product_ids))
 
@@ -149,6 +151,7 @@ class Meta:
     def bulk_action(cls, info: ResolveInfo, queryset, /):
         vouchers = list(queryset)
         queryset.delete()
-        manager = get_plugin_manager_promise(info.context).get()
-        for voucher in vouchers:
-            manager.voucher_deleted(voucher)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.VOUCHER_DELETED):
+            manager = get_plugin_manager_promise(info.context).get()
+            for voucher in vouchers:
+                manager.voucher_deleted(voucher, webhooks=webhooks)
diff --git a/saleor/graphql/discount/tests/mutations/test_sale_bulk_delete.py b/saleor/graphql/discount/tests/mutations/test_sale_bulk_delete.py
index 0b78117b58e..4f9d6bee66c 100644
--- a/saleor/graphql/discount/tests/mutations/test_sale_bulk_delete.py
+++ b/saleor/graphql/discount/tests/mutations/test_sale_bulk_delete.py
@@ -73,6 +73,9 @@ def promotion_converted_from_sale_list(channel_USD, product_list, category, coll
     """
 
 
+@mock.patch(
+    ("saleor.graphql.discount.mutations." "bulk_mutations.get_webhooks_for_event")
+)
 @mock.patch("saleor.plugins.manager.PluginsManager.sale_deleted")
 @mock.patch(
     "saleor.product.tasks.update_products_discounted_prices_for_promotion_task.delay"
@@ -80,12 +83,17 @@ def promotion_converted_from_sale_list(channel_USD, product_list, category, coll
 def test_delete_sales(
     update_products_discounted_prices_for_promotion_task,
     deleted_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     promotion_converted_from_sale_list,
     permission_manage_discounts,
+    any_webhook,
+    settings,
 ):
     # given
     promotion_list = promotion_converted_from_sale_list
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     variables = {
         "ids": [
             graphene.Node.to_global_id("Sale", promotion.old_sale_id)
@@ -109,7 +117,9 @@ def test_delete_sales(
     assert deleted_webhook_mock.call_count == len(promotion_list)
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    ("saleor.graphql.discount.mutations." "bulk_mutations.get_webhooks_for_event")
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_sales_triggers_webhook(
     mocked_webhook_trigger,
@@ -144,7 +154,9 @@ def test_delete_sales_triggers_webhook(
     assert mocked_webhook_trigger.call_count == 3
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    ("saleor.graphql.discount.mutations." "bulk_mutations.get_webhooks_for_event")
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_sales_with_variants_triggers_webhook(
     mocked_webhook_trigger,
@@ -203,6 +215,9 @@ def test_delete_sales_with_variants_triggers_webhook(
     assert mocked_webhook_trigger.call_count == 3
 
 
+@mock.patch(
+    ("saleor.graphql.discount.mutations." "bulk_mutations.get_webhooks_for_event")
+)
 @mock.patch("saleor.plugins.manager.PluginsManager.sale_deleted")
 @mock.patch(
     "saleor.product.tasks.update_products_discounted_prices_for_promotion_task.delay"
@@ -210,12 +225,16 @@ def test_delete_sales_with_variants_triggers_webhook(
 def test_delete_sales_with_promotion_ids(
     update_products_discounted_prices_for_promotion_task,
     deleted_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     any_webhook,
     promotion_converted_from_sale_list,
     permission_manage_discounts,
+    settings,
 ):
     # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     variables = {
         "ids": [
             graphene.Node.to_global_id("Promotion", promotion.id)
diff --git a/saleor/graphql/discount/tests/mutations/test_voucher_bulk_delete.py b/saleor/graphql/discount/tests/mutations/test_voucher_bulk_delete.py
index ba97f0dad2b..cb5c02a1ad4 100644
--- a/saleor/graphql/discount/tests/mutations/test_voucher_bulk_delete.py
+++ b/saleor/graphql/discount/tests/mutations/test_voucher_bulk_delete.py
@@ -70,7 +70,9 @@ def test_delete_vouchers(staff_api_client, voucher_list, permission_manage_disco
     ).exists()
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    ("saleor.graphql.discount.mutations." "bulk_mutations.get_webhooks_for_event")
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_vouchers_trigger_webhook(
     mocked_webhook_trigger,
diff --git a/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_activate.py b/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_activate.py
index 739463e6573..f465ba4d203 100644
--- a/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_activate.py
+++ b/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_activate.py
@@ -7,6 +7,7 @@
 from ....giftcard.utils import is_gift_card_expired
 from ....permission.enums import GiftcardPermissions
 from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...app.dataloaders import get_app_promise
 from ...core import ResolveInfo
 from ...core.descriptions import ADDED_IN_31
@@ -54,6 +55,9 @@ def bulk_action(cls, info: ResolveInfo, queryset, /):
         events.gift_cards_activated_event(
             gift_card_ids, user=info.context.user, app=app
         )
-        manager = get_plugin_manager_promise(info.context).get()
-        for card in models.GiftCard.objects.filter(id__in=gift_card_ids):
-            manager.gift_card_status_changed(card)
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.GIFT_CARD_STATUS_CHANGED
+        ):
+            manager = get_plugin_manager_promise(info.context).get()
+            for card in models.GiftCard.objects.filter(id__in=gift_card_ids):
+                manager.gift_card_status_changed(card, webhooks=webhooks)
diff --git a/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_create.py b/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_create.py
index 4cb8ebdc510..d8b430f1289 100644
--- a/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_create.py
+++ b/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_create.py
@@ -11,6 +11,7 @@
 from ....giftcard.error_codes import GiftCardErrorCode
 from ....permission.enums import GiftcardPermissions
 from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...app.dataloaders import get_app_promise
 from ...core import ResolveInfo
 from ...core.descriptions import ADDED_IN_31
@@ -91,9 +92,8 @@ def perform_mutation(  # type: ignore[override]
         instances = cls.create_instances(input, info)
         if tags:
             cls.assign_gift_card_tags(instances, tags)
-        manager = get_plugin_manager_promise(info.context).get()
         transaction.on_commit(
-            lambda: cls.call_gift_card_created_on_plugins(instances, manager)
+            lambda: cls.call_gift_card_created_on_plugins(instances, info.context)
         )
         return cls(count=len(instances), gift_cards=instances)
 
@@ -173,6 +173,8 @@ def assign_gift_card_tags(
             tag_instance.gift_cards.set(instances)
 
     @staticmethod
-    def call_gift_card_created_on_plugins(instances, manager):
-        for instance in instances:
-            manager.gift_card_created(instance)
+    def call_gift_card_created_on_plugins(instances, context):
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.GIFT_CARD_CREATED):
+            manager = get_plugin_manager_promise(context).get()
+            for instance in instances:
+                manager.gift_card_created(instance, webhooks=webhooks)
diff --git a/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_deactivate.py b/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_deactivate.py
index 71d51a7dc8a..acd2e0f11b5 100644
--- a/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_deactivate.py
+++ b/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_deactivate.py
@@ -4,6 +4,7 @@
 from ....giftcard import events, models
 from ....permission.enums import GiftcardPermissions
 from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...app.dataloaders import get_app_promise
 from ...core import ResolveInfo
 from ...core.descriptions import ADDED_IN_31
@@ -45,6 +46,9 @@ def bulk_action(cls, info: ResolveInfo, queryset, /):
         events.gift_cards_deactivated_event(
             gift_card_ids, user=info.context.user, app=app
         )
-        manager = get_plugin_manager_promise(info.context).get()
-        for card in models.GiftCard.objects.filter(id__in=gift_card_ids):
-            manager.gift_card_status_changed(card)
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.GIFT_CARD_STATUS_CHANGED
+        ):
+            manager = get_plugin_manager_promise(info.context).get()
+            for card in models.GiftCard.objects.filter(id__in=gift_card_ids):
+                manager.gift_card_status_changed(card, webhooks=webhooks)
diff --git a/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_delete.py b/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_delete.py
index 62ea5367852..3704e6bbcdb 100644
--- a/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_delete.py
+++ b/saleor/graphql/giftcard/bulk_mutations/gift_card_bulk_delete.py
@@ -3,6 +3,7 @@
 from ....giftcard import models
 from ....permission.enums import GiftcardPermissions
 from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core import ResolveInfo
 from ...core.descriptions import ADDED_IN_31
 from ...core.mutations import ModelBulkDeleteMutation
@@ -35,6 +36,7 @@ class Meta:
     def bulk_action(cls, info: ResolveInfo, queryset, /):
         instances = [card for card in queryset]
         queryset.delete()
-        manager = get_plugin_manager_promise(info.context).get()
-        for instance in instances:
-            manager.gift_card_deleted(instance)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.GIFT_CARD_DELETED):
+            manager = get_plugin_manager_promise(info.context).get()
+            for instance in instances:
+                manager.gift_card_deleted(instance, webhooks=webhooks)
diff --git a/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_activate.py b/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_activate.py
index edf20396374..92a3a562b6a 100644
--- a/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_activate.py
+++ b/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_activate.py
@@ -183,7 +183,10 @@ def test_gift_card_bulk_activate_expired_cards(
         assert error in expected_errors
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    "saleor.graphql.giftcard.bulk_mutations."
+    "gift_card_bulk_activate.get_webhooks_for_event"
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_gift_card_bulk_activate_trigger_webhook(
     mocked_webhook_trigger,
diff --git a/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_create.py b/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_create.py
index d17d303903d..5e5fcfafef4 100644
--- a/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_create.py
+++ b/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_create.py
@@ -162,7 +162,10 @@ def test_create_never_expiry_gift_cards(
         assert not card_data["events"][0]["balance"]["oldCurrentBalance"]
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    "saleor.graphql.giftcard.bulk_mutations."
+    "gift_card_bulk_create.get_webhooks_for_event"
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_create_gift_cards_trigger_webhooks(
     mocked_webhook_trigger,
diff --git a/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_deactivate.py b/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_deactivate.py
index 9e4b0032f8c..f82e89ea065 100644
--- a/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_deactivate.py
+++ b/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_deactivate.py
@@ -146,7 +146,10 @@ def test_gift_card_bulk_deactivate_by_customer(
     assert_no_permission(response)
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    "saleor.graphql.giftcard.bulk_mutations."
+    "gift_card_bulk_deactivate.get_webhooks_for_event"
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_gift_card_bulk_deactivate_trigger_webhook(
     mocked_webhook_trigger,
diff --git a/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_delete.py b/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_delete.py
index 0b91df043e2..fecfa00ddd1 100644
--- a/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_delete.py
+++ b/saleor/graphql/giftcard/tests/bulk_mutations/test_gift_card_bulk_delete.py
@@ -88,7 +88,10 @@ def test_gift_card_bulk_delete_by_customer(
     assert_no_permission(response)
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    "saleor.graphql.giftcard.bulk_mutations."
+    "gift_card_bulk_delete.get_webhooks_for_event"
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_gift_card_bulk_delete_trigger_webhook(
     mocked_webhook_trigger,
diff --git a/saleor/graphql/menu/bulk_mutations/menu_bulk_delete.py b/saleor/graphql/menu/bulk_mutations/menu_bulk_delete.py
index 1fd4488411f..da36d7deb17 100644
--- a/saleor/graphql/menu/bulk_mutations/menu_bulk_delete.py
+++ b/saleor/graphql/menu/bulk_mutations/menu_bulk_delete.py
@@ -3,6 +3,7 @@
 from ....menu import models
 from ....permission.enums import MenuPermissions
 from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core import ResolveInfo
 from ...core.mutations import ModelBulkDeleteMutation
 from ...core.types import MenuError, NonNullList
@@ -35,6 +36,7 @@ class Meta:
     def bulk_action(cls, info: ResolveInfo, queryset, /):
         menus = list(queryset)
         queryset.delete()
-        manager = get_plugin_manager_promise(info.context).get()
-        for menu in menus:
-            manager.menu_deleted(menu)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.MENU_DELETED):
+            manager = get_plugin_manager_promise(info.context).get()
+            for menu in menus:
+                manager.menu_deleted(menu, webhooks=webhooks)
diff --git a/saleor/graphql/menu/bulk_mutations/menu_item_bulk_delete.py b/saleor/graphql/menu/bulk_mutations/menu_item_bulk_delete.py
index 1ccab1d7ae9..857eeecaed5 100644
--- a/saleor/graphql/menu/bulk_mutations/menu_item_bulk_delete.py
+++ b/saleor/graphql/menu/bulk_mutations/menu_item_bulk_delete.py
@@ -3,6 +3,7 @@
 from ....menu import models
 from ....permission.enums import MenuPermissions
 from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core import ResolveInfo
 from ...core.mutations import ModelBulkDeleteMutation
 from ...core.types import MenuError, NonNullList
@@ -35,6 +36,7 @@ class Meta:
     def bulk_action(cls, info: ResolveInfo, queryset, /):
         menu_items = list(queryset)
         queryset.delete()
-        manager = get_plugin_manager_promise(info.context).get()
-        for menu_item in menu_items:
-            manager.menu_item_deleted(menu_item)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.MENU_ITEM_DELETED):
+            manager = get_plugin_manager_promise(info.context).get()
+            for menu_item in menu_items:
+                manager.menu_item_deleted(menu_item, webhooks=webhooks)
diff --git a/saleor/graphql/menu/tests/bulk_mutations/test_menu_bulk_delete.py b/saleor/graphql/menu/tests/bulk_mutations/test_menu_bulk_delete.py
index f2decc49295..9005d86c9f8 100644
--- a/saleor/graphql/menu/tests/bulk_mutations/test_menu_bulk_delete.py
+++ b/saleor/graphql/menu/tests/bulk_mutations/test_menu_bulk_delete.py
@@ -44,15 +44,17 @@ def test_delete_menus(staff_api_client, menu_list, permission_manage_menus):
     assert not Menu.objects.filter(id__in=[menu.id for menu in menu_list]).exists()
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    "saleor.graphql.menu.bulk_mutations.menu_bulk_delete.get_webhooks_for_event"
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_menus_trigger_webhook(
     mocked_webhook_trigger,
     mocked_get_webhooks_for_event,
-    any_webhook,
     staff_api_client,
     menu_list,
     permission_manage_menus,
+    any_webhook,
     settings,
 ):
     # given
diff --git a/saleor/graphql/menu/tests/bulk_mutations/test_menu_item_bulk_delete.py b/saleor/graphql/menu/tests/bulk_mutations/test_menu_item_bulk_delete.py
index e616bff58cc..81e86a97687 100644
--- a/saleor/graphql/menu/tests/bulk_mutations/test_menu_item_bulk_delete.py
+++ b/saleor/graphql/menu/tests/bulk_mutations/test_menu_item_bulk_delete.py
@@ -38,7 +38,9 @@ def test_delete_menu_items(staff_api_client, menu_item_list, permission_manage_m
     ).exists()
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    "saleor.graphql.menu.bulk_mutations.menu_item_bulk_delete.get_webhooks_for_event"
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_menu_items_trigger_webhook(
     mocked_webhook_trigger,
diff --git a/saleor/graphql/order/bulk_mutations/orders.py b/saleor/graphql/order/bulk_mutations/order_bulk_cancel.py
similarity index 80%
rename from saleor/graphql/order/bulk_mutations/orders.py
rename to saleor/graphql/order/bulk_mutations/order_bulk_cancel.py
index fd258c38d5a..a1ef058f450 100644
--- a/saleor/graphql/order/bulk_mutations/orders.py
+++ b/saleor/graphql/order/bulk_mutations/order_bulk_cancel.py
@@ -6,6 +6,8 @@
 from ....order import models
 from ....order.actions import cancel_order
 from ....permission.enums import OrderPermissions
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...app.dataloaders import get_app_promise
 from ...core import ResolveInfo
 from ...core.mutations import BaseBulkWithRestrictedChannelAccessMutation
@@ -35,6 +37,10 @@ def clean_instance(cls, _info: ResolveInfo, instance) -> None:
 
     @classmethod
     def bulk_action(cls, info: ResolveInfo, queryset, /) -> None:
+        webhooks_cancelled = get_webhooks_for_event(
+            WebhookEventAsyncType.ORDER_CANCELLED
+        )
+        webhooks_updated = get_webhooks_for_event(WebhookEventAsyncType.ORDER_UPDATED)
         manager = get_plugin_manager_promise(info.context).get()
         for order in queryset:
             cancel_order(
@@ -42,6 +48,8 @@ def bulk_action(cls, info: ResolveInfo, queryset, /) -> None:
                 user=info.context.user,
                 app=get_app_promise(info.context).get(),
                 manager=manager,
+                webhooks_cancelled=webhooks_cancelled,
+                webhooks_updated=webhooks_updated,
             )
 
     @classmethod
diff --git a/saleor/graphql/order/schema.py b/saleor/graphql/order/schema.py
index 79454c74be4..0d5c136fc3e 100644
--- a/saleor/graphql/order/schema.py
+++ b/saleor/graphql/order/schema.py
@@ -22,8 +22,8 @@
 from ..core.utils import ext_ref_to_global_id_or_error, from_global_id_or_error
 from ..core.validators import validate_one_of_args_is_in_query
 from .bulk_mutations.draft_orders import DraftOrderBulkDelete, DraftOrderLinesBulkDelete
+from .bulk_mutations.order_bulk_cancel import OrderBulkCancel
 from .bulk_mutations.order_bulk_create import OrderBulkCreate
-from .bulk_mutations.orders import OrderBulkCancel
 from .filters import DraftOrderFilter, OrderFilter
 from .mutations.draft_order_complete import DraftOrderComplete
 from .mutations.draft_order_create import DraftOrderCreate
diff --git a/saleor/graphql/order/tests/mutations/test_order_bulk_cancel.py b/saleor/graphql/order/tests/mutations/test_order_bulk_cancel.py
index 7073f47b7a0..e831aa599b2 100644
--- a/saleor/graphql/order/tests/mutations/test_order_bulk_cancel.py
+++ b/saleor/graphql/order/tests/mutations/test_order_bulk_cancel.py
@@ -19,15 +19,23 @@
 """
 
 
-@patch("saleor.graphql.order.bulk_mutations.orders.cancel_order")
+@patch("saleor.graphql.order.bulk_mutations.order_bulk_cancel.get_webhooks_for_event")
+@patch("saleor.graphql.order.bulk_mutations.order_bulk_cancel.cancel_order")
 def test_order_bulk_cancel(
     mock_cancel_order,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     order_list,
     fulfilled_order_with_all_cancelled_fulfillments,
     permission_group_manage_orders,
     address,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
+
     permission_group_manage_orders.user_set.add(staff_api_client.user)
     orders = order_list
     orders.append(fulfilled_order_with_all_cancelled_fulfillments)
@@ -42,7 +50,14 @@ def test_order_bulk_cancel(
     assert not data["errors"]
 
     calls = [
-        call(order=order, user=staff_api_client.user, app=None, manager=ANY)
+        call(
+            order=order,
+            user=staff_api_client.user,
+            app=None,
+            manager=ANY,
+            webhooks_cancelled=[any_webhook],
+            webhooks_updated=[any_webhook],
+        )
         for order in orders
     ]
 
@@ -96,15 +111,23 @@ def test_order_bulk_cancel_with_back_in_stock_webhook(
     product_variant_back_in_stock_webhook_mock.assert_called_once()
 
 
-@patch("saleor.graphql.order.bulk_mutations.orders.cancel_order")
+@patch("saleor.graphql.order.bulk_mutations.order_bulk_cancel.get_webhooks_for_event")
+@patch("saleor.graphql.order.bulk_mutations.order_bulk_cancel.cancel_order")
 def test_order_bulk_cancel_as_app(
     mock_cancel_order,
+    mocked_get_webhooks_for_event,
     app_api_client,
     order_list,
     fulfilled_order_with_all_cancelled_fulfillments,
     permission_manage_orders,
     address,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
+
     orders = order_list
     orders.append(fulfilled_order_with_all_cancelled_fulfillments)
     expected_count = sum(order.can_cancel() for order in orders)
@@ -120,7 +143,14 @@ def test_order_bulk_cancel_as_app(
     assert not data["errors"]
 
     calls = [
-        call(order=order, user=None, app=app_api_client.app, manager=ANY)
+        call(
+            order=order,
+            user=None,
+            app=app_api_client.app,
+            manager=ANY,
+            webhooks_cancelled=[any_webhook],
+            webhooks_updated=[any_webhook],
+        )
         for order in orders
     ]
 
@@ -128,15 +158,23 @@ def test_order_bulk_cancel_as_app(
     assert mock_cancel_order.call_count == expected_count
 
 
-@patch("saleor.graphql.order.bulk_mutations.orders.cancel_order")
+@patch("saleor.graphql.order.bulk_mutations.order_bulk_cancel.get_webhooks_for_event")
+@patch("saleor.graphql.order.bulk_mutations.order_bulk_cancel.cancel_order")
 def test_order_bulk_cancel_without_sku(
     mock_cancel_order,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     order_list,
     fulfilled_order_with_all_cancelled_fulfillments,
     permission_group_manage_orders,
     address,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
+
     permission_group_manage_orders.user_set.add(staff_api_client.user)
     ProductVariant.objects.update(sku=None)
     OrderLine.objects.update(product_sku=None)
@@ -154,9 +192,16 @@ def test_order_bulk_cancel_without_sku(
     assert not data["errors"]
 
     calls = [
-        call(order=order, user=staff_api_client.user, app=None, manager=ANY)
+        call(
+            order=order,
+            user=staff_api_client.user,
+            app=None,
+            manager=ANY,
+            webhooks_cancelled=[any_webhook],
+            webhooks_updated=[any_webhook],
+        )
         for order in orders
     ]
 
     mock_cancel_order.assert_has_calls(calls, any_order=True)
-    mock_cancel_order.call_count == expected_count
+    assert mock_cancel_order.call_count == expected_count
diff --git a/saleor/graphql/product/bulk_mutations/collection_bulk_delete.py b/saleor/graphql/product/bulk_mutations/collection_bulk_delete.py
index c3d40c986c4..a28e3dbedf9 100644
--- a/saleor/graphql/product/bulk_mutations/collection_bulk_delete.py
+++ b/saleor/graphql/product/bulk_mutations/collection_bulk_delete.py
@@ -4,6 +4,8 @@
 from ....permission.enums import ProductPermissions
 from ....product import models
 from ....product.tasks import update_products_discounted_prices_for_promotion_task
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core.mutations import ModelBulkDeleteMutation
 from ...core.types import CollectionError, NonNullList
 from ...plugins.dataloaders import get_plugin_manager_promise
@@ -36,12 +38,14 @@ def bulk_action(cls, info, queryset):
             )
         )
         manager = get_plugin_manager_promise(info.context).get()
-        for collection in queryset.iterator():
-            manager.collection_deleted(collection)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.COLLECTION_DELETED):
+            for collection in queryset.iterator():
+                manager.collection_deleted(collection, webhooks=webhooks)
         queryset.delete()
 
-        for product in products:
-            manager.product_updated(product)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.PRODUCT_UPDATED):
+            for product in products:
+                manager.product_updated(product, webhooks=webhooks)
 
         update_products_discounted_prices_for_promotion_task.delay(
             [product.id for product in products]
diff --git a/saleor/graphql/product/bulk_mutations/product_bulk_create.py b/saleor/graphql/product/bulk_mutations/product_bulk_create.py
index 72d540fc77e..d857c17626c 100644
--- a/saleor/graphql/product/bulk_mutations/product_bulk_create.py
+++ b/saleor/graphql/product/bulk_mutations/product_bulk_create.py
@@ -21,6 +21,8 @@
 from ....product.tasks import update_products_discounted_prices_for_promotion_task
 from ....thumbnail.utils import get_filename_from_url
 from ....warehouse.models import Warehouse
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...attribute.types import AttributeValueInput
 from ...attribute.utils import AttributeAssignmentMixin
 from ...channel import ChannelContext
@@ -830,15 +832,22 @@ def prepare_media(cls, info, product, media_inputs, media_to_create):
     def post_save_actions(cls, info, products, variants, channels):
         manager = get_plugin_manager_promise(info.context).get()
         product_ids = []
+        webhooks = get_webhooks_for_event(WebhookEventAsyncType.PRODUCT_CREATED)
         for product in products:
-            cls.call_event(manager.product_created, product.node)
+            cls.call_event(manager.product_created, product.node, webhooks=webhooks)
             product_ids.append(product.node.id)
 
-        for variant in variants:
-            cls.call_event(manager.product_variant_created, variant)
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.PRODUCT_VARIANT_CREATED
+        ):
+            for variant in variants:
+                cls.call_event(
+                    manager.product_variant_created, variant, webhooks=webhooks
+                )
 
-        for channel in channels:
-            cls.call_event(manager.channel_updated, channel)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.CHANNEL_UPDATED):
+            for channel in channels:
+                cls.call_event(manager.channel_updated, channel, webhooks=webhooks)
 
         update_products_discounted_prices_for_promotion_task.delay(product_ids)
 
diff --git a/saleor/graphql/product/bulk_mutations/product_bulk_delete.py b/saleor/graphql/product/bulk_mutations/product_bulk_delete.py
index 733c7943308..328af472f1a 100644
--- a/saleor/graphql/product/bulk_mutations/product_bulk_delete.py
+++ b/saleor/graphql/product/bulk_mutations/product_bulk_delete.py
@@ -11,6 +11,8 @@
 from ....order.tasks import recalculate_orders_task
 from ....permission.enums import ProductPermissions
 from ....product import models
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...app.dataloaders import get_app_promise
 from ...core import ResolveInfo
 from ...core.mutations import ModelBulkDeleteMutation
@@ -93,7 +95,8 @@ def bulk_action(  # type: ignore[override]
 
         products = [product for product in queryset]
         queryset.delete()
-        manager = get_plugin_manager_promise(info.context).get()
-        for product in products:
-            variants = product_variant_map.get(product.id, [])
-            manager.product_deleted(product, variants)
+        if webhooks := get_webhooks_for_event(WebhookEventAsyncType.PRODUCT_DELETED):
+            manager = get_plugin_manager_promise(info.context).get()
+            for product in products:
+                variants = product_variant_map.get(product.id, [])
+                manager.product_deleted(product, variants, webhooks=webhooks)
diff --git a/saleor/graphql/product/bulk_mutations/product_variant_bulk_create.py b/saleor/graphql/product/bulk_mutations/product_variant_bulk_create.py
index b072422e23f..5244b0b9e54 100644
--- a/saleor/graphql/product/bulk_mutations/product_variant_bulk_create.py
+++ b/saleor/graphql/product/bulk_mutations/product_variant_bulk_create.py
@@ -14,6 +14,8 @@
 from ....product.error_codes import ProductVariantBulkErrorCode
 from ....product.tasks import update_products_discounted_prices_for_promotion_task
 from ....warehouse import models as warehouse_models
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...attribute.types import (
     AttributeValueDescriptions,
     AttributeValueSelectableTypeInput,
@@ -915,15 +917,19 @@ def prepare_stocks(cls, variant, stocks_input, stocks_to_create):
 
     @classmethod
     def post_save_actions(cls, info, instances, product):
-        manager = get_plugin_manager_promise(info.context).get()
-
         # Recalculate the "discounted price" for the parent product
         update_products_discounted_prices_for_promotion_task.delay([product.pk])
         product.search_index_dirty = True
         product.save(update_fields=["search_index_dirty"])
 
-        for instance in instances:
-            cls.call_event(manager.product_variant_created, instance.node)
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.PRODUCT_VARIANT_CREATED
+        ):
+            manager = get_plugin_manager_promise(info.context).get()
+            for instance in instances:
+                cls.call_event(
+                    manager.product_variant_created, instance.node, webhooks=webhooks
+                )
 
     @classmethod
     @traced_atomic_transaction()
diff --git a/saleor/graphql/product/bulk_mutations/product_variant_bulk_delete.py b/saleor/graphql/product/bulk_mutations/product_variant_bulk_delete.py
index af420a47a23..0310487b5e7 100644
--- a/saleor/graphql/product/bulk_mutations/product_variant_bulk_delete.py
+++ b/saleor/graphql/product/bulk_mutations/product_variant_bulk_delete.py
@@ -18,6 +18,8 @@
 from ....product import models
 from ....product.search import prepare_product_search_vector_value
 from ....product.tasks import update_products_discounted_prices_for_promotion_task
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...app.dataloaders import get_app_promise
 from ...core import ResolveInfo
 from ...core.descriptions import ADDED_IN_38
@@ -87,9 +89,15 @@ def perform_mutation(cls, _root, info: ResolveInfo, /, ids=None, skus=None, **da
         cls.delete_product_channel_listings_without_available_variants(product_pks, pks)
         response = super().perform_mutation(_root, info, ids=ids, **data)
         manager = get_plugin_manager_promise(info.context).get()
-        transaction.on_commit(
-            lambda: [manager.product_variant_deleted(variant) for variant in variants]
-        )
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.PRODUCT_VARIANT_DELETED
+        ):
+            transaction.on_commit(
+                lambda: [
+                    manager.product_variant_deleted(variant, webhooks=webhooks)
+                    for variant in variants
+                ]
+            )
 
         # delete order lines for deleted variants
         order_models.OrderLine.objects.filter(
diff --git a/saleor/graphql/product/bulk_mutations/product_variant_bulk_update.py b/saleor/graphql/product/bulk_mutations/product_variant_bulk_update.py
index 366417065c6..493143a5953 100644
--- a/saleor/graphql/product/bulk_mutations/product_variant_bulk_update.py
+++ b/saleor/graphql/product/bulk_mutations/product_variant_bulk_update.py
@@ -12,6 +12,8 @@
 from ....product.error_codes import ProductErrorCode, ProductVariantBulkErrorCode
 from ....product.tasks import update_products_discounted_prices_for_promotion_task
 from ....warehouse import models as warehouse_models
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...attribute.utils import AttributeAssignmentMixin
 from ...core.descriptions import ADDED_IN_311, ADDED_IN_312, PREVIEW_FEATURE
 from ...core.doc_category import DOC_CATEGORY_PRODUCTS
@@ -663,8 +665,13 @@ def post_save_actions(cls, info, instances, product):
         product.search_index_dirty = True
         product.save(update_fields=["search_index_dirty"])
 
-        for instance in instances:
-            cls.call_event(manager.product_variant_updated, instance.node)
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.PRODUCT_VARIANT_UPDATED
+        ):
+            for instance in instances:
+                cls.call_event(
+                    manager.product_variant_updated, instance.node, webhooks=webhooks
+                )
 
     @classmethod
     @traced_atomic_transaction()
diff --git a/saleor/graphql/product/bulk_mutations/product_variant_stocks_create.py b/saleor/graphql/product/bulk_mutations/product_variant_stocks_create.py
index ad3a3d82ced..1425e7eb86b 100644
--- a/saleor/graphql/product/bulk_mutations/product_variant_stocks_create.py
+++ b/saleor/graphql/product/bulk_mutations/product_variant_stocks_create.py
@@ -8,6 +8,8 @@
 from ....core.tracing import traced_atomic_transaction
 from ....permission.enums import ProductPermissions
 from ....warehouse.error_codes import StockErrorCode
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...channel import ChannelContext
 from ...core import ResolveInfo
 from ...core.doc_category import DOC_CATEGORY_PRODUCTS
@@ -61,10 +63,15 @@ def perform_mutation(cls, _root, info: ResolveInfo, /, **data):
                 raise ValidationError(errors)
             new_stocks = create_stocks(variant, stocks, warehouses)
 
-            for stock in new_stocks:
-                transaction.on_commit(
-                    lambda: manager.product_variant_back_in_stock(stock)
-                )
+            if webhooks := get_webhooks_for_event(
+                WebhookEventAsyncType.PRODUCT_VARIANT_BACK_IN_STOCK
+            ):
+                for stock in new_stocks:
+                    transaction.on_commit(
+                        lambda: manager.product_variant_back_in_stock(
+                            stock, webhooks=webhooks
+                        )
+                    )
 
         StocksWithAvailableQuantityByProductVariantIdCountryCodeAndChannelLoader(
             info.context
diff --git a/saleor/graphql/product/bulk_mutations/product_variant_stocks_delete.py b/saleor/graphql/product/bulk_mutations/product_variant_stocks_delete.py
index 6c9c3158381..683bf675ef2 100644
--- a/saleor/graphql/product/bulk_mutations/product_variant_stocks_delete.py
+++ b/saleor/graphql/product/bulk_mutations/product_variant_stocks_delete.py
@@ -6,6 +6,8 @@
 from ....permission.enums import ProductPermissions
 from ....product import models
 from ....warehouse import models as warehouse_models
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...channel import ChannelContext
 from ...core import ResolveInfo
 from ...core.doc_category import DOC_CATEGORY_PRODUCTS
@@ -74,8 +76,15 @@ def perform_mutation(cls, _root, info: ResolveInfo, /, **data):
             product_variant=variant, warehouse__pk__in=warehouses_pks
         )
 
-        for stock in stocks_to_delete:
-            transaction.on_commit(lambda: manager.product_variant_out_of_stock(stock))
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.PRODUCT_VARIANT_OUT_OF_STOCK
+        ):
+            for stock in stocks_to_delete:
+                transaction.on_commit(
+                    lambda: manager.product_variant_out_of_stock(
+                        stock, webhooks=webhooks
+                    )
+                )
 
         stocks_to_delete.delete()
 
diff --git a/saleor/graphql/product/bulk_mutations/product_variant_stocks_update.py b/saleor/graphql/product/bulk_mutations/product_variant_stocks_update.py
index b519d83b3fe..4e55350a0cf 100644
--- a/saleor/graphql/product/bulk_mutations/product_variant_stocks_update.py
+++ b/saleor/graphql/product/bulk_mutations/product_variant_stocks_update.py
@@ -9,6 +9,8 @@
 from ....permission.enums import ProductPermissions
 from ....product import models
 from ....warehouse import models as warehouse_models
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...channel import ChannelContext
 from ...core import ResolveInfo
 from ...core.doc_category import DOC_CATEGORY_PRODUCTS
@@ -92,6 +94,15 @@ def perform_mutation(cls, _root, info: ResolveInfo, /, **data):
     @traced_atomic_transaction()
     def update_or_create_variant_stocks(cls, variant, stocks_data, warehouses, manager):
         stocks = []
+        webhooks_stock_in = get_webhooks_for_event(
+            WebhookEventAsyncType.PRODUCT_VARIANT_BACK_IN_STOCK
+        )
+        webhooks_stock_out = get_webhooks_for_event(
+            WebhookEventAsyncType.PRODUCT_VARIANT_OUT_OF_STOCK
+        )
+        webhooks_stock_update = get_webhooks_for_event(
+            WebhookEventAsyncType.PRODUCT_VARIANT_STOCK_UPDATED
+        )
         for stock_data, warehouse in zip(stocks_data, warehouses):
             stock, is_created = warehouse_models.Stock.objects.get_or_create(
                 product_variant=variant, warehouse=warehouse
@@ -102,18 +113,26 @@ def update_or_create_variant_stocks(cls, variant, stocks_data, warehouses, manag
                 < stock_data["quantity"]
             ):
                 transaction.on_commit(
-                    lambda: manager.product_variant_back_in_stock(stock)
+                    lambda: manager.product_variant_back_in_stock(
+                        stock, webhooks=webhooks_stock_in
+                    )
                 )
 
             if stock_data["quantity"] <= 0 or (
                 stock_data["quantity"] - stock.quantity_allocated <= 0
             ):
                 transaction.on_commit(
-                    lambda: manager.product_variant_out_of_stock(stock)
+                    lambda: manager.product_variant_out_of_stock(
+                        stock, webhooks=webhooks_stock_out
+                    )
                 )
 
             stock.quantity = stock_data["quantity"]
             stocks.append(stock)
-            transaction.on_commit(lambda: manager.product_variant_stock_updated(stock))
+            transaction.on_commit(
+                lambda: manager.product_variant_stock_updated(
+                    stock, webhooks=webhooks_stock_update
+                )
+            )
 
         warehouse_models.Stock.objects.bulk_update(stocks, ["quantity"])
diff --git a/saleor/graphql/product/tests/benchmark/test_variant_stocks.py b/saleor/graphql/product/tests/benchmark/test_variant_stocks.py
index 9953a04a629..3d89dea9e54 100644
--- a/saleor/graphql/product/tests/benchmark/test_variant_stocks.py
+++ b/saleor/graphql/product/tests/benchmark/test_variant_stocks.py
@@ -9,15 +9,27 @@
 
 @pytest.mark.django_db
 @pytest.mark.count_queries(autouse=False)
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_create.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
 def test_product_variants_stocks_create(
     product_variant_back_in_stock_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     variant,
     warehouse,
     permission_manage_products,
     count_queries,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     query = """
     mutation ProductVariantStocksCreate($variantId: ID!, $stocks: [StockInput!]!){
         productVariantStocksCreate(variantId: $variantId, stocks: $stocks){
@@ -73,20 +85,34 @@ def test_product_variants_stocks_create(
         == stocks_count + len(stocks)
     )
     assert product_variant_back_in_stock_webhook_mock.call_count == 2
-    product_variant_back_in_stock_webhook_mock.assert_called_with(Stock.objects.last())
+    product_variant_back_in_stock_webhook_mock.assert_called_with(
+        Stock.objects.last(), webhooks=[any_webhook]
+    )
 
 
 @pytest.mark.django_db
 @pytest.mark.count_queries(autouse=False)
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_create.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
 def test_product_variants_stocks_create_with_single_webhook_called(
     product_variant_back_in_stock_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     variant,
     warehouse,
     permission_manage_products,
     count_queries,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     query = """
     mutation ProductVariantStocksCreate($variantId: ID!, $stocks: [StockInput!]!){
         productVariantStocksCreate(variantId: $variantId, stocks: $stocks){
@@ -137,7 +163,9 @@ def test_product_variants_stocks_create_with_single_webhook_called(
         == variant.stocks.count()
         == stocks_count + len(stocks)
     )
-    product_variant_back_in_stock_webhook_mock.assert_called_with(Stock.objects.last())
+    product_variant_back_in_stock_webhook_mock.assert_called_with(
+        Stock.objects.last(), webhooks=[any_webhook]
+    )
 
 
 PRODUCT_VARIANT_STOCKS_UPDATE_MUTATION = """
@@ -269,15 +297,27 @@ def test_product_variants_stocks_update_by_sku(
 
 @pytest.mark.django_db
 @pytest.mark.count_queries(autouse=False)
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
 def test_product_variants_stocks_delete_by_id(
     product_variant_out_of_stock_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     variant,
     warehouse,
     permission_manage_products,
     count_queries,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     variant_id = graphene.Node.to_global_id("ProductVariant", variant.pk)
     second_warehouse = Warehouse.objects.get(pk=warehouse.pk)
     second_warehouse.slug = "second warehouse"
@@ -311,20 +351,34 @@ def test_product_variants_stocks_delete_by_id(
         == stocks_count - 1
     )
     assert stock_to_delete not in Stock.objects.all()
-    product_variant_out_of_stock_webhook_mock.assert_called_once_with(stock_to_delete)
+    product_variant_out_of_stock_webhook_mock.assert_called_once_with(
+        stock_to_delete, webhooks=[any_webhook]
+    )
 
 
 @pytest.mark.django_db
 @pytest.mark.count_queries(autouse=False)
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
 def test_product_variants_stocks_delete_by_sku(
     product_variant_out_of_stock_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     variant,
     warehouse,
     permission_manage_products,
     count_queries,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     second_warehouse = Warehouse.objects.get(pk=warehouse.pk)
     second_warehouse.slug = "second warehouse"
     second_warehouse.pk = None
@@ -357,20 +411,34 @@ def test_product_variants_stocks_delete_by_sku(
         == stocks_count - 1
     )
     assert stock_to_delete not in Stock.objects.all()
-    product_variant_out_of_stock_webhook_mock.assert_called_once_with(stock_to_delete)
+    product_variant_out_of_stock_webhook_mock.assert_called_once_with(
+        stock_to_delete, webhooks=[any_webhook]
+    )
 
 
 @pytest.mark.django_db
 @pytest.mark.count_queries(autouse=False)
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
 def test_product_variants_stocks_delete_with_out_of_stock_webhook_many_calls(
     product_variant_out_of_stock_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     variant,
     warehouse,
     permission_manage_products,
     count_queries,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     query = """
     mutation ProductVariantStocksDelete($variantId: ID!, $warehouseIds: [ID!]!){
             productVariantStocksDelete(
diff --git a/saleor/graphql/product/tests/deprecated/test_product_variant_bulk_create.py b/saleor/graphql/product/tests/deprecated/test_product_variant_bulk_create.py
index 9d6c5f47f3d..b2ba3db7e61 100644
--- a/saleor/graphql/product/tests/deprecated/test_product_variant_bulk_create.py
+++ b/saleor/graphql/product/tests/deprecated/test_product_variant_bulk_create.py
@@ -60,14 +60,26 @@
 """
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations.product_variant_bulk_create."
+        "get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_created")
 def test_product_variant_bulk_create_by_name(
     product_variant_created_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product,
     size_attribute,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     # given
     product_variant_count = ProductVariant.objects.count()
     attribute_value_count = size_attribute.values.count()
@@ -109,14 +121,26 @@ def test_product_variant_bulk_create_by_name(
     assert product_variant_created_webhook_mock.call_count == data["count"]
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_create.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_created")
 def test_product_variant_bulk_create_by_attribute_id(
     product_variant_created_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product,
     size_attribute,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     product_variant_count = ProductVariant.objects.count()
     attribute_value_count = size_attribute.values.count()
     product_id = graphene.Node.to_global_id("Product", product.pk)
diff --git a/saleor/graphql/product/tests/mutations/test_product_bulk_create.py b/saleor/graphql/product/tests/mutations/test_product_bulk_create.py
index 550d0a3dbb0..a3f559e5fde 100644
--- a/saleor/graphql/product/tests/mutations/test_product_bulk_create.py
+++ b/saleor/graphql/product/tests/mutations/test_product_bulk_create.py
@@ -1194,19 +1194,30 @@ def test_product_bulk_create_with_variants_with_duplicated_sku(
     assert prod_2_errors[0]["code"] == ProductBulkCreateErrorCode.UNIQUE.name
 
 
+@patch(
+    "saleor.graphql.product.bulk_mutations."
+    "product_bulk_create.get_webhooks_for_event"
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_created")
 @patch("saleor.plugins.manager.PluginsManager.product_created")
 def test_product_bulk_create_with_variants_send_product_variant_created_event(
     product_created_webhook_mock,
     variant_created_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product_type,
     category,
     size_attribute,
     description_json,
     permission_manage_products,
+    gift_card_expiry_date,
+    any_webhook,
+    settings,
 ):
     # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
+
     description_json = json.dumps(description_json)
     product_type_id = graphene.Node.to_global_id("ProductType", product_type.pk)
     category_id = graphene.Node.to_global_id("Category", category.pk)
@@ -1501,9 +1512,16 @@ def test_product_bulk_create_with_variants_and_invalid_stock(
     assert data["count"] == 0
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_bulk_create.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.channel_updated")
 def test_product_bulk_create_with_variants_and_channel_listings(
     channel_updated_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product_type,
     category,
@@ -1511,7 +1529,12 @@ def test_product_bulk_create_with_variants_and_channel_listings(
     description_json,
     permission_manage_products,
     channel_USD,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     # given
     description_json = json.dumps(description_json)
     product_type_id = graphene.Node.to_global_id("ProductType", product_type.pk)
@@ -1629,7 +1652,9 @@ def test_product_bulk_create_with_variants_and_channel_listings(
 
     # 2 product channel listing and 2 variant channel listing were created but
     # all are using same channel so only one event should be sent
-    channel_updated_webhook_mock.assert_called_once_with(channel_USD)
+    channel_updated_webhook_mock.assert_called_once_with(
+        channel_USD, webhooks=[any_webhook]
+    )
 
 
 def test_product_bulk_create_with_variants_and_channel_listings_with_wrong_price(
diff --git a/saleor/graphql/product/tests/mutations/test_product_variant_bulk_create.py b/saleor/graphql/product/tests/mutations/test_product_variant_bulk_create.py
index 8f8833de764..490dd45c698 100644
--- a/saleor/graphql/product/tests/mutations/test_product_variant_bulk_create.py
+++ b/saleor/graphql/product/tests/mutations/test_product_variant_bulk_create.py
@@ -99,6 +99,12 @@
 """
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_create.get_webhooks_for_event"
+    )
+)
 @patch(
     "saleor.product.tasks.update_products_discounted_prices_for_promotion_task.delay"
 )
@@ -106,11 +112,17 @@
 def test_product_variant_bulk_create_by_name(
     product_variant_created_webhook_mock,
     update_products_discounted_prices_for_promotion_task_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product,
     size_attribute,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     # given
     product_variant_count = ProductVariant.objects.count()
     attribute_value_count = size_attribute.values.count()
@@ -170,6 +182,12 @@ def test_product_variant_bulk_create_by_name(
     )
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_create.get_webhooks_for_event"
+    )
+)
 @patch(
     "saleor.product.tasks.update_products_discounted_prices_for_promotion_task.delay"
 )
@@ -177,11 +195,17 @@ def test_product_variant_bulk_create_by_name(
 def test_product_variant_bulk_create_by_attribute_id(
     product_variant_created_webhook_mock,
     update_products_discounted_prices_for_promotion_task_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product,
     size_attribute,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     # given
     product_variant_count = ProductVariant.objects.count()
     attribute_value_count = size_attribute.values.count()
diff --git a/saleor/graphql/product/tests/mutations/test_product_variant_bulk_update.py b/saleor/graphql/product/tests/mutations/test_product_variant_bulk_update.py
index e2a5aa0df6c..f626be1bf44 100644
--- a/saleor/graphql/product/tests/mutations/test_product_variant_bulk_update.py
+++ b/saleor/graphql/product/tests/mutations/test_product_variant_bulk_update.py
@@ -67,6 +67,12 @@
 """
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_update.get_webhooks_for_event"
+    )
+)
 @patch(
     "saleor.product.tasks.update_products_discounted_prices_for_promotion_task.delay"
 )
@@ -74,11 +80,17 @@
 def test_product_variant_bulk_update(
     product_variant_created_webhook_mock,
     update_products_discounted_prices_for_promotion_task_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product_with_single_variant,
     size_attribute,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     # given
     variant = product_with_single_variant.variants.last()
     product_id = graphene.Node.to_global_id("Product", product_with_single_variant.pk)
@@ -124,17 +136,29 @@ def test_product_variant_bulk_update(
     )
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_update.get_webhooks_for_event"
+    )
+)
 @patch(
     "saleor.product.tasks.update_products_discounted_prices_for_promotion_task.delay"
 )
 def test_product_variant_bulk_update_stocks(
     update_products_discounted_prices_for_promotion_task_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     variant_with_many_stocks,
     warehouse,
     size_attribute,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     # given
     variant = variant_with_many_stocks
     product_id = graphene.Node.to_global_id("Product", variant.product_id)
diff --git a/saleor/graphql/product/tests/mutations/test_product_variant_stocks_update.py b/saleor/graphql/product/tests/mutations/test_product_variant_stocks_update.py
index 09408e31f65..eaae42952fa 100644
--- a/saleor/graphql/product/tests/mutations/test_product_variant_stocks_update.py
+++ b/saleor/graphql/product/tests/mutations/test_product_variant_stocks_update.py
@@ -243,10 +243,26 @@ def test_update_or_create_variant_stocks(variant, warehouses):
     }
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_update.get_webhooks_for_event"
+    )
+)
+@patch("saleor.plugins.manager.PluginsManager.product_variant_stock_updated")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
 def test_update_or_create_variant_stocks_when_stock_out_of_quantity(
-    back_in_stock_webhook_trigger, variant, warehouses
+    back_in_stock_webhook_trigger,
+    stock_updated_webhook_trigger,
+    mocked_get_webhooks_for_event,
+    variant,
+    warehouses,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     stock = Stock.objects.create(
         product_variant=variant,
         warehouse=warehouses[0],
@@ -264,7 +280,8 @@ def test_update_or_create_variant_stocks_when_stock_out_of_quantity(
     assert {stock.quantity for stock in variant.stocks.all()} == {
         data["quantity"] for data in stocks_data
     }
-    back_in_stock_webhook_trigger.assert_called_once_with(stock)
+    back_in_stock_webhook_trigger.assert_called_once_with(stock, webhooks=[any_webhook])
+    stock_updated_webhook_trigger.assert_called_once_with(stock, webhooks=[any_webhook])
     assert variant.stocks.all()[0].quantity == 10
 
 
@@ -286,14 +303,24 @@ def test_update_or_create_variant_stocks_empty_stocks_data(variant, warehouses):
     assert stock.quantity == 5
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_update.get_webhooks_for_event"
+    )
+)
+@patch("saleor.plugins.manager.PluginsManager.product_variant_stock_updated")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
 def test_update_or_create_variant_with_back_in_stock_webhooks_only_success(
     product_variant_stock_out_of_stock_webhook,
     product_variant_back_in_stock_webhook,
+    product_variant_stock_updated_webhook,
+    mocked_get_webhooks_for_event,
     settings,
     variant,
     warehouses,
+    any_webhook,
 ):
     Stock.objects.bulk_create(
         [
@@ -301,7 +328,7 @@ def test_update_or_create_variant_with_back_in_stock_webhooks_only_success(
             for warehouse in warehouses
         ]
     )
-
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
     settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     plugins = get_plugins_manager()
     stocks_data = [
@@ -317,11 +344,20 @@ def test_update_or_create_variant_with_back_in_stock_webhooks_only_success(
 
     flush_post_commit_hooks()
     product_variant_back_in_stock_webhook.assert_called_once_with(
-        Stock.objects.all()[1]
+        Stock.objects.all()[1], webhooks=[any_webhook]
+    )
+    product_variant_stock_updated_webhook.assert_called_once_with(
+        Stock.objects.all()[1], webhooks=[any_webhook]
     )
     product_variant_stock_out_of_stock_webhook.assert_not_called()
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_update.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_stock_updated")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
@@ -329,9 +365,11 @@ def test_update_or_create_variant_with_back_in_stock_webhooks_only_failed(
     product_variant_stock_out_of_stock_webhook,
     product_variant_back_in_stock_webhook,
     product_variant_stock_update_webhook,
+    mocked_get_webhooks_for_event,
     settings,
     variant,
     warehouses,
+    any_webhook,
 ):
     Stock.objects.bulk_create(
         [
@@ -340,6 +378,7 @@ def test_update_or_create_variant_with_back_in_stock_webhooks_only_failed(
         ]
     )
 
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
     settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     plugins = get_plugins_manager()
     stocks_data = [
@@ -356,21 +395,32 @@ def test_update_or_create_variant_with_back_in_stock_webhooks_only_failed(
     flush_post_commit_hooks()
     product_variant_back_in_stock_webhook.assert_not_called()
     product_variant_stock_out_of_stock_webhook.assert_called_once_with(
-        Stock.objects.all()[1]
+        Stock.objects.all()[1], webhooks=[any_webhook]
     )
     assert product_variant_stock_update_webhook.call_count == 1
-    product_variant_stock_update_webhook.assert_called_with(Stock.objects.all()[1])
+    product_variant_stock_update_webhook.assert_called_with(
+        Stock.objects.all()[1], webhooks=[any_webhook]
+    )
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_update.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_stock_updated")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
 def test_update_or_create_variant_with_back_in_stock_webhooks_with_allocations(
     product_variant_stock_out_of_stock_webhook,
     product_variant_back_in_stock_webhook,
+    product_variant_stock_updated_webhook,
+    mocked_get_webhooks_for_event,
     settings,
     variant,
     warehouse,
+    any_webhook,
 ):
     stock_quantity = 4
     stock = Stock.objects.create(
@@ -378,6 +428,7 @@ def test_update_or_create_variant_with_back_in_stock_webhooks_with_allocations(
     )
 
     # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
     settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     plugins = get_plugins_manager()
     stock.quantity_allocated = stock_quantity
@@ -392,18 +443,33 @@ def test_update_or_create_variant_with_back_in_stock_webhooks_with_allocations(
     )
     # then
     flush_post_commit_hooks()
-    product_variant_back_in_stock_webhook.assert_called_once_with(stock)
+    product_variant_back_in_stock_webhook.assert_called_once_with(
+        stock, webhooks=[any_webhook]
+    )
+    product_variant_stock_updated_webhook.assert_called_once_with(
+        stock, webhooks=[any_webhook]
+    )
     product_variant_stock_out_of_stock_webhook.assert_not_called()
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_update.get_webhooks_for_event"
+    )
+)
+@patch("saleor.plugins.manager.PluginsManager.product_variant_stock_updated")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
 def test_update_or_create_variant_with_out_of_stock_webhooks_with_allocations(
     product_variant_stock_out_of_stock_webhook,
     product_variant_back_in_stock_webhook,
+    product_variant_stock_updated_webhook,
+    mocked_get_webhooks_for_event,
     settings,
     variant,
     warehouse,
+    any_webhook,
 ):
     stock_quantity = 4
     stock = Stock.objects.create(
@@ -411,6 +477,7 @@ def test_update_or_create_variant_with_out_of_stock_webhooks_with_allocations(
     )
 
     # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
     settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     plugins = get_plugins_manager()
     stock.quantity_allocated = stock_quantity - 1
@@ -425,10 +492,21 @@ def test_update_or_create_variant_with_out_of_stock_webhooks_with_allocations(
     )
     # then
     flush_post_commit_hooks()
-    product_variant_stock_out_of_stock_webhook.assert_called_once_with(stock)
+    product_variant_stock_out_of_stock_webhook.assert_called_once_with(
+        stock, webhooks=[any_webhook]
+    )
+    product_variant_stock_updated_webhook.assert_called_once_with(
+        stock, webhooks=[any_webhook]
+    )
     product_variant_back_in_stock_webhook.assert_not_called()
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_stocks_update.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_stock_updated")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_back_in_stock")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_out_of_stock")
@@ -436,9 +514,11 @@ def test_update_or_create_variant_stocks_with_out_of_stock_webhook_only(
     product_variant_stock_out_of_stock_webhook,
     product_variant_back_in_stock_webhook,
     product_variant_stock_update_webhook,
+    mocked_get_webhooks_for_event,
     settings,
     variant,
     warehouses,
+    any_webhook,
 ):
     Stock.objects.bulk_create(
         [
@@ -447,6 +527,7 @@ def test_update_or_create_variant_stocks_with_out_of_stock_webhook_only(
         ]
     )
 
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
     settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
 
     plugins = get_plugins_manager()
@@ -467,10 +548,12 @@ def test_update_or_create_variant_stocks_with_out_of_stock_webhook_only(
     flush_post_commit_hooks()
 
     product_variant_stock_out_of_stock_webhook.assert_called_once_with(
-        Stock.objects.last()
+        Stock.objects.last(), webhooks=[any_webhook]
     )
     assert product_variant_stock_update_webhook.call_count == 2
-    product_variant_stock_update_webhook.assert_called_with(Stock.objects.last())
+    product_variant_stock_update_webhook.assert_called_with(
+        Stock.objects.last(), webhooks=[any_webhook]
+    )
     product_variant_back_in_stock_webhook.assert_not_called()
 
 
diff --git a/saleor/graphql/product/tests/test_bulk_delete.py b/saleor/graphql/product/tests/test_bulk_delete.py
index a7a82ec13d7..50555d7d865 100644
--- a/saleor/graphql/product/tests/test_bulk_delete.py
+++ b/saleor/graphql/product/tests/test_bulk_delete.py
@@ -80,7 +80,7 @@ def test_delete_categories(
     assert set(kwargs["product_ids"]) == {product.id for product in product_list}
 
 
-@patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@patch("saleor.product.utils.get_webhooks_for_event")
 @patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_categories_trigger_webhook(
     mocked_webhook_trigger,
@@ -158,14 +158,22 @@ def test_delete_categories_with_images(
     assert not Thumbnail.objects.all()
 
 
+@patch("saleor.product.utils.get_webhooks_for_event")
 @patch("saleor.plugins.manager.PluginsManager.product_updated")
 def test_delete_categories_trigger_product_updated_webhook(
     product_updated_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     category_list,
     product_list,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
+
     first_product = product_list[0]
     first_product.category = category_list[0]
     first_product.save()
@@ -364,13 +372,25 @@ def test_delete_collections_with_images(
     assert not Thumbnail.objects.all()
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "collection_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.collection_deleted")
 def test_delete_collections_trigger_collection_deleted_webhook(
     collection_deleted_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     collection_list,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     variables = {
         "ids": [
             graphene.Node.to_global_id("Collection", collection.id)
@@ -391,14 +411,26 @@ def test_delete_collections_trigger_collection_deleted_webhook(
     assert len(collection_list) == collection_deleted_mock.call_count
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "collection_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_updated")
 def test_delete_collections_trigger_product_updated_webhook(
     product_updated_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     collection_list,
     product_list,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     for collection in collection_list:
         collection.products.add(*product_list)
     variables = {
@@ -573,7 +605,12 @@ def test_delete_products_with_images(
     mocked_recalculate_orders_task.assert_not_called()
 
 
-@patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 @patch("saleor.order.tasks.recalculate_orders_task.delay")
 def test_delete_products_trigger_webhook(
@@ -608,7 +645,12 @@ def test_delete_products_trigger_webhook(
     mocked_recalculate_orders_task.assert_not_called()
 
 
-@patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_products_without_variants(
     mocked_webhook_trigger,
@@ -902,16 +944,26 @@ def test_delete_product_types_with_file_attributes(
 """
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations.product_variant_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_deleted")
 @patch("saleor.order.tasks.recalculate_orders_task.delay")
 def test_delete_product_variants_by_sku(
     mocked_recalculate_orders_task,
     product_variant_deleted_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product_variant_list,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
     # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     product = product_variant_list[0].product
 
     variant = product.variants.get(sku="123")
@@ -945,6 +997,12 @@ def test_delete_product_variants_by_sku(
     mocked_recalculate_orders_task.assert_not_called()
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch(
     "saleor.product.tasks.update_products_discounted_prices_for_promotion_task.delay"
 )
@@ -954,11 +1012,16 @@ def test_delete_product_variants_by_sku_task_for_recalculate_product_prices_call
     mocked_recalculate_orders_task,
     product_variant_deleted_webhook_mock,
     update_products_discounted_price_task_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product_list,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
     # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     variants = [product.variants.first() for product in product_list]
     assert ProductVariantChannelListing.objects.filter(
         variant_id__in=[variant.id for variant in variants]
@@ -1003,15 +1066,27 @@ def test_delete_product_variants_by_sku_task_for_recalculate_product_prices_call
 """
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_deleted")
 @patch("saleor.order.tasks.recalculate_orders_task.delay")
 def test_delete_product_variants(
     mocked_recalculate_orders_task,
     product_variant_deleted_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product_variant_list,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     query = PRODUCT_VARIANT_BULK_DELETE_MUTATION
 
     product = product_variant_list[0].product
@@ -1047,6 +1122,12 @@ def test_delete_product_variants(
     mocked_recalculate_orders_task.assert_not_called()
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch(
     "saleor.product.tasks.update_products_discounted_prices_for_promotion_task.delay"
 )
@@ -1056,10 +1137,16 @@ def test_delete_product_variants_task_for_recalculate_product_prices_called(
     mocked_recalculate_orders_task,
     product_variant_deleted_webhook_mock,
     update_products_discounted_price_task_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product_list,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     query = PRODUCT_VARIANT_BULK_DELETE_MUTATION
 
     variants = [product.variants.first() for product in product_list]
@@ -1167,6 +1254,12 @@ def test_delete_product_variants_removes_checkout_lines(
     assert old_quantity == calculate_checkout_quantity(lines) + 2
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.product.signals.delete_from_storage_task")
 @patch("saleor.plugins.manager.PluginsManager.product_variant_deleted")
 @patch("saleor.order.tasks.recalculate_orders_task.delay")
@@ -1174,12 +1267,18 @@ def test_delete_product_variants_with_images(
     mocked_recalculate_orders_task,
     product_variant_deleted_webhook_mock,
     delete_from_storage_task_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product_variant_list,
     image_list,
     permission_manage_products,
     media_root,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     query = PRODUCT_VARIANT_BULK_DELETE_MUTATION
 
     assert ProductVariantChannelListing.objects.filter(
@@ -1388,16 +1487,28 @@ def test_product_delete_removes_reference_to_page(
     assert not data["errors"]
 
 
+@patch(
+    (
+        "saleor.graphql.product.bulk_mutations."
+        "product_variant_bulk_delete.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_deleted")
 @patch("saleor.order.tasks.recalculate_orders_task.delay")
 def test_delete_product_variants_with_file_attribute(
     mocked_recalculate_orders_task,
     product_variant_deleted_webhook_mock,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     product_variant_list,
     permission_manage_products,
     file_attribute,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     query = PRODUCT_VARIANT_BULK_DELETE_MUTATION
 
     assert ProductVariantChannelListing.objects.filter(
diff --git a/saleor/graphql/product/tests/test_category.py b/saleor/graphql/product/tests/test_category.py
index 5dc0ae2f750..71b479305b2 100644
--- a/saleor/graphql/product/tests/test_category.py
+++ b/saleor/graphql/product/tests/test_category.py
@@ -1207,7 +1207,7 @@ def test_category_delete_mutation(
 
 
 @freeze_time("2022-05-12 12:00:00")
-@patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@patch("saleor.product.utils.get_webhooks_for_event")
 @patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_category_delete_trigger_webhook(
     mocked_webhook_trigger,
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index 4a6d0798d5a..259de2c94f7 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -15087,6 +15087,9 @@ type Mutation {
   Note: this API is currently in Feature Preview and can be subject to changes at later point. 
   
   Requires one of the following permissions: MANAGE_PRODUCTS.
+  
+  Triggers the following webhook events:
+  - PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated.
   """
   stockBulkUpdate(
     """Policies of error handling. DEFAULT: REJECT_EVERYTHING"""
@@ -15094,7 +15097,7 @@ type Mutation {
 
     """Input list of stocks to update."""
     stocks: [StockBulkUpdateInput!]!
-  ): StockBulkUpdate @doc(category: "Products")
+  ): StockBulkUpdate @doc(category: "Products") @webhookEventsInfo(asyncEvents: [PRODUCT_VARIANT_STOCK_UPDATED], syncEvents: [])
 
   """
   Creates a new staff notification recipient. 
@@ -20577,8 +20580,11 @@ Added in Saleor 3.13.
 Note: this API is currently in Feature Preview and can be subject to changes at later point. 
 
 Requires one of the following permissions: MANAGE_PRODUCTS.
+
+Triggers the following webhook events:
+- PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated.
 """
-type StockBulkUpdate @doc(category: "Products") {
+type StockBulkUpdate @doc(category: "Products") @webhookEventsInfo(asyncEvents: [PRODUCT_VARIANT_STOCK_UPDATED], syncEvents: []) {
   """Returns how many objects were updated."""
   count: Int!
 
diff --git a/saleor/graphql/shipping/bulk_mutations/shipping_price_bulk_delete.py b/saleor/graphql/shipping/bulk_mutations/shipping_price_bulk_delete.py
index 6acd948e3b4..52970148d31 100644
--- a/saleor/graphql/shipping/bulk_mutations/shipping_price_bulk_delete.py
+++ b/saleor/graphql/shipping/bulk_mutations/shipping_price_bulk_delete.py
@@ -2,6 +2,8 @@
 
 from ....permission.enums import ShippingPermissions
 from ....shipping import models
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core import ResolveInfo
 from ...core.mutations import ModelBulkDeleteMutation
 from ...core.types import NonNullList, ShippingError
@@ -46,6 +48,9 @@ def get_nodes_or_error(
     def bulk_action(cls, info: ResolveInfo, queryset, /):
         shipping_methods = [sm for sm in queryset]
         queryset.delete()
-        manager = get_plugin_manager_promise(info.context).get()
-        for method in shipping_methods:
-            manager.shipping_price_deleted(method)
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.SHIPPING_PRICE_DELETED
+        ):
+            manager = get_plugin_manager_promise(info.context).get()
+            for method in shipping_methods:
+                manager.shipping_price_deleted(method, webhooks=webhooks)
diff --git a/saleor/graphql/shipping/bulk_mutations/shipping_zone_bulk_delete.py b/saleor/graphql/shipping/bulk_mutations/shipping_zone_bulk_delete.py
index 6761e18e362..f30f0630abd 100644
--- a/saleor/graphql/shipping/bulk_mutations/shipping_zone_bulk_delete.py
+++ b/saleor/graphql/shipping/bulk_mutations/shipping_zone_bulk_delete.py
@@ -2,6 +2,8 @@
 
 from ....permission.enums import ShippingPermissions
 from ....shipping import models
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core import ResolveInfo
 from ...core.mutations import ModelBulkDeleteMutation
 from ...core.types import NonNullList, ShippingError
@@ -29,6 +31,9 @@ class Meta:
     def bulk_action(cls, info: ResolveInfo, queryset, /):
         zones = [zone for zone in queryset]
         queryset.delete()
-        manager = get_plugin_manager_promise(info.context).get()
-        for zone in zones:
-            manager.shipping_zone_deleted(zone)
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.SHIPPING_ZONE_DELETED
+        ):
+            manager = get_plugin_manager_promise(info.context).get()
+            for zone in zones:
+                manager.shipping_zone_deleted(zone, webhooks=webhooks)
diff --git a/saleor/graphql/shipping/tests/mutations/test_bulk_delete.py b/saleor/graphql/shipping/tests/mutations/test_bulk_delete.py
index f615f3e56ef..7d39ac88164 100644
--- a/saleor/graphql/shipping/tests/mutations/test_bulk_delete.py
+++ b/saleor/graphql/shipping/tests/mutations/test_bulk_delete.py
@@ -65,7 +65,10 @@ def test_delete_shipping_methods(
     ).exists()
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    "saleor.graphql.shipping.bulk_mutations."
+    "shipping_price_bulk_delete.get_webhooks_for_event"
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_shipping_methods_trigger_multiple_webhook_events(
     mocked_webhook_trigger,
@@ -136,7 +139,10 @@ def test_delete_shipping_zones(
     ).exists()
 
 
-@mock.patch("saleor.plugins.webhook.plugin.get_webhooks_for_event")
+@mock.patch(
+    "saleor.graphql.shipping.bulk_mutations."
+    "shipping_zone_bulk_delete.get_webhooks_for_event"
+)
 @mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_async")
 def test_delete_shipping_zones_trigger_multiple_webhook_events(
     mocked_webhook_trigger,
diff --git a/saleor/graphql/warehouse/bulk_mutations/stock_bulk_update.py b/saleor/graphql/warehouse/bulk_mutations/stock_bulk_update.py
index a0a512878b1..e7c79d30464 100644
--- a/saleor/graphql/warehouse/bulk_mutations/stock_bulk_update.py
+++ b/saleor/graphql/warehouse/bulk_mutations/stock_bulk_update.py
@@ -8,6 +8,8 @@
 from ....permission.enums import ProductPermissions
 from ....warehouse import models
 from ....warehouse.error_codes import StockBulkUpdateErrorCode
+from ....webhook.event_types import WebhookEventAsyncType
+from ....webhook.utils import get_webhooks_for_event
 from ...core.descriptions import ADDED_IN_313, PREVIEW_FEATURE
 from ...core.doc_category import DOC_CATEGORY_PRODUCTS
 from ...core.enums import ErrorPolicyEnum
@@ -18,6 +20,7 @@
     NonNullList,
     StockBulkUpdateError,
 )
+from ...core.utils import WebhookEventInfo
 from ...core.validators import validate_one_of_args_is_in_mutation
 from ...plugins.dataloaders import get_plugin_manager_promise
 from ..types import Stock
@@ -88,6 +91,12 @@ class Meta:
         doc_category = DOC_CATEGORY_PRODUCTS
         permissions = (ProductPermissions.MANAGE_PRODUCTS,)
         error_type_class = StockBulkUpdateError
+        webhook_events_info = [
+            WebhookEventInfo(
+                type=WebhookEventAsyncType.PRODUCT_VARIANT_STOCK_UPDATED,
+                description="A product variant stock details were updated.",
+            )
+        ]
 
     @classmethod
     def validate_variant(
@@ -354,8 +363,13 @@ def save_stocks(cls, instances_data_with_errors_list):
     @classmethod
     def post_save_actions(cls, info, instances):
         manager = get_plugin_manager_promise(info.context).get()
-        for instance in instances:
-            cls.call_event(manager.product_variant_stock_updated, instance)
+        if webhooks := get_webhooks_for_event(
+            WebhookEventAsyncType.PRODUCT_VARIANT_STOCK_UPDATED
+        ):
+            for instance in instances:
+                cls.call_event(
+                    manager.product_variant_stock_updated, instance, webhooks=webhooks
+                )
 
     @classmethod
     def get_results(cls, instances_data_with_errors_list, reject_everything=False):
diff --git a/saleor/graphql/warehouse/tests/benchmark/test_stock_bulk_update.py b/saleor/graphql/warehouse/tests/benchmark/test_stock_bulk_update.py
index afd62462eaa..6879f873b7c 100644
--- a/saleor/graphql/warehouse/tests/benchmark/test_stock_bulk_update.py
+++ b/saleor/graphql/warehouse/tests/benchmark/test_stock_bulk_update.py
@@ -1,5 +1,6 @@
 import graphene
 import pytest
+from django.db import connection
 
 from .....warehouse.models import Stock
 from ....tests.utils import get_graphql_content
@@ -35,8 +36,11 @@ def test_stocks_bulk_update_queries_count(
     permission_manage_products,
     django_assert_num_queries,
     count_queries,
+    any_webhook,
+    settings,
 ):
     # given
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     variant_1 = variant_with_many_stocks
     variant_2 = variant
 
@@ -67,7 +71,7 @@ def test_stocks_bulk_update_queries_count(
     ]
 
     # test number of queries when single object is updated
-    with django_assert_num_queries(9):
+    with django_assert_num_queries(10):
         staff_api_client.user.user_permissions.add(permission_manage_products)
         response = staff_api_client.post_graphql(
             STOCKS_BULK_UPDATE_MUTATION, {"stocks": stocks_input}
@@ -75,6 +79,14 @@ def test_stocks_bulk_update_queries_count(
         content = get_graphql_content(response)
         data = content["data"]["stockBulkUpdate"]
         assert data["count"] == 1
+        webhook_queries_count = sum(
+            [
+                1
+                for query in connection.queries
+                if query["sql"].startswith('SELECT "webhook_webhook"')
+            ]
+        )
+        assert webhook_queries_count == 1
 
     stocks_input += [
         {
@@ -95,11 +107,20 @@ def test_stocks_bulk_update_queries_count(
     ]
 
     # Test number of queries when multiple objects are updated
-    with django_assert_num_queries(9):
+    with django_assert_num_queries(10):
         staff_api_client.user.user_permissions.add(permission_manage_products)
         response = staff_api_client.post_graphql(
             STOCKS_BULK_UPDATE_MUTATION, {"stocks": stocks_input}
         )
+
         content = get_graphql_content(response)
         data = content["data"]["stockBulkUpdate"]
         assert data["count"] == 4
+        webhook_queries_count = sum(
+            [
+                1
+                for query in connection.queries
+                if query["sql"].startswith('SELECT "webhook_webhook"')
+            ]
+        )
+        assert webhook_queries_count == 2
diff --git a/saleor/graphql/warehouse/tests/bulk_mutations/test_stock_bulk_update.py b/saleor/graphql/warehouse/tests/bulk_mutations/test_stock_bulk_update.py
index bfe63a63446..0951f9df963 100644
--- a/saleor/graphql/warehouse/tests/bulk_mutations/test_stock_bulk_update.py
+++ b/saleor/graphql/warehouse/tests/bulk_mutations/test_stock_bulk_update.py
@@ -76,14 +76,25 @@ def test_stocks_bulk_update_using_ids(
     assert stock_2.quantity == new_quantity_2
 
 
+@patch(
+    (
+        "saleor.graphql.warehouse.bulk_mutations."
+        "stock_bulk_update.get_webhooks_for_event"
+    )
+)
 @patch("saleor.plugins.manager.PluginsManager.product_variant_stock_updated")
 def test_stocks_bulk_update_send_stock_updated_event(
     product_variant_stock_update_webhook,
+    mocked_get_webhooks_for_event,
     staff_api_client,
     variant_with_many_stocks,
     permission_manage_products,
+    any_webhook,
+    settings,
 ):
     # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     variant = variant_with_many_stocks
     variant_id = graphene.Node.to_global_id("ProductVariant", variant.pk)
     stocks = variant.stocks.all()
diff --git a/saleor/order/actions.py b/saleor/order/actions.py
index c88f86615b5..0c3200cc31c 100644
--- a/saleor/order/actions.py
+++ b/saleor/order/actions.py
@@ -174,6 +174,8 @@ def cancel_order(
     user: Optional[User],
     app: Optional["App"],
     manager: "PluginsManager",
+    webhooks_cancelled=None,
+    webhooks_updated=None,
 ):
     """Cancel order.
 
@@ -186,8 +188,8 @@ def cancel_order(
         order.status = OrderStatus.CANCELED
         order.save(update_fields=["status", "updated_at"])
 
-        call_event(manager.order_cancelled, order)
-        call_event(manager.order_updated, order)
+        call_event(manager.order_cancelled, order, webhooks=webhooks_cancelled)
+        call_event(manager.order_updated, order, webhooks=webhooks_updated)
 
         call_event(send_order_canceled_confirmation, order, user, app, manager)
 
diff --git a/saleor/plugins/base_plugin.py b/saleor/plugins/base_plugin.py
index d3e0f85afc5..abecfe43158 100644
--- a/saleor/plugins/base_plugin.py
+++ b/saleor/plugins/base_plugin.py
@@ -77,7 +77,7 @@
     from ..shipping.models import ShippingMethod, ShippingZone
     from ..site.models import SiteSettings
     from ..tax.models import TaxClass
-    from ..warehouse.models import Warehouse
+    from ..warehouse.models import Stock, Warehouse
 
 PluginConfigurationType = List[dict]
 RequestorOrLazyObject = Union[SimpleLazyObject, "Requestor"]
@@ -406,7 +406,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a category is
     # deleted.
-    category_deleted: Callable[["Category", None], None]
+    category_deleted: Callable[["Category", None, None], None]
 
     # Trigger when category is updated.
     #
@@ -430,7 +430,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a channel is
     # updated.
-    channel_updated: Callable[["Channel", None], None]
+    channel_updated: Callable[["Channel", None, None], None]
 
     # Trigger when channel status is changed.
     #
@@ -485,7 +485,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a collection is
     # deleted.
-    collection_deleted: Callable[["Collection", Any], Any]
+    collection_deleted: Callable[["Collection", Any, None], Any]
 
     # Trigger when collection is updated.
     #
@@ -511,19 +511,19 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a user is
     # deleted.
-    customer_deleted: Callable[["User", Any], Any]
+    customer_deleted: Callable[["User", Any, None], Any]
 
     # Trigger when user is updated.
     #
     # Overwrite this method if you need to trigger specific logic after a user is
     # updated.
-    customer_updated: Callable[["User", Any], Any]
+    customer_updated: Callable[["User", Any, None], Any]
 
     # Trigger when user metadata is updated.
     #
     # Overwrite this method if you need to trigger specific logic after a user
     # metadata is updated.
-    customer_metadata_updated: Callable[["User", Any], Any]
+    customer_metadata_updated: Callable[["User", Any, None], Any]
 
     # Handle authentication request.
     #
@@ -643,13 +643,13 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a gift card is
     # created.
-    gift_card_created: Callable[["GiftCard", None], None]
+    gift_card_created: Callable[["GiftCard", None, None], None]
 
     # Trigger when gift card is deleted.
     #
     # Overwrite this method if you need to trigger specific logic after a gift card is
     # deleted.
-    gift_card_deleted: Callable[["GiftCard", None], None]
+    gift_card_deleted: Callable[["GiftCard", None, None], None]
 
     # Trigger when gift card is updated.
     #
@@ -667,7 +667,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a gift card
     # status is changed.
-    gift_card_status_changed: Callable[["GiftCard", None], None]
+    gift_card_status_changed: Callable[["GiftCard", None, None], None]
 
     # Trigger when gift cards export is completed.
     #
@@ -744,7 +744,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a menu is
     # deleted.
-    menu_deleted: Callable[["Menu", None], None]
+    menu_deleted: Callable[["Menu", None, None], None]
 
     # Trigger when menu is updated.
     #
@@ -762,7 +762,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a menu item is
     # deleted.
-    menu_item_deleted: Callable[["MenuItem", None], None]
+    menu_item_deleted: Callable[["MenuItem", None, None], None]
 
     # Trigger when menu item is updated.
     #
@@ -779,7 +779,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic when an order is
     # canceled.
-    order_cancelled: Callable[["Order", Any], Any]
+    order_cancelled: Callable[["Order", Any, None], Any]
 
     # Trigger when order is expired.
     #
@@ -833,7 +833,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic when an order is
     # changed.
-    order_updated: Callable[["Order", Any], Any]
+    order_updated: Callable[["Order", Any, None], Any]
 
     # Trigger when order metadata is updated.
     #
@@ -950,19 +950,19 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a product is
     # created.
-    product_created: Callable[["Product", Any], Any]
+    product_created: Callable[["Product", Any, None], Any]
 
     # Trigger when product is deleted.
     #
     # Overwrite this method if you need to trigger specific logic after a product is
     # deleted.
-    product_deleted: Callable[["Product", List[int], Any], Any]
+    product_deleted: Callable[["Product", List[int], Any, None], Any]
 
     # Trigger when product is updated.
     #
     # Overwrite this method if you need to trigger specific logic after a product is
     # updated.
-    product_updated: Callable[["Product", Any], Any]
+    product_updated: Callable[["Product", Any, None], Any]
 
     # Trigger when product media is created.
     #
@@ -992,19 +992,19 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a product
     # variant is created.
-    product_variant_created: Callable[["ProductVariant", Any], Any]
+    product_variant_created: Callable[["ProductVariant", Any, None], Any]
 
     # Trigger when product variant is deleted.
     #
     # Overwrite this method if you need to trigger specific logic after a product
     # variant is deleted.
-    product_variant_deleted: Callable[["ProductVariant", Any], Any]
+    product_variant_deleted: Callable[["ProductVariant", Any, None], Any]
 
     # Trigger when product variant is updated.
     #
     # Overwrite this method if you need to trigger specific logic after a product
     # variant is updated.
-    product_variant_updated: Callable[["ProductVariant", Any], Any]
+    product_variant_updated: Callable[["ProductVariant", Any, None], Any]
 
     # Trigger when product variant metadata is updated.
     #
@@ -1012,6 +1012,24 @@ def __str__(self):
     # variant metadata is updated.
     product_variant_metadata_updated: Callable[["ProductVariant", Any], Any]
 
+    # Trigger when product variant is out of stock.
+    #
+    # Overwrite this method if you need to trigger specific logic after a product
+    # variant is out of stock.
+    product_variant_out_of_stock: Callable[["Stock", None, None], Any]
+
+    # Trigger when product variant is back in stock.
+    #
+    # Overwrite this method if you need to trigger specific logic after a product
+    # variant is back in stock.
+    product_variant_back_in_stock: Callable[["Stock", None, None], Any]
+
+    # Trigger when product variant stock is updated.
+    #
+    # Overwrite this method if you need to trigger specific logic after a product
+    # variant stock is updated.
+    product_variant_stock_updated: Callable[["Stock", None, None], Any]
+
     # Trigger when a product export is completed.
     #
     # Overwrite this method if you need to trigger specific logic after a product
@@ -1097,7 +1115,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a shipping
     # price is deleted.
-    shipping_price_deleted: Callable[["ShippingMethod", None], None]
+    shipping_price_deleted: Callable[["ShippingMethod", None, None], None]
 
     # Trigger when shipping price is updated.
     #
@@ -1115,7 +1133,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a shipping zone
     # is deleted.
-    shipping_zone_deleted: Callable[["ShippingZone", None], None]
+    shipping_zone_deleted: Callable[["ShippingZone", None, None], None]
 
     # Trigger when shipping zone is updated.
     #
@@ -1201,7 +1219,7 @@ def __str__(self):
     #
     # Overwrite this method if you need to trigger specific logic after a voucher is
     # deleted.
-    voucher_deleted: Callable[["Voucher", None], None]
+    voucher_deleted: Callable[["Voucher", None, None], None]
 
     # Trigger when voucher is updated.
     #
diff --git a/saleor/plugins/manager.py b/saleor/plugins/manager.py
index 28096db6b0d..3a944c95892 100644
--- a/saleor/plugins/manager.py
+++ b/saleor/plugins/manager.py
@@ -587,18 +587,22 @@ def customer_created(self, customer: "User"):
         default_value = None
         return self.__run_method_on_plugins("customer_created", default_value, customer)
 
-    def customer_deleted(self, customer: "User"):
+    def customer_deleted(self, customer: "User", webhooks=None):
         default_value = None
-        return self.__run_method_on_plugins("customer_deleted", default_value, customer)
+        return self.__run_method_on_plugins(
+            "customer_deleted", default_value, customer, webhooks=webhooks
+        )
 
-    def customer_updated(self, customer: "User"):
+    def customer_updated(self, customer: "User", webhooks=None):
         default_value = None
-        return self.__run_method_on_plugins("customer_updated", default_value, customer)
+        return self.__run_method_on_plugins(
+            "customer_updated", default_value, customer, webhooks=webhooks
+        )
 
-    def customer_metadata_updated(self, customer: "User"):
+    def customer_metadata_updated(self, customer: "User", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "customer_metadata_updated", default_value, customer
+            "customer_metadata_updated", default_value, customer, webhooks=webhooks
         )
 
     def collection_created(self, collection: "Collection"):
@@ -613,10 +617,10 @@ def collection_updated(self, collection: "Collection"):
             "collection_updated", default_value, collection
         )
 
-    def collection_deleted(self, collection: "Collection"):
+    def collection_deleted(self, collection: "Collection", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "collection_deleted", default_value, collection
+            "collection_deleted", default_value, collection, webhooks=webhooks
         )
 
     def collection_metadata_updated(self, collection: "Collection"):
@@ -625,18 +629,22 @@ def collection_metadata_updated(self, collection: "Collection"):
             "collection_metadata_updated", default_value, collection
         )
 
-    def product_created(self, product: "Product"):
+    def product_created(self, product: "Product", webhooks=None):
         default_value = None
-        return self.__run_method_on_plugins("product_created", default_value, product)
+        return self.__run_method_on_plugins(
+            "product_created", default_value, product, webhooks=webhooks
+        )
 
-    def product_updated(self, product: "Product"):
+    def product_updated(self, product: "Product", webhooks=None):
         default_value = None
-        return self.__run_method_on_plugins("product_updated", default_value, product)
+        return self.__run_method_on_plugins(
+            "product_updated", default_value, product, webhooks=webhooks
+        )
 
-    def product_deleted(self, product: "Product", variants: List[int]):
+    def product_deleted(self, product: "Product", variants: List[int], webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "product_deleted", default_value, product, variants
+            "product_deleted", default_value, product, variants, webhooks=webhooks
         )
 
     def product_media_created(self, media: "ProductMedia"):
@@ -663,42 +671,40 @@ def product_metadata_updated(self, product: "Product"):
             "product_metadata_updated", default_value, product
         )
 
-    def product_variant_created(self, product_variant: "ProductVariant"):
+    def product_variant_created(self, product_variant: "ProductVariant", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "product_variant_created", default_value, product_variant
+            "product_variant_created", default_value, product_variant, webhooks=webhooks
         )
 
-    def product_variant_updated(self, product_variant: "ProductVariant"):
+    def product_variant_updated(self, product_variant: "ProductVariant", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "product_variant_updated", default_value, product_variant
+            "product_variant_updated", default_value, product_variant, webhooks=webhooks
         )
 
-    def product_variant_deleted(self, product_variant: "ProductVariant"):
+    def product_variant_deleted(self, product_variant: "ProductVariant", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "product_variant_deleted",
-            default_value,
-            product_variant,
+            "product_variant_deleted", default_value, product_variant, webhooks=webhooks
         )
 
-    def product_variant_out_of_stock(self, stock: "Stock"):
+    def product_variant_out_of_stock(self, stock: "Stock", webhooks=None):
         default_value = None
         self.__run_method_on_plugins(
-            "product_variant_out_of_stock", default_value, stock
+            "product_variant_out_of_stock", default_value, stock, webhooks=webhooks
         )
 
-    def product_variant_back_in_stock(self, stock: "Stock"):
+    def product_variant_back_in_stock(self, stock: "Stock", webhooks=None):
         default_value = None
         self.__run_method_on_plugins(
-            "product_variant_back_in_stock", default_value, stock
+            "product_variant_back_in_stock", default_value, stock, webhooks=webhooks
         )
 
-    def product_variant_stock_updated(self, stock: "Stock"):
+    def product_variant_stock_updated(self, stock: "Stock", webhooks=None):
         default_value = None
         self.__run_method_on_plugins(
-            "product_variant_stock_updated", default_value, stock
+            "product_variant_stock_updated", default_value, stock, webhooks=webhooks
         )
 
     def product_variant_metadata_updated(self, product_variant: "ProductVariant"):
@@ -755,10 +761,10 @@ def sale_created(self, sale: "Promotion", current_catalogue):
             "sale_created", default_value, sale, current_catalogue
         )
 
-    def sale_deleted(self, sale: "Promotion", previous_catalogue):
+    def sale_deleted(self, sale: "Promotion", previous_catalogue, webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "sale_deleted", default_value, sale, previous_catalogue
+            "sale_deleted", default_value, sale, previous_catalogue, webhooks=webhooks
         )
 
     def sale_updated(self, sale: "Promotion", previous_catalogue, current_catalogue):
@@ -880,16 +886,24 @@ def order_refunded(self, order: "Order"):
             "order_refunded", default_value, order, channel_slug=order.channel.slug
         )
 
-    def order_updated(self, order: "Order"):
+    def order_updated(self, order: "Order", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "order_updated", default_value, order, channel_slug=order.channel.slug
+            "order_updated",
+            default_value,
+            order,
+            channel_slug=order.channel.slug,
+            webhooks=webhooks,
         )
 
-    def order_cancelled(self, order: "Order"):
+    def order_cancelled(self, order: "Order", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "order_cancelled", default_value, order, channel_slug=order.channel.slug
+            "order_cancelled",
+            default_value,
+            order,
+            channel_slug=order.channel.slug,
+            webhooks=webhooks,
         )
 
     def order_expired(self, order: "Order"):
@@ -1275,17 +1289,21 @@ def category_updated(self, category: "Category"):
         default_value = None
         return self.__run_method_on_plugins("category_updated", default_value, category)
 
-    def category_deleted(self, category: "Category"):
+    def category_deleted(self, category: "Category", webhooks=None):
         default_value = None
-        return self.__run_method_on_plugins("category_deleted", default_value, category)
+        return self.__run_method_on_plugins(
+            "category_deleted", default_value, category, webhooks=webhooks
+        )
 
     def channel_created(self, channel: "Channel"):
         default_value = None
         return self.__run_method_on_plugins("channel_created", default_value, channel)
 
-    def channel_updated(self, channel: "Channel"):
+    def channel_updated(self, channel: "Channel", webhooks=None):
         default_value = None
-        return self.__run_method_on_plugins("channel_updated", default_value, channel)
+        return self.__run_method_on_plugins(
+            "channel_updated", default_value, channel, webhooks=webhooks
+        )
 
     def channel_deleted(self, channel: "Channel"):
         default_value = None
@@ -1303,10 +1321,10 @@ def channel_metadata_updated(self, channel: "Channel"):
             "channel_metadata_updated", default_value, channel
         )
 
-    def gift_card_created(self, gift_card: "GiftCard"):
+    def gift_card_created(self, gift_card: "GiftCard", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "gift_card_created", default_value, gift_card
+            "gift_card_created", default_value, gift_card, webhooks=webhooks
         )
 
     def gift_card_updated(self, gift_card: "GiftCard"):
@@ -1315,10 +1333,10 @@ def gift_card_updated(self, gift_card: "GiftCard"):
             "gift_card_updated", default_value, gift_card
         )
 
-    def gift_card_deleted(self, gift_card: "GiftCard"):
+    def gift_card_deleted(self, gift_card: "GiftCard", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "gift_card_deleted", default_value, gift_card
+            "gift_card_deleted", default_value, gift_card, webhooks=webhooks
         )
 
     def gift_card_sent(self, gift_card: "GiftCard", channel_slug: str, email: str):
@@ -1331,10 +1349,10 @@ def gift_card_sent(self, gift_card: "GiftCard", channel_slug: str, email: str):
             email,
         )
 
-    def gift_card_status_changed(self, gift_card: "GiftCard"):
+    def gift_card_status_changed(self, gift_card: "GiftCard", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "gift_card_status_changed", default_value, gift_card
+            "gift_card_status_changed", default_value, gift_card, webhooks=webhooks
         )
 
     def gift_card_metadata_updated(self, gift_card: "GiftCard"):
@@ -1357,9 +1375,11 @@ def menu_updated(self, menu: "Menu"):
         default_value = None
         return self.__run_method_on_plugins("menu_updated", default_value, menu)
 
-    def menu_deleted(self, menu: "Menu"):
+    def menu_deleted(self, menu: "Menu", webhooks=None):
         default_value = None
-        return self.__run_method_on_plugins("menu_deleted", default_value, menu)
+        return self.__run_method_on_plugins(
+            "menu_deleted", default_value, menu, webhooks=webhooks
+        )
 
     def menu_item_created(self, menu_item: "MenuItem"):
         default_value = None
@@ -1373,10 +1393,10 @@ def menu_item_updated(self, menu_item: "MenuItem"):
             "menu_item_updated", default_value, menu_item
         )
 
-    def menu_item_deleted(self, menu_item: "MenuItem"):
+    def menu_item_deleted(self, menu_item: "MenuItem", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "menu_item_deleted", default_value, menu_item
+            "menu_item_deleted", default_value, menu_item, webhooks=webhooks
         )
 
     def shipping_price_created(self, shipping_method: "ShippingMethod"):
@@ -1391,10 +1411,10 @@ def shipping_price_updated(self, shipping_method: "ShippingMethod"):
             "shipping_price_updated", default_value, shipping_method
         )
 
-    def shipping_price_deleted(self, shipping_method: "ShippingMethod"):
+    def shipping_price_deleted(self, shipping_method: "ShippingMethod", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "shipping_price_deleted", default_value, shipping_method
+            "shipping_price_deleted", default_value, shipping_method, webhooks=webhooks
         )
 
     def shipping_zone_created(self, shipping_zone: "ShippingZone"):
@@ -1409,10 +1429,10 @@ def shipping_zone_updated(self, shipping_zone: "ShippingZone"):
             "shipping_zone_updated", default_value, shipping_zone
         )
 
-    def shipping_zone_deleted(self, shipping_zone: "ShippingZone"):
+    def shipping_zone_deleted(self, shipping_zone: "ShippingZone", webhooks=None):
         default_value = None
         return self.__run_method_on_plugins(
-            "shipping_zone_deleted", default_value, shipping_zone
+            "shipping_zone_deleted", default_value, shipping_zone, webhooks=webhooks
         )
 
     def shipping_zone_metadata_updated(self, shipping_zone: "ShippingZone"):
@@ -1487,9 +1507,11 @@ def voucher_updated(self, voucher: "Voucher"):
         default_value = None
         return self.__run_method_on_plugins("voucher_updated", default_value, voucher)
 
-    def voucher_deleted(self, voucher: "Voucher"):
+    def voucher_deleted(self, voucher: "Voucher", webhooks=None):
         default_value = None
-        return self.__run_method_on_plugins("voucher_deleted", default_value, voucher)
+        return self.__run_method_on_plugins(
+            "voucher_deleted", default_value, voucher, webhooks=webhooks
+        )
 
     def voucher_metadata_updated(self, voucher: "Voucher"):
         default_value = None
diff --git a/saleor/plugins/webhook/plugin.py b/saleor/plugins/webhook/plugin.py
index 3dc41895d36..59d0aaa82ac 100644
--- a/saleor/plugins/webhook/plugin.py
+++ b/saleor/plugins/webhook/plugin.py
@@ -180,8 +180,8 @@ def _serialize_payload(data):
     def _generate_meta(self):
         return generate_meta(requestor_data=generate_requestor(self.requestor))
 
-    def _trigger_metadata_updated_event(self, event_type, instance):
-        if webhooks := get_webhooks_for_event(event_type):
+    def _trigger_metadata_updated_event(self, event_type, instance, webhooks=None):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             metadata_payload_generator = partial(
                 generate_metadata_updated_payload, instance, self.requestor
             )
@@ -482,8 +482,8 @@ def attribute_value_deleted(
             WebhookEventAsyncType.ATTRIBUTE_VALUE_DELETED, attribute_value
         )
 
-    def __trigger_category_event(self, event_type, category):
-        if webhooks := get_webhooks_for_event(event_type):
+    def __trigger_category_event(self, event_type, category, webhooks=None):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             payload = self._serialize_payload(
                 {
                     "id": graphene.Node.to_global_id("Category", category.id),
@@ -504,13 +504,17 @@ def category_updated(self, category: "Category", previous_value: None) -> None:
             return previous_value
         self.__trigger_category_event(WebhookEventAsyncType.CATEGORY_UPDATED, category)
 
-    def category_deleted(self, category: "Category", previous_value: None) -> None:
+    def category_deleted(
+        self, category: "Category", previous_value: None, webhooks=None
+    ) -> None:
         if not self.active:
             return previous_value
-        self.__trigger_category_event(WebhookEventAsyncType.CATEGORY_DELETED, category)
+        self.__trigger_category_event(
+            WebhookEventAsyncType.CATEGORY_DELETED, category, webhooks=webhooks
+        )
 
-    def __trigger_channel_event(self, event_type, channel):
-        if webhooks := get_webhooks_for_event(event_type):
+    def __trigger_channel_event(self, event_type, channel, webhooks=None):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             payload = self._serialize_payload(
                 {
                     "id": graphene.Node.to_global_id("Channel", channel.id),
@@ -527,10 +531,14 @@ def channel_created(self, channel: "Channel", previous_value: None) -> None:
             return previous_value
         self.__trigger_channel_event(WebhookEventAsyncType.CHANNEL_CREATED, channel)
 
-    def channel_updated(self, channel: "Channel", previous_value: None) -> None:
+    def channel_updated(
+        self, channel: "Channel", previous_value: None, webhooks=None
+    ) -> None:
         if not self.active:
             return previous_value
-        self.__trigger_channel_event(WebhookEventAsyncType.CHANNEL_UPDATED, channel)
+        self.__trigger_channel_event(
+            WebhookEventAsyncType.CHANNEL_UPDATED, channel, webhooks=webhooks
+        )
 
     def channel_deleted(self, channel: "Channel", previous_value: None) -> None:
         if not self.active:
@@ -553,8 +561,10 @@ def channel_metadata_updated(
             WebhookEventAsyncType.CHANNEL_METADATA_UPDATED, channel
         )
 
-    def _trigger_gift_card_event(self, event_type, gift_card: "GiftCard"):
-        if webhooks := get_webhooks_for_event(event_type):
+    def _trigger_gift_card_event(
+        self, event_type, gift_card: "GiftCard", webhooks=None
+    ):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             payload = self._serialize_payload(
                 {
                     "id": graphene.Node.to_global_id("GiftCard", gift_card.id),
@@ -566,11 +576,13 @@ def _trigger_gift_card_event(self, event_type, gift_card: "GiftCard"):
                 payload, event_type, webhooks, gift_card, self.requestor
             )
 
-    def gift_card_created(self, gift_card: "GiftCard", previous_value: None) -> None:
+    def gift_card_created(
+        self, gift_card: "GiftCard", previous_value: None, webhooks=None
+    ) -> None:
         if not self.active:
             return previous_value
         self._trigger_gift_card_event(
-            WebhookEventAsyncType.GIFT_CARD_CREATED, gift_card
+            WebhookEventAsyncType.GIFT_CARD_CREATED, gift_card, webhooks=webhooks
         )
 
     def gift_card_updated(self, gift_card: "GiftCard", previous_value: None) -> None:
@@ -580,11 +592,13 @@ def gift_card_updated(self, gift_card: "GiftCard", previous_value: None) -> None
             WebhookEventAsyncType.GIFT_CARD_UPDATED, gift_card
         )
 
-    def gift_card_deleted(self, gift_card: "GiftCard", previous_value: None) -> None:
+    def gift_card_deleted(
+        self, gift_card: "GiftCard", previous_value: None, webhooks=None
+    ) -> None:
         if not self.active:
             return previous_value
         self._trigger_gift_card_event(
-            WebhookEventAsyncType.GIFT_CARD_DELETED, gift_card
+            WebhookEventAsyncType.GIFT_CARD_DELETED, gift_card, webhooks=webhooks
         )
 
     def gift_card_sent(
@@ -630,12 +644,12 @@ def gift_card_metadata_updated(
         )
 
     def gift_card_status_changed(
-        self, gift_card: "GiftCard", previous_value: None
+        self, gift_card: "GiftCard", previous_value: None, webhooks=None
     ) -> None:
         if not self.active:
             return previous_value
         self._trigger_gift_card_event(
-            WebhookEventAsyncType.GIFT_CARD_STATUS_CHANGED, gift_card
+            WebhookEventAsyncType.GIFT_CARD_STATUS_CHANGED, gift_card, webhooks=webhooks
         )
 
     def _trigger_export_event(self, event_type: str, export: "ExportFile"):
@@ -683,8 +697,8 @@ def order_created(self, order: "Order", previous_value: Any) -> Any:
                 legacy_data_generator=order_data_generator,
             )
 
-    def _trigger_menu_event(self, event_type, menu):
-        if webhooks := get_webhooks_for_event(event_type):
+    def _trigger_menu_event(self, event_type, menu, webhooks=None):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             payload = self._serialize_payload(
                 {
                     "id": graphene.Node.to_global_id("Menu", menu.id),
@@ -704,13 +718,15 @@ def menu_updated(self, menu: "Menu", previous_value: None) -> None:
             return previous_value
         self._trigger_menu_event(WebhookEventAsyncType.MENU_UPDATED, menu)
 
-    def menu_deleted(self, menu: "Menu", previous_value: None) -> None:
+    def menu_deleted(self, menu: "Menu", previous_value: None, webhooks=None) -> None:
         if not self.active:
             return previous_value
-        self._trigger_menu_event(WebhookEventAsyncType.MENU_DELETED, menu)
+        self._trigger_menu_event(
+            WebhookEventAsyncType.MENU_DELETED, menu, webhooks=webhooks
+        )
 
-    def __trigger_menu_item_event(self, event_type, menu_item):
-        if webhooks := get_webhooks_for_event(event_type):
+    def __trigger_menu_item_event(self, event_type, menu_item, webhooks=None):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             payload = self._serialize_payload(
                 {
                     "id": graphene.Node.to_global_id("MenuItem", menu_item.id),
@@ -739,11 +755,13 @@ def menu_item_updated(self, menu_item: "MenuItem", previous_value: None) -> None
             WebhookEventAsyncType.MENU_ITEM_UPDATED, menu_item
         )
 
-    def menu_item_deleted(self, menu_item: "MenuItem", previous_value: None) -> None:
+    def menu_item_deleted(
+        self, menu_item: "MenuItem", previous_value: None, webhooks=None
+    ) -> None:
         if not self.active:
             return previous_value
         self.__trigger_menu_item_event(
-            WebhookEventAsyncType.MENU_ITEM_DELETED, menu_item
+            WebhookEventAsyncType.MENU_ITEM_DELETED, menu_item, webhooks=webhooks
         )
 
     def order_confirmed(self, order: "Order", previous_value: Any) -> Any:
@@ -831,11 +849,11 @@ def order_fully_refunded(self, order: "Order", previous_value: Any) -> Any:
                 legacy_data_generator=order_data_generator,
             )
 
-    def order_updated(self, order: "Order", previous_value: Any) -> Any:
+    def order_updated(self, order: "Order", previous_value: Any, webhooks=None) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.ORDER_UPDATED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             order_data_generator = partial(
                 generate_order_payload, order, self.requestor
             )
@@ -923,11 +941,12 @@ def sale_deleted(
         sale: "Promotion",
         previous_catalogue: DefaultDict[str, Set[str]],
         previous_value: Any,
+        webhooks=None,
     ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.SALE_DELETED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             sale_data_generator = partial(
                 generate_sale_payload,
                 sale,
@@ -1137,11 +1156,13 @@ def invoice_sent(self, invoice: "Invoice", email: str, previous_value: Any) -> A
                 legacy_data_generator=invoice_data_generator,
             )
 
-    def order_cancelled(self, order: "Order", previous_value: Any) -> Any:
+    def order_cancelled(
+        self, order: "Order", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.ORDER_CANCELLED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             order_data_generator = partial(
                 generate_order_payload, order, self.requestor
             )
@@ -1350,11 +1371,13 @@ def customer_created(self, customer: "User", previous_value: Any) -> Any:
                 legacy_data_generator=customer_data_generator,
             )
 
-    def customer_updated(self, customer: "User", previous_value: Any) -> Any:
+    def customer_updated(
+        self, customer: "User", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.CUSTOMER_UPDATED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             customer_data_generator = partial(
                 generate_customer_payload, customer, self.requestor
             )
@@ -1367,11 +1390,13 @@ def customer_updated(self, customer: "User", previous_value: Any) -> Any:
                 legacy_data_generator=customer_data_generator,
             )
 
-    def customer_deleted(self, customer: "User", previous_value: Any) -> Any:
+    def customer_deleted(
+        self, customer: "User", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.CUSTOMER_DELETED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             customer_data_generator = partial(
                 generate_customer_payload, customer, self.requestor
             )
@@ -1384,11 +1409,13 @@ def customer_deleted(self, customer: "User", previous_value: Any) -> Any:
                 legacy_data_generator=customer_data_generator,
             )
 
-    def customer_metadata_updated(self, customer: "User", previous_value: Any) -> Any:
+    def customer_metadata_updated(
+        self, customer: "User", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         self._trigger_metadata_updated_event(
-            WebhookEventAsyncType.CUSTOMER_METADATA_UPDATED, customer
+            WebhookEventAsyncType.CUSTOMER_METADATA_UPDATED, customer, webhooks=webhooks
         )
 
     def collection_created(self, collection: "Collection", previous_value: Any) -> Any:
@@ -1425,11 +1452,13 @@ def collection_updated(self, collection: "Collection", previous_value: Any) -> A
                 legacy_data_generator=collection_data_generator,
             )
 
-    def collection_deleted(self, collection: "Collection", previous_value: Any) -> Any:
+    def collection_deleted(
+        self, collection: "Collection", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.COLLECTION_DELETED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             collection_data_generator = partial(
                 generate_collection_payload, collection, self.requestor
             )
@@ -1451,11 +1480,13 @@ def collection_metadata_updated(
             WebhookEventAsyncType.COLLECTION_METADATA_UPDATED, collection
         )
 
-    def product_created(self, product: "Product", previous_value: Any) -> Any:
+    def product_created(
+        self, product: "Product", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.PRODUCT_CREATED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             product_data_generator = partial(
                 generate_product_payload, product, self.requestor
             )
@@ -1468,11 +1499,13 @@ def product_created(self, product: "Product", previous_value: Any) -> Any:
                 legacy_data_generator=product_data_generator,
             )
 
-    def product_updated(self, product: "Product", previous_value: Any) -> Any:
+    def product_updated(
+        self, product: "Product", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.PRODUCT_UPDATED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             product_data_generator = partial(
                 generate_product_payload, product, self.requestor
             )
@@ -1504,12 +1537,16 @@ def product_export_completed(
         )
 
     def product_deleted(
-        self, product: "Product", variants: List[int], previous_value: Any
+        self,
+        product: "Product",
+        variants: List[int],
+        previous_value: Any,
+        webhooks=None,
     ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.PRODUCT_DELETED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             product_data_generator = partial(
                 generate_product_deleted_payload, product, variants, self.requestor
             )
@@ -1568,12 +1605,12 @@ def product_media_deleted(self, media: "ProductMedia", previous_value: Any) -> A
             )
 
     def product_variant_created(
-        self, product_variant: "ProductVariant", previous_value: Any
+        self, product_variant: "ProductVariant", previous_value: Any, webhooks=None
     ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.PRODUCT_VARIANT_CREATED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             product_variant_data_generator = partial(
                 generate_product_variant_payload, [product_variant], self.requestor
             )
@@ -1587,12 +1624,12 @@ def product_variant_created(
             )
 
     def product_variant_updated(
-        self, product_variant: "ProductVariant", previous_value: Any
+        self, product_variant: "ProductVariant", previous_value: Any, webhooks=None
     ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.PRODUCT_VARIANT_UPDATED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             product_variant_data_generator = partial(
                 generate_product_variant_payload, [product_variant], self.requestor
             )
@@ -1606,12 +1643,12 @@ def product_variant_updated(
             )
 
     def product_variant_deleted(
-        self, product_variant: "ProductVariant", previous_value: Any
+        self, product_variant: "ProductVariant", previous_value: Any, webhooks=None
     ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.PRODUCT_VARIANT_DELETED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             product_variant_data_generator = partial(
                 generate_product_variant_payload, [product_variant], self.requestor
             )
@@ -1633,11 +1670,13 @@ def product_variant_metadata_updated(
             WebhookEventAsyncType.PRODUCT_VARIANT_METADATA_UPDATED, product_variant
         )
 
-    def product_variant_out_of_stock(self, stock: "Stock", previous_value: Any) -> Any:
+    def product_variant_out_of_stock(
+        self, stock: "Stock", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.PRODUCT_VARIANT_OUT_OF_STOCK
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             product_variant_data_generator = partial(
                 generate_product_variant_with_stock_payload, [stock]
             )
@@ -1650,11 +1689,13 @@ def product_variant_out_of_stock(self, stock: "Stock", previous_value: Any) -> A
                 legacy_data_generator=product_variant_data_generator,
             )
 
-    def product_variant_back_in_stock(self, stock: "Stock", previous_value: Any) -> Any:
+    def product_variant_back_in_stock(
+        self, stock: "Stock", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.PRODUCT_VARIANT_BACK_IN_STOCK
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             product_variant_data_generator = partial(
                 generate_product_variant_with_stock_payload, [stock], self.requestor
             )
@@ -1667,11 +1708,13 @@ def product_variant_back_in_stock(self, stock: "Stock", previous_value: Any) ->
                 legacy_data_generator=product_variant_data_generator,
             )
 
-    def product_variant_stock_updated(self, stock: "Stock", previous_value: Any) -> Any:
+    def product_variant_stock_updated(
+        self, stock: "Stock", previous_value: Any, webhooks=None
+    ) -> Any:
         if not self.active:
             return previous_value
         event_type = WebhookEventAsyncType.PRODUCT_VARIANT_STOCK_UPDATED
-        if webhooks := get_webhooks_for_event(event_type):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             product_variant_data_generator = (
                 generate_product_variant_with_stock_payload([stock], self.requestor)
             )
@@ -1875,8 +1918,8 @@ def permission_group_deleted(self, group: "Group", previous_value: Any) -> Any:
             WebhookEventAsyncType.PERMISSION_GROUP_DELETED, group
         )
 
-    def _trigger_shipping_price_event(self, event_type, shipping_method):
-        if webhooks := get_webhooks_for_event(event_type):
+    def _trigger_shipping_price_event(self, event_type, shipping_method, webhooks=None):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             payload = self._serialize_payload(
                 {
                     "id": graphene.Node.to_global_id(
@@ -1908,17 +1951,19 @@ def shipping_price_updated(
         )
 
     def shipping_price_deleted(
-        self, shipping_method: "ShippingMethod", previous_value: None
+        self, shipping_method: "ShippingMethod", previous_value: None, webhooks=None
     ) -> None:
         if not self.active:
             return previous_value
 
         self._trigger_shipping_price_event(
-            WebhookEventAsyncType.SHIPPING_PRICE_DELETED, shipping_method
+            WebhookEventAsyncType.SHIPPING_PRICE_DELETED,
+            shipping_method,
+            webhooks=webhooks,
         )
 
-    def _trigger_shipping_zone_event(self, event_type, shipping_zone):
-        if webhooks := get_webhooks_for_event(event_type):
+    def _trigger_shipping_zone_event(self, event_type, shipping_zone, webhooks=None):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             payload = self._serialize_payload(
                 {
                     "id": graphene.Node.to_global_id("ShippingZone", shipping_zone.id),
@@ -1948,12 +1993,14 @@ def shipping_zone_updated(
         )
 
     def shipping_zone_deleted(
-        self, shipping_zone: "ShippingZone", previous_value: None
+        self, shipping_zone: "ShippingZone", previous_value: None, webhooks=None
     ) -> None:
         if not self.active:
             return previous_value
         self._trigger_shipping_zone_event(
-            WebhookEventAsyncType.SHIPPING_ZONE_DELETED, shipping_zone
+            WebhookEventAsyncType.SHIPPING_ZONE_DELETED,
+            shipping_zone,
+            webhooks=webhooks,
         )
 
     def shipping_zone_metadata_updated(
@@ -2105,8 +2152,8 @@ def warehouse_metadata_updated(
             WebhookEventAsyncType.WAREHOUSE_METADATA_UPDATED, warehouse
         )
 
-    def _trigger_voucher_event(self, event_type, voucher):
-        if webhooks := get_webhooks_for_event(event_type):
+    def _trigger_voucher_event(self, event_type, voucher, webhooks=None):
+        if webhooks := webhooks if webhooks else get_webhooks_for_event(event_type):
             payload = self._serialize_payload(
                 {
                     "id": graphene.Node.to_global_id("Voucher", voucher.id),
@@ -2129,10 +2176,14 @@ def voucher_updated(self, voucher: "Voucher", previous_value: None) -> None:
             return previous_value
         self._trigger_voucher_event(WebhookEventAsyncType.VOUCHER_UPDATED, voucher)
 
-    def voucher_deleted(self, voucher: "Voucher", previous_value: None) -> None:
+    def voucher_deleted(
+        self, voucher: "Voucher", previous_value: None, webhooks=None
+    ) -> None:
         if not self.active:
             return previous_value
-        self._trigger_voucher_event(WebhookEventAsyncType.VOUCHER_DELETED, voucher)
+        self._trigger_voucher_event(
+            WebhookEventAsyncType.VOUCHER_DELETED, voucher, webhooks=webhooks
+        )
 
     def voucher_metadata_updated(
         self, voucher: "Voucher", previous_value: None
diff --git a/saleor/product/tests/test_category.py b/saleor/product/tests/test_category.py
index e71ddf8d2c7..4f79ec293e2 100644
--- a/saleor/product/tests/test_category.py
+++ b/saleor/product/tests/test_category.py
@@ -46,11 +46,18 @@ def test_delete_categories(
             assert not product_channel_listing.published_at
 
 
+@patch("saleor.product.utils.get_webhooks_for_event")
 @patch("saleor.plugins.manager.PluginsManager.product_updated")
 def test_delete_categories_trigger_product_updated_webhook(
     product_updated_mock,
+    mocked_get_webhooks_for_event,
     categories_tree_with_published_products,
+    any_webhook,
+    settings,
 ):
+    # given
+    mocked_get_webhooks_for_event.return_value = [any_webhook]
+    settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
     parent = categories_tree_with_published_products
     child = parent.children.first()
     product_list = [child.products.first(), parent.products.first()]
diff --git a/saleor/product/utils/__init__.py b/saleor/product/utils/__init__.py
index fd9539047c5..31fb0ce202a 100644
--- a/saleor/product/utils/__init__.py
+++ b/saleor/product/utils/__init__.py
@@ -2,6 +2,8 @@
 
 from ...core.taxes import TaxedMoney, zero_taxed_money
 from ...core.tracing import traced_atomic_transaction
+from ...webhook.event_types import WebhookEventAsyncType
+from ...webhook.utils import get_webhooks_for_event
 from ..models import Product, ProductChannelListing
 from ..tasks import update_products_discounted_prices_for_promotion_task
 
@@ -54,12 +56,12 @@ def delete_categories(categories_ids: List[Union[str, int]], manager):
 
     category_instances = [cat for cat in categories]
     categories.delete()
-
-    for category in category_instances:
-        manager.category_deleted(category)
-
-    for product in products:
-        manager.product_updated(product)
+    if webhooks := get_webhooks_for_event(WebhookEventAsyncType.CATEGORY_DELETED):
+        for category in category_instances:
+            manager.category_deleted(category, webhooks=webhooks)
+    if webhooks := get_webhooks_for_event(WebhookEventAsyncType.PRODUCT_UPDATED):
+        for product in products:
+            manager.product_updated(product, webhooks=webhooks)
 
     update_products_discounted_prices_for_promotion_task.delay(
         product_ids=[product.id for product in products]


Code review comments:
diff_hunk: @@ -90,6 +90,33 @@ All notable, unreleased changes to this project will be documented in this file.
 
 ### Breaking changes
 - Deprecate `external_url` on `Invoice` GraphQL type in favour of `url`. No matter if the invoice is stored on Saleor or is a link to an external invoice it will get returned in the `url` field.
+- Extend methods of `BasePlugin` to accept optional `webhooks` parameter to reduce number of queries executed in bulk mutations.
path: CHANGELOG.md
text: @user1:
This should be moved to the 3.18 section.