Taxes
Saleor offers two ways of calculating taxes: flat rates and dynamic taxes.
Flat rates allow you to configure static tax rates per country and channel, and assign them to products via tax classes. Dynamic taxes, on the other hand, enable you to delegate tax calculations to 3rd-party tax providers like Avalara, TaxJar, or entirely custom tax apps.
You can configure the tax calculation method per channel and override the configuration for different countries.
Tax Configuration
The tax configuration object defines various configuration options for a channel. This includes the tax calculation method, whether to charge taxes in the channel, or whether the entered prices include the tax. The configuration object is created automatically for each sales channel. In the API, it is represented by the TaxConfiguration
object.
The tax configuration object has several properties, including:
-
chargeTaxes
- Determines whether taxes are charged in the channel. If enabled, taxes are calculated and customers pay the gross prices. If disabled, taxes are still calculated and available for display, but customers pay the net prices. -
taxCalculationStrategy
- The strategy for tax calculation in the channel. There are two strategies available:- flat rates - the default strategy
- dynamic taxes (tax apps)
-
displayGrossPrices
- Determines whether prices displayed in a storefront should include taxes. Note that this setting doesn’t affect internal calculations in Saleor, and it’s only meant for the storefront to decide if it should displayTaxedMoney.net
orTaxedMoney.gross
price (see TaxedMoney API reference). For storefront queries, this property can be fetched at two levels:- product level in the ProductPricingInfo.displayGrossPrices field - use it when rendering product listings or product details pages
- checkout level in the Checkout.displayGrossPrices field - use it when rendering the cart or checkout pages
-
pricesEnteredWithTax
- Determine whether product prices are entered with tax included (typical for B2C in EU) or not (typical for US and B2B). -
taxAppId
- Specify which Tax App should be responsible for tax calculation when tax apps are used as a calculation strategy.You can query all the available tax apps with:
query GetShop {
shop {
availableTaxApps {
id
name
identifier
}
}
}where
identifier
is the value you should use to set thetaxAppId
in thetaxConfigurationUpdate
mutation.Besides tax apps, you can assign plugins to
taxAppId
. To assign a plugin, you need to provide the value in the following format:plugin:PLUGIN_ID
. Please note that plugins will be deprecated.
Additionally, the following properties can be overridden for specific countries within the channel: chargeTaxes
, taxCalculationStrategy
, displayGrossPrices
and taxAppId
. This allows for handling various situations where a store operates in different countries with different tax rules.
Fetching tax configurations
Below are some common API examples of retrieving and managing the tax configuration. To fetch the list of tax configurations with their properties, use the following query:
query TaxConfigurations {
taxConfigurations(first: 10) {
edges {
node {
id
channel {
slug
}
chargeTaxes
displayGrossPrices
pricesEnteredWithTax
taxCalculationStrategy
taxAppId
}
}
}
}
To query a specific tax configuration by ID, use the query below (in this case, we’re only asking for the taxCalculationStrategy
field):
query TaxConfiguration {
taxConfiguration(id: "VGF4Q29uZmlndXJhdGlvbjox") {
taxCalculationStrategy
}
}
Updating the tax configuration
The taxConfigurationUpdate
mutation allows you to update the specific tax configuration. The example below shows how to set the tax calculation strategy to dynamic taxes (in API represented as the TAX_APP
option):
mutation {
taxConfigurationUpdate(
id: "VGF4Q29uZmlndXJhdGlvbjox"
input: { taxCalculationStrategy: TAX_APP }
) {
errors {
field
message
}
taxConfiguration {
taxCalculationStrategy
}
}
}
Overriding tax configuration per country
Tax configurations specific to a channel can be modified for different countries. For example, a single sales channel may use flat rates for EU countries but use the tax app for the US. To represent this scenario in Saleor, you would set flat rates as the channel's default tax calculation strategy and override the configuration for the US to use dynamic taxes.
mutation {
taxConfigurationUpdate(
id: "VGF4Q29uZmlndXJhdGlvbjox"
input: {
taxCalculationStrategy: FLAT_RATES
updateCountriesConfiguration: [
{
countryCode: US
taxCalculationStrategy: TAX_APP
displayGrossPrices: false
chargeTaxes: true
}
]
}
) {
errors {
field
message
}
taxConfiguration {
taxCalculationStrategy
countries {
country {
code
}
chargeTaxes
taxCalculationStrategy
displayGrossPrices
}
}
}
}
To remove the country-specific configuration, use the removeCountriesConfiguration
field and provide the country code for which to remove the configuration:
mutation {
taxConfigurationUpdate(
id: "VGF4Q29uZmlndXJhdGlvbjox"
input: { removeCountriesConfiguration: [US] }
) {
errors {
field
message
}
taxConfiguration {
countries {
country {
code
}
taxCalculationStrategy
}
}
}
}
Flat rates
Flat rates are the default tax calculation strategy that is enabled for any new sales channel. With this method, you can configure static tax rates and associate them with products, product types, or shipping methods. Tax rates can be defined as default country rates or overridden for specific products with tax classes.
Enabling flat rates for a channel
To enable flat rates, use the following mutation:
mutation {
taxConfigurationUpdate(
id: "VGF4Q29uZmlndXJhdGlvbjox"
input: { taxCalculationStrategy: FLAT_RATES }
) {
errors {
field
message
}
taxConfiguration {
taxCalculationStrategy
}
}
}
VGF4Q29uZmlndXJhdGlvbjox
is the ID of the tax configuration object related to the channel. See Fetching tax configurations to learn how to get the ID.
Creating a default country rate
The default country rate is used for products and shipping methods when there is no other tax class assigned to them. To provide a default tax rate for a country, use the following mutation:
mutation {
taxCountryConfigurationUpdate(
countryCode: PL
updateTaxClassRates: [{ rate: 23 }]
) {
taxCountryConfiguration {
country {
code
}
taxClassCountryRates {
rate
}
}
errors {
field
message
}
}
}
In this example, we’re setting the default tax rate for Poland to 23%.
Assigning a specific tax rate to a product
To create a tax rate that would be used only for specific products, you must create a tax class and assign it to the product. In the example below, we’re creating a tax class “Healthcare products” with an 8% tax rate for Poland:
mutation {
taxClassCreate(
input: {
name: "Healthcare products"
createCountryRates: [{ countryCode: PL, rate: 8 }]
}
) {
errors {
field
message
}
taxClass {
id
countries {
rate
}
}
}
}
To assign the tax class to a product, use the following mutation:
mutation {
productUpdate(id: "UHJvZHVjdDox", input: { taxClass: "VGF4Q2xhc3M6Mg==" }) {
errors {
field
message
}
product {
taxClass {
name
}
}
}
}
When the product is added to checkout, Saleor will use the 8% tax rate.
You can assign tax classes to products directly, or you can assign them to product types using the productTypeUpdate
mutation. When a product type has a tax class assigned, all products of this type will use it, as long as they don’t have their own tax rates assigned.
Assigning tax rate to a shipping method
To calculate taxes on shipping, you can either configure [the default tax rate for a country(developer/taxes.mdx#creating-a-default-country-rate) or create a custom tax class and assign it to the shipping method. Here is an example:
mutation {
shippingPriceUpdate(
id: "U2hpcHBpbmdNZXRob2RUeXBlOjE="
input: { taxClass: "VGF4Q2xhc3M6Mg==" }
) {
errors {
field
message
}
shippingMethod {
taxClass {
name
}
}
}
}
Querying products with taxed prices for a specific country
When using flat rates, the API can list products with prices that include tax rates for different countries. To do this, pass the country code for which you have flat rate configurations available. If the country
argument is not specified, the default country for the channel is assumed. (See Channel.defaultCountry.)
In this example, we are fetching a product list with prices for Poland.
{
products(channel: "default-channel", first: 20) {
edges {
node {
id
name
pricing(address: { country: PL }) {
priceRange {
start {
currency
gross {
amount
}
net {
amount
}
}
}
}
}
}
}
}
Rounding
The following rounding rule is applied to the tax calculation:
If the discarded fraction is >= 0.5, round up; if the discarded fraction is < 0.5, then round down
Examples:
$9.945 => $9.95
$9.94499 => $9.94
Precision for most currencies is calculated up to 2 decimal places.
Dynamic taxes
Dynamic taxes let you delegate the tax calculation to external apps, including 3rd-party tax providers like Avalara, TaxJar, or custom tax apps. This means that your store can use real-time tax rates from external sources instead of setting up static tax rates. This method is useful when your store operates in multiple countries or regions with complex tax rules or when you want to automate the tax calculation process.
To learn more about configuring dynamic taxes, navigate to the Tax Webhooks page.
To enable dynamic taxes in a channel use:
mutation {
taxConfigurationUpdate(
id: "VGF4Q29uZmlndXJhdGlvbjox"
input: { taxCalculationStrategy: TAX_APP, taxAppId: "saleor.tax" }
) {
errors {
field
message
}
taxConfiguration {
taxCalculationStrategy
}
}
}
If you decide to set taxAppId
checkoutComplete and draftOrderComplete mutations will fail if your Tax App doesn't respond or provide invalid data.
From Saleor 4.0 forward taxAppId
will become mandatory when TAX_APP
is set as taxCalculationStrategy
.
If you use the TAX_APP
calculation strategy and decide not to set taxAppId
then Saleor will try to iterate through all installed Apps and return the first result.
In case when any app doesn't respond, checkoutComplete and draftOrderComplete mutations will proceed without errors.
Tax exemption
The Tax Exemption API allows you to exempt checkouts or orders from taxes, regardless of any channel or country-specific configuration. Note that this API is restricted to users with the MANAGE_TAXES
permission.
To exempt a checkout from taxes, use the following mutation:
mutation {
taxExemptionManage(
id: "Q2hlY2tvdXQ6ZTZiMzAzYmMtMzc2Zi00YThkLTkxYzAtNmU3MjYyNDU1OWU3"
taxExemption: true
) {
errors {
field
message
}
taxableObject {
... on Checkout {
taxExemption
}
}
}
}
For orders, pass the order ID as an argument and adjust the query to fetch the order data in the taxableObject
field. To re-enable tax charging, pass taxExemption: false
.
Address for tax calculation
The process of determining the country code follows this sequence:
- Shipping address
- Billing address
- The channel's default country
The table provides examples to help understand how tax calculation is determined based on the address.
Store Address | Shipping Address | Billing Address | Warehouse Address | |
---|---|---|---|---|
Country | US | US | US | US |
CountryArea | LA | VA | TX | VT |
Example for shipable products
- Create a checkout with channel and lines. Since the shipping and billing addresses are empty, taxes are calculated based on the store address (Country US, state LA)
- Update the billing address. The shipping address is still empty, but since the billing address is now available, taxes are calculated based on it (Country US, state TX)
- Update the shipping address. Taxes are now calculated based on the shipping address (Country US, state VA).
Example for digital products
- Create a checkout with channel and non-shippable lines. Since the shipping and billing addresses are empty, taxes are calculated based on the store address (Country US, state LA)
- Update the billing address. Taxes are calculated based on it (Country US, state TX)
It's possible to set shipping address in the checkoutCreate mutation. In this case, the shipping address will be used for tax calculation since we always prioritize shipping address over billing address.
Example for click and collect shipping
- Create a checkout with channel and lines. Since the shipping address is empty, taxes are calculated based on the store address (Country US, state LA)
- Update the billing address. Taxes are calculated based on the billing address (Country US, state TX)
- Update the shipping address. Taxes are calculated based on the shipping address (Country US, state VA).
- Update the delivery method with the ID of availableCollectionPoint. The shipping address has been changed to the warehouse address. Taxes are now calculated based on the warehouse address. (Country US, state VT)