Developing a Custom Saleor Plugin
Saleor is built on Django and provides an explicit extension point through the plugin system — BasePlugin. Each plugin is registered in Django's PLUGINS settings and intercepts events through hooks. These are not WordPress-style plugins: there is no magic, just Python classes with a predictable lifecycle.
Plugin Architecture
A Saleor plugin is a class that inherits from BasePlugin from saleor.plugins.base_plugin. Available hooks cover the entire order lifecycle, payment pipeline, product display, and webhook events.
from saleor.plugins.base_plugin import BasePlugin, ConfigurationTypeField
class TaxProviderPlugin(BasePlugin):
PLUGIN_ID = "custom.tax_provider"
PLUGIN_NAME = "Custom Tax Provider"
DEFAULT_ACTIVE = False
CONFIG_STRUCTURE = {
"api_key": {
"type": ConfigurationTypeField.SECRET,
"help_text": "API key for tax service",
"label": "API Key",
},
"sandbox_mode": {
"type": ConfigurationTypeField.BOOLEAN,
"help_text": "Use sandbox endpoint",
"label": "Sandbox",
},
}
def calculate_checkout_line_tax(
self, checkout_line_info, checkout_info, address, discounts, previous_value
):
config = self._get_config()
api_key = next(
(c["value"] for c in config if c["name"] == "api_key"), None
)
# calculate tax via external API
return TaxedMoney(
net=checkout_line_info.line.unit_price_net,
gross=self._fetch_tax(checkout_line_info, api_key),
)
The _get_config() method returns the configuration saved through the Dashboard. Values of SECRET type are stored encrypted.
Payment Pipeline Hooks
The most requested hooks are payment-related. Saleor separates processing into authorize, capture, refund, void:
def authorize_payment(
self, payment_information: "PaymentData", previous_value
) -> "GatewayResponse":
token = payment_information.token
amount = payment_information.amount
currency = payment_information.currency
response = self._call_payment_gateway(
action="authorize",
token=token,
amount=amount,
currency=currency,
)
return GatewayResponse(
is_success=response.get("status") == "authorized",
action_required=False,
kind=TransactionKind.AUTH,
amount=amount,
currency=currency,
transaction_id=response.get("transaction_id"),
error=response.get("error_message"),
)
Webhook Events
Since version 3.x, Saleor supports async webhooks. A plugin can declare subscriptions through GraphQL subscriptions instead of polling:
WEBHOOK_EVENTS_SUBSCRIPTIONS = """
subscription {
event {
... on OrderCreated {
order {
id
number
total { gross { amount currency } }
user { email }
}
}
}
}
"""
Saleor will send a POST with the payload to the specified endpoint for each ORDER_CREATED event. The subscription body determines which fields end up in the payload — it's a GraphQL fragment, not just a config.
Registration and Configuration
# settings.py
PLUGINS = [
"myapp.plugins.tax_provider.TaxProviderPlugin",
"myapp.plugins.custom_payment.CustomPaymentPlugin",
]
After restarting the server, the plugin will appear in the Plugins section of the Dashboard. Configuration fields from CONFIG_STRUCTURE are rendered automatically.
Testing
Saleor provides PluginsManager — test plugins through it without spinning up the full Django environment:
from unittest.mock import patch, MagicMock
from saleor.plugins.manager import PluginsManager
def test_tax_calculation():
plugin = TaxProviderPlugin(
configuration=[{"name": "api_key", "value": "test-key"}],
active=True,
)
with patch.object(plugin, "_fetch_tax", return_value=Decimal("12.50")):
result = plugin.calculate_checkout_line_tax(
checkout_line_info=mock_line,
checkout_info=mock_checkout,
address=mock_address,
discounts=[],
previous_value=TaxedMoney(net=Decimal("100"), gross=Decimal("100")),
)
assert result.gross.amount == Decimal("12.50")
Typical Tasks and Timeframes
| Task | Complexity | Timeframe |
|---|---|---|
| Tax plugin with external API | Medium | 3–5 days |
| Payment gateway (authorize + capture + refund) | High | 5–8 days |
| Webhook integration with CRM/ERP | Medium | 2–4 days |
| Custom discount logic | Medium | 3–4 days |
| Notification plugin (email/SMS) | Low | 1–2 days |
What to Provide Before Starting
- Saleor version (3.x changes hook signatures relative to 2.x)
- Description of business logic: which events to intercept, which external API to call
- Test environment credentials
- Dashboard configuration requirements (need secret fields?)
Development is conducted in a separate Python package, connected via pip install -e. This allows versioning the plugin independently from the Saleor core and updating it without forking.







