@spree/docs 0.1.182 → 0.1.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/developer/core-concepts/addresses.md +106 -198
  2. package/dist/developer/core-concepts/architecture.md +97 -126
  3. package/dist/developer/core-concepts/calculators.md +75 -252
  4. package/dist/developer/core-concepts/carts.md +1 -1
  5. package/dist/developer/core-concepts/channels.md +0 -4
  6. package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
  7. package/dist/developer/core-concepts/customers.md +0 -3
  8. package/dist/developer/core-concepts/discounts.md +133 -0
  9. package/dist/developer/core-concepts/events.md +83 -576
  10. package/dist/developer/core-concepts/fees.md +144 -0
  11. package/dist/developer/core-concepts/imports-exports.md +105 -679
  12. package/dist/developer/core-concepts/inventory.md +114 -248
  13. package/dist/developer/core-concepts/markets.md +9 -12
  14. package/dist/developer/core-concepts/media.md +9 -11
  15. package/dist/developer/core-concepts/metafields.md +123 -200
  16. package/dist/developer/core-concepts/order-totals.md +110 -0
  17. package/dist/developer/core-concepts/orders.md +1 -1
  18. package/dist/developer/core-concepts/payments.md +11 -14
  19. package/dist/developer/core-concepts/pricing.md +11 -13
  20. package/dist/developer/core-concepts/products.md +35 -19
  21. package/dist/developer/core-concepts/promotions.md +12 -11
  22. package/dist/developer/core-concepts/search-filtering.md +2 -4
  23. package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
  24. package/dist/developer/core-concepts/taxes.md +125 -113
  25. package/dist/developer/core-concepts/translations.md +61 -68
  26. package/dist/developer/core-concepts/webhooks.md +25 -59
  27. package/dist/developer/how-to/custom-promotion.md +3 -3
  28. package/package.json +1 -1
  29. package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
@@ -3,8 +3,6 @@ title: Pricing
3
3
  description: Prices, Price Lists, Price Rules, and the Pricing Context — Spree's flexible pricing engine for regional, wholesale, volume, and market-based pricing.
4
4
  ---
5
5
 
6
- import { Since } from '/snippets/since.mdx';
7
-
8
6
  ## Overview
9
7
 
10
8
  Spree's pricing system supports both simple single-currency pricing and advanced multi-currency, rule-based pricing through Price Lists. Every [Variant](products.md#variants) can have multiple prices — a base price per currency, plus additional prices from Price Lists that apply conditionally based on rules like geography, customer segment, or quantity.
@@ -94,7 +92,7 @@ spree api post /prices/bulk_upsert -d '{
94
92
  ```
95
93
 
96
94
 
97
- ## Price Lists
95
+ ## Price Lists
98
96
 
99
97
  Price Lists allow you to create different pricing strategies based on various conditions. This enables advanced pricing scenarios like:
100
98
 
@@ -165,27 +163,27 @@ spree api patch /price_lists/pl_xxx/activate
165
163
  ```
166
164
 
167
165
 
168
- ## Price Rules
166
+ ## Price Rules
169
167
 
170
168
  Price Rules define conditions that must be met for a Price List to apply. Spree includes five built-in rule types:
171
169
 
172
170
  | Rule | Description | Use Case |
173
171
  |------|-------------|----------|
174
172
  | **Market Rule** | Matches based on the current [Market](markets.md) | Regional pricing across markets |
175
- | **Zone Rule** | Matches based on the customer's geographic zone | Country or state-level pricing |
173
+ | **Channel Rule** | Matches based on the [Channel](channels.md) the customer is buying through | App-only or in-store pricing |
176
174
  | **User Rule** | Matches specific customer accounts | VIP customers, wholesale accounts |
177
175
  | **Customer Group Rule** | Matches members of customer groups | Loyalty tiers, membership pricing |
178
176
  | **Volume Rule** | Matches based on quantity purchased | Bulk discounts, tiered pricing |
179
177
 
180
- ### Market Rule
178
+ ### Market Rule
181
179
 
182
- The recommended approach for regional pricing when using Markets. Applies the Price List when the customer is in one of the specified markets.
180
+ Regional pricing. Applies the Price List when the customer is in one of the specified markets.
183
181
 
184
182
  **Example:** Price a product at $29.99 in North America and €24.99 in Europe, rather than relying on exchange rate conversion.
185
183
 
186
- ### Zone Rule
184
+ ### Channel Rule
187
185
 
188
- Applies based on the customer's geographic zone. Useful for regional or country-specific pricing when not using Markets.
186
+ Applies when the customer is buying through a particular sales channel — a discount that exists only in the mobile app, or a price that only applies at a retail till.
189
187
 
190
188
  ### User Rule
191
189
 
@@ -207,7 +205,7 @@ Applies based on quantity purchased. Supports `min_quantity` and `max_quantity`
207
205
 
208
206
  > **INFO:** Custom Price Rules can be created for specialized pricing logic. See the [Customization Quickstart](../customization/quickstart.md) for details.
209
207
 
210
- ## Pricing Context
208
+ ## Pricing Context
211
209
 
212
210
  When resolving prices, Spree considers the full context of the request:
213
211
 
@@ -215,8 +213,8 @@ When resolving prices, Spree considers the full context of the request:
215
213
  |---------|--------|-------------|
216
214
  | Currency | Market or request header | The currency to price in |
217
215
  | Market | Customer's country | The [Market](markets.md) for market-based rules |
218
- | Zone | Customer's address | The geographic zone for zone-based rules |
219
- | Customer | JWT authentication | The logged-in customer for user-based rules |
216
+ | Channel | The API credential in use | The [Channel](channels.md) for channel-based rules |
217
+ | Customer | Authentication | The signed-in customer for user-based rules |
220
218
  | Quantity | Cart line item | The quantity for volume-based rules |
221
219
  | Date | Current time | For time-based Price List scheduling |
222
220
 
@@ -236,7 +234,7 @@ Price Lists are managed in the Admin Panel under **Products → Price Lists**, o
236
234
 
237
235
  Each Price List contains prices for specific variants and currencies. Products can be added to a Price List, and individual variant prices set within it.
238
236
 
239
- ## Price History (EU Omnibus Directive)
237
+ ## Price History (EU Omnibus Directive)
240
238
 
241
239
  Spree automatically records price changes for EU Omnibus Directive compliance. When a product goes on sale, EU regulations require displaying the lowest price in the preceding 30 days alongside the discounted price.
242
240
 
@@ -3,8 +3,6 @@ title: Products
3
3
  description: How Spree models products, variants, option types, images, prices, and categories — the building blocks of every catalog and storefront.
4
4
  ---
5
5
 
6
- import { Since } from '/snippets/since.mdx';
7
-
8
6
  ## Overview
9
7
 
10
8
  A product represents something you sell. Each product has one or more **variants** — the actual purchasable items with their own SKU, price, and inventory. For example, a "T-Shirt" product might have variants for each size and color combination.
@@ -16,15 +14,16 @@ Products are organized into **categories** — a flexible hierarchy for grouping
16
14
  ```mermaid
17
15
  erDiagram
18
16
  Product ||--o{ Variant : "has many"
19
- Product }o--o{ OptionType : "has many"
20
- Product ||--o{ Classification : "has many"
21
- Variant ||--o{ Price : "has many"
22
- Variant ||--o{ StockItem : "has many"
23
- Variant ||--o{ Image : "has many"
24
- Variant }o--o{ OptionValue : "has many"
17
+ Product }o--o{ OptionType : "varies by"
18
+ Product }o--o{ Category : "filed under"
19
+ Product }o--o{ Collection : "grouped into"
20
+ Product ||--o{ Media : "images and video"
21
+ Product }o--|| DeliveryProfile : "ships by"
22
+ Variant ||--o{ Price : "one per currency"
23
+ Variant ||--o{ StockLevel : "stocked per location"
24
+ Variant }o--o{ OptionValue : "identified by"
25
25
  OptionType ||--o{ OptionValue : "has many"
26
- Taxon ||--o{ Classification : "has many"
27
- Taxonomy ||--o{ Taxon : "has many"
26
+ Category ||--o{ Category : "nests under"
28
27
 
29
28
  Product {
30
29
  string name
@@ -36,7 +35,7 @@ erDiagram
36
35
 
37
36
  Variant {
38
37
  string sku
39
- boolean is_master
38
+ string barcode
40
39
  decimal weight
41
40
  }
42
41
 
@@ -221,7 +220,7 @@ spree api delete /products/prod_xxx
221
220
 
222
221
  > **TIP:** Operating on many products at once? The Admin API has bulk actions — `bulkStatusUpdate`, `bulkAddToCategories`, `bulkAddTags`, `bulkDestroy`, and more. See the [Admin API endpoint index](../../api-reference/admin-api/endpoints.md).
223
222
 
224
- ## Seller submissions
223
+ ## Seller submissions
225
224
 
226
225
  On a marketplace, a seller lists a product but does not publish one. They submit it, and the marketplace decides. This adds two statuses to the three above — both hidden from the storefront, since only `active` is visible.
227
226
 
@@ -606,14 +605,19 @@ See the [Pricing](pricing.md) guide for details on Price Lists, Price Rules, and
606
605
 
607
606
  ## Categories
608
607
 
609
- Categories provide a flexible way to organize products into hierarchical trees. Internally, Spree uses Taxonomies (category trees) and Taxons (nodes within those trees), but the Store API exposes them simply as **Categories**.
608
+ There are two ways to group products, and they answer different questions.
609
+
610
+ **Categories** are a hierarchy — the navigation tree a shopper browses. Clothing contains T-Shirts, which contains Long Sleeve. A product can sit in several categories, and each one has a permalink built from its path.
611
+
612
+ **Collections** are flat groupings — "Summer 2025", "Best Sellers", "Under $50". A collection can be curated by hand, or defined by rules so products join and leave it on their own as their price, tags or stock change.
610
613
 
611
- For example:
612
- - **Categories** → Clothing → T-Shirts, Dresses
613
- - **Brands** Nike, Adidas, Puma
614
- - **Collections** Summer 2025, Best Sellers
614
+ | | Categories | Collections |
615
+ |---|---|---|
616
+ | Shape | Nested tree | Flat list |
617
+ | Membership | You assign it | Assigned, or matched by rules |
618
+ | Typical use | Site navigation | Merchandising and campaigns |
615
619
 
616
- Products can belong to multiple categories.
620
+ A brand is usually best modelled as one or the other rather than as a separate concept — a category if you want it in the navigation tree, a collection if it's a landing page.
617
621
 
618
622
 
619
623
  ```typescript Store SDK
@@ -650,7 +654,19 @@ curl 'https://api.mystore.com/api/v3/store/categories/clothing/shirts/products?l
650
654
 
651
655
  > **INFO:** Category `name` and `description` fields are translatable.
652
656
 
653
- ## Publications and Sales Channels
657
+ Collections work the same way from a storefront's point of view:
658
+
659
+ ```typescript Store SDK
660
+ const { data: collections } = await client.collections.list()
661
+
662
+ const { data: products } = await client.collections.products.list('summer-2025', {
663
+ limit: 12,
664
+ })
665
+ ```
666
+
667
+ A rule-based collection is defined once and maintains itself — set it to match everything tagged `sale` and under $50, and products appear and disappear as those facts change. Ordering can be manual or by a rule such as newest first.
668
+
669
+ ## Publications and Sales Channels
654
670
 
655
671
  A product is visible on a [Channel](channels.md) only when a `ProductPublication` record joins the two. Publications carry an optional time window so a product can be scheduled to go live and come down without code or manual toggles.
656
672
 
@@ -5,7 +5,9 @@ description: Build percentage and fixed-amount discounts, free shipping, BOGO of
5
5
 
6
6
  ## Overview
7
7
 
8
- Promotions give money off an order — a percentage off, free shipping, or free items added to the cart. A promotion is a campaign definition: **rules** decide when it applies, **actions** decide what it does. When a promotion applies, its actions write [Discount](taxes-discounts-fees.md#discounts) rows on the order.
8
+ A promotion is a campaign: a percentage off, free shipping, or free items in the cart. **Rules** decide when it applies, **actions** decide what it does.
9
+
10
+ A promotion is a *rule that exists on its own*, set up in advance and lasting across many orders. What lands on any one order is a [Discount](discounts.md) — the record of money actually coming off. The promotion is the campaign; the discount is what a particular customer got.
9
11
 
10
12
  ```mermaid
11
13
  erDiagram
@@ -52,14 +54,13 @@ Promotions can be activated in two ways:
52
54
  - **Automatic promotions** — applied on their own when the rules are met (e.g. free shipping on orders over $50)
53
55
  - **Coupon code promotions** — applied when a customer enters a valid code during checkout
54
56
 
55
- ### How discounts are recorded
57
+ ### When promotions compete
56
58
 
57
- Each applied promotion becomes one or more Discount rows on the order — on a specific line item or on a fulfillment, never a loose adjustment. Each row keeps a copy of the coupon code and the configured value ("10% off"), so the order still tells the whole story even if the promotion is deleted later.
59
+ Several promotions can qualify for the same item at once. Spree applies **only the one that saves the customer the most**, and records just that one.
58
60
 
59
- Two things follow from this design:
61
+ Losing candidates aren't stored or tracked — they're simply reconsidered on every cart change. So a promotion that loses today can win tomorrow, when the basket changes, without any bookkeeping to keep straight.
60
62
 
61
- - **Only winners are stored.** When several promotions compete for the same item, Spree picks the one that saves the customer the most and writes only that one. Losing candidates are recomputed on every cart change, so a promotion that loses today can win tomorrow without any bookkeeping.
62
- - **A discount on the whole order is shared out across the items.** If a customer later returns one item, Spree knows exactly how much of the discount belonged to it.
63
+ What an applied promotion leaves behind on the order is a [Discount](discounts.md) a permanent record that survives the promotion being edited or deleted.
63
64
 
64
65
  ## Promotion Attributes
65
66
 
@@ -112,7 +113,7 @@ You can also [build your own rules](../how-to/custom-promotion.md) for business-
112
113
 
113
114
  Actions define what happens when a promotion applies.
114
115
 
115
- ### CreateAdjustment
116
+ ### Order discount (`CreateAdjustment`)
116
117
 
117
118
  A discount on the whole order. The amount is distributed proportionally across the line items — there is no single order-level row, so per-item reporting and partial returns always know their share.
118
119
 
@@ -120,7 +121,7 @@ A discount on the whole order. The amount is distributed proportionally across t
120
121
 
121
122
  **Use case:** "10% off your order", "$20 off orders over $100".
122
123
 
123
- ### CreateItemAdjustments
124
+ ### Item discount (`CreateItemAdjustments`)
124
125
 
125
126
  A discount on individual line items. Only items that match the promotion's rules receive it — if a Category rule says "Electronics", only electronics are discounted.
126
127
 
@@ -128,13 +129,13 @@ A discount on individual line items. Only items that match the promotion's rules
128
129
 
129
130
  **Use case:** "15% off shoes", "Buy 2+ shirts get 10% off each".
130
131
 
131
- ### FreeShipping
132
+ ### Free shipping (`FreeShipping`)
132
133
 
133
134
  Writes a discount on each fulfillment covering its delivery cost. The row is kept even when the cost is zero — its presence is what marks the order as having free shipping.
134
135
 
135
136
  **Use case:** "Free shipping on orders over $75", "Free shipping with code FREESHIP".
136
137
 
137
- ### CreateLineItems
138
+ ### Free items (`CreateLineItems`)
138
139
 
139
140
  Adds specified products to the cart when the promotion is eligible, checking stock first. Items are not removed automatically if eligibility is lost — customers remove them manually.
140
141
 
@@ -245,7 +246,7 @@ Automatic promotions need no customer action at all — they are evaluated on ev
245
246
 
246
247
  ## Related Documentation
247
248
 
248
- - [Taxes, Discounts & Fees](taxes-discounts-fees.md) — the Discount rows promotions write, and manual discounts
249
+ - [Discounts](discounts.md) — the rows promotions write, and manual discounts
249
250
  - [Build Custom Promotion Rules & Actions](../how-to/custom-promotion.md) — step-by-step guide to custom rules, actions and adjusters
250
251
  - [Calculators](calculators.md) — promotion calculators
251
252
  - [Carts](carts.md) — the cart lifecycle promotions act on
@@ -3,13 +3,11 @@ title: Search & Filtering
3
3
  description: "Use Ransack-style query parameters to search, filter, sort, and paginate products, taxons, and other resources through the Spree Store API."
4
4
  ---
5
5
 
6
- import { Since } from '/snippets/since.mdx';
7
-
8
6
  ## Overview
9
7
 
10
8
  Spree provides powerful search, filtering, and sorting capabilities for products and other resources. The Store API supports:
11
9
 
12
- - Full-text search across product names, SKUs, and searchable product metafields
10
+ - Full-text search across product names, SKUs, and searchable custom fields
13
11
  - Attribute-based filtering (price range, availability, stock status)
14
12
  - Category and taxon filtering
15
13
  - Faceted search with filter counts
@@ -276,7 +274,7 @@ curl 'https://api.mystore.com/api/v3/store/products?page=1&limit=24' \
276
274
 
277
275
  See [Querying](../../api-reference/store-api/querying.md) for the full list of filtering, sorting, and pagination options.
278
276
 
279
- ## Search Providers
277
+ ## Search Providers
280
278
 
281
279
  Spree uses a pluggable search provider architecture. The default provider uses SQL (ILIKE + Ransack). For production catalogs with 1,000+ products, we recommend switching to [Meilisearch](../../integrations/search/meilisearch.md) for typo tolerance, relevance ranking, and faster faceted search.
282
280
 
@@ -3,9 +3,6 @@ title: Store Credits & Gift Cards
3
3
  description: How Spree models store credits and gift cards — stored value balances, redeemable codes, and checkout usage for refunds, loyalty, and gifting.
4
4
  ---
5
5
 
6
- import { Since } from '/snippets/since.mdx';
7
-
8
-
9
6
  ## Overview
10
7
 
11
8
  Spree provides two stored value mechanisms that customers can use at checkout:
@@ -1,163 +1,175 @@
1
1
  ---
2
2
  title: Taxes
3
- description: Tax categories, tax rates, zones, and how Spree calculates taxes at checkout
3
+ description: How Spree works out tax — tax categories and rates, inclusive versus added-on pricing, and connecting an external tax service.
4
4
  ---
5
5
 
6
6
  ## Overview
7
7
 
8
- Spree uses Tax Categories and Tax Rates to calculate taxes for orders. Products are assigned to tax categories, and tax rates define the percentage charged within specific geographic [Zones](addresses.md#zones).
8
+ Tax is the part of commerce most likely to differ from what you assumed. A US shopper expects `$17.99` on the shelf and a little more at the till. A German shopper expects `€17.99` to be the whole story, VAT already inside it. Both are correct, and a store selling to both has to do both.
9
+
10
+ Spree handles this by keeping three things separate:
11
+
12
+ - **What is being sold** — a [tax category](#tax-categories) groups products taxed the same way
13
+ - **Where the customer is** — their [market](markets.md) and shipping address
14
+ - **Who works out the number** — Spree's own rates, or an external tax service
15
+
16
+ The result of all this is a set of **tax lines** on the order — one per charge, recording what was taxed and at what rate. This page is about how they get their numbers.
9
17
 
10
18
  ```mermaid
11
- erDiagram
12
- TaxCategory ||--o{ TaxRate : "has many"
13
- TaxCategory ||--o{ Product : "has many"
14
- TaxRate }o--|| Zone : "applies in"
15
- TaxRate ||--o{ Adjustment : "creates"
16
- Zone ||--o{ ZoneMember : "has many"
17
-
18
- TaxCategory {
19
- string name
20
- boolean is_default
21
- string tax_code
22
- }
23
-
24
- TaxRate {
25
- string name
26
- decimal amount
27
- boolean included_in_price
28
- }
29
-
30
- Zone {
31
- string name
32
- string kind
33
- }
19
+ flowchart LR
20
+ Product -->|"has a"| TaxCategory
21
+ Address -->|"determines"| Market
22
+ Market -->|"selects"| Provider["Tax provider"]
23
+ TaxCategory --> Provider
24
+ Provider -->|"writes"| TaxLine["Tax lines on the order"]
34
25
  ```
35
26
 
36
- **Key relationships:**
37
- - **Tax Category** groups products for tax purposes (e.g., "Clothing", "Food", "Digital")
38
- - **Tax Rate** defines the percentage and rules for a Tax Category within a [Zone](addresses.md#zones)
39
- - **Zone** defines geographic regions (countries or states) where taxes apply
40
- - **[Adjustments](taxes-discounts-fees.md)** are created on orders/line items to apply taxes
27
+ ## Tax categories
41
28
 
42
- ## Tax Categories
29
+ A tax category is how you say "these things are taxed alike". Children's clothing, books and hot food are treated differently from general goods in many countries, and a tax category is where that distinction lives.
43
30
 
44
- Tax categories group products by how they're taxed. Common examples:
31
+ Every product carries one. If you don't choose, the default is used.
45
32
 
46
- - **Clothing** standard tax rate
47
- - **Food** — reduced or exempt rate
48
- - **Digital** different rules per jurisdiction
33
+ | Field | Description | Example |
34
+ |---|---|---|
35
+ | `name` | What it's called | `Standard`, `Reduced`, `Digital` |
36
+ | `is_default` | Used by products that don't pick one | `true` |
37
+ | `tax_code` | The code your tax service knows it by | `1257L` |
49
38
 
50
- Each product is assigned a tax category. One category can be set as the default for products that don't have an explicit assignment.
39
+ ```typescript Admin SDK
40
+ const reduced = await adminClient.taxCategories.create({
41
+ name: 'Reduced rate',
42
+ tax_code: '1257L',
43
+ })
44
+ ```
51
45
 
52
- | Attribute | Description | Example |
53
- |-----------|-------------|---------|
54
- | `name` | Category name | `Clothing` |
55
- | `is_default` | Whether this is the default category | `true` |
56
- | `tax_code` | Code from your tax provider (e.g., Stripe, Avalara) | `1257L` |
46
+ `tax_code` matters once you connect an external service — it is how you tell that service what kind of thing this is, in the vocabulary it already uses.
57
47
 
58
- Manage tax categories via the [Admin API](../../api-reference/admin-api/introduction.md), using the [Admin SDK resource clients](../sdk/admin/resources.md):
48
+ ## Tax rates
59
49
 
50
+ A tax rate is the number itself, plus where it applies and whether it's already in the price.
60
51
 
61
- ```typescript Admin SDK
62
- import { createAdminClient } from '@spree/admin-sdk'
52
+ | Field | Description | Example |
53
+ |---|---|---|
54
+ | `name` | What appears on the invoice | `VAT 20%` |
55
+ | `amount` | The rate as a decimal | `0.2` for 20% |
56
+ | `included_in_price` | Whether the price already contains it | `true` in the EU |
57
+ | `country_code` / `state_code` | Where it applies | `DE`, or `US` + `CA` |
58
+ | `tax_category` | What it applies to | `Standard` |
63
59
 
64
- const client = createAdminClient({
65
- baseUrl: 'https://store.example.com',
66
- secretKey: 'sk_xxx',
60
+ ```typescript Admin SDK
61
+ const { data: rates } = await adminClient.taxRates.list()
62
+
63
+ await adminClient.taxRates.create({
64
+ name: 'VAT 20%',
65
+ amount: '0.2',
66
+ included_in_price: true,
67
+ country_code: 'GB',
68
+ tax_category_id: 'taxcat_xxx',
67
69
  })
70
+ ```
68
71
 
69
- const clothing = await client.taxCategories.create({
70
- name: 'Clothing',
71
- tax_code: '1257L',
72
- is_default: true,
73
- })
72
+ Rates name their country and state by **ISO code**, not by an ID — `DE`, `US` + `CA`. There's no separate region record to create first.
74
73
 
75
- await client.taxCategories.update('taxcat_xxx', { is_default: false })
76
- await client.taxCategories.delete('taxcat_xxx')
77
- ```
74
+ ## Included or added on
78
75
 
79
- ```bash CLI
80
- spree api post /tax_categories -d '{"name": "Clothing", "tax_code": "1257L", "is_default": true}'
81
- spree api get /tax_categories
82
- ```
76
+ `included_in_price` is the single most consequential setting here, so it's worth being precise about what it does.
83
77
 
78
+ **Added on (US style):**
84
79
 
85
- ## Tax Rates
80
+ The shelf price is what the product costs. Tax is worked out at checkout, once the address is known, and added.
86
81
 
87
- Tax rates define the percentage charged for a specific tax category within a geographic zone.
82
+ A $17.99 item at 5%:
88
83
 
89
- | Attribute | Description | Example |
90
- |-----------|-------------|---------|
91
- | `name` | Rate name | `California Sales Tax` |
92
- | `amount` | Tax percentage as a decimal | `0.08` (8%) |
93
- | `included_in_price` | Whether tax is included in the displayed price | `false` |
94
- | `zone` | Geographic region where this rate applies | `US` |
95
- | `tax_category` | Products this rate applies to | `Clothing` |
84
+ | | |
85
+ |---|---|
86
+ | Price shown | **$17.99** |
87
+ | Tax added | $0.90 |
88
+ | Customer pays | **$18.89** |
96
89
 
97
- ## Tax Types
90
+ Set `included_in_price: false`.
98
91
 
99
- ### Sales Tax (tax-exclusive)
92
+ **Included (EU style):**
100
93
 
101
- Common in the United States. The displayed price does **not** include taxtax is added at checkout based on the shipping address.
94
+ The shelf price is the whole price. The tax inside it is shown for information the customer pays the number they saw.
102
95
 
103
- **Example:** A $17.99 item with 5% sales tax:
104
- - Displayed price: **$17.99**
105
- - Tax at checkout: **$0.90**
106
- - Order total: **$18.89**
96
+ A £17.99 item at 20% VAT:
107
97
 
108
- ### Value Added Tax / VAT (tax-inclusive)
98
+ | | |
99
+ |---|---|
100
+ | Price shown | **£17.99** |
101
+ | VAT inside it | £3.00 |
102
+ | Customer pays | **£17.99** |
109
103
 
110
- Common in Europe and many other countries. The displayed price **already includes** tax. When shipping outside the tax zone, the tax is removed from the price.
104
+ Set `included_in_price: true`.
111
105
 
112
- **Example:** A £17.99 item with 5% VAT included:
113
- - Displayed price: **£17.99** (includes £0.86 VAT)
114
- - If shipped outside VAT zone: price reduces to **£17.13**
115
106
 
116
- > **INFO:** The `tax_inclusive` setting on [Markets](markets.md) controls whether prices are displayed with or without tax for each geographic region.
107
+ > **WARNING:** When building an order summary, add `additional_tax_total` only. `included_tax_total` is **already inside** the item prices adding it counts the tax twice. This is the most common bug in a European storefront.
117
108
 
118
- ## Default Tax Zone
109
+ Because prices are shown before anyone knows where the shopper lives, each [Market](markets.md) declares whether its prices are quoted with tax included. That's what lets a catalogue page show honest prices to a German visitor and a US visitor at the same time.
119
110
 
120
- Spree uses a default tax zone to estimate taxes before the customer enters a shipping address. This is important for stores with tax-inclusive pricing (VAT) — it determines which tax rate is assumed in the displayed price.
111
+ ## Tax lines
121
112
 
122
- If the customer's shipping address is outside the default tax zone, the assumed tax is removed and the correct rate for their zone is applied.
113
+ Whatever works out the tax, the record is the same: a tax line per charge, saying what was taxed and how.
123
114
 
124
- ## Tax Calculation at Checkout
115
+ | Attribute | Description |
116
+ |---|---|
117
+ | `label` | What the customer sees — `VAT 20%` |
118
+ | `rate` | The rate applied — `"0.2"` |
119
+ | `included` | Whether the tax was already inside the displayed price |
120
+ | `amount` / `display_amount` | The tax charged |
121
+ | `line_item_id` / `fulfillment_id` / `fee_id` | What was taxed — exactly one is set |
125
122
 
126
- During checkout, taxes are calculated based on the shipping address:
123
+ ```typescript Admin SDK
124
+ const { data: taxLines } = await adminClient.orders.taxLines.list('or_xxx')
125
+ ```
127
126
 
128
- 1. The customer's shipping address determines their [Zone](addresses.md#zones)
129
- 2. Spree finds matching tax rates for that zone and the product's tax category
130
- 3. Tax [Adjustments](taxes-discounts-fees.md) are created on line items
131
- 4. The order total is updated
127
+ Tax lines attach to a line item, a [fulfillment](fulfillments.md), or a [fee](fees.md) — so tax on goods and tax on delivery stay separately visible, which is what a tax return needs.
132
128
 
133
- > **NOTE:** For complex tax requirements (interstate US sales, international VAT), consider using automated tax calculation. The [Stripe Tax integration](../../integrations/payments/stripe.md) handles this automatically.
129
+ Each line also keeps its own copy of the rate and label. If someone edits that tax rate next year, the order still says what the customer was actually charged.
134
130
 
135
- ## Managing Taxes
131
+ You never create tax lines yourself; they're written whenever the total is worked out.
132
+
133
+ ## When tax is worked out
134
+
135
+ Tax is recalculated whenever the answer could have changed — an item added, an address entered, a delivery option chosen. Every one of those requests returns the updated cart, so a storefront never needs a separate "refresh the tax" call.
136
+
137
+ **Step 1: Before an address is known**
138
+
139
+ Prices are shown using the market's tax treatment. For an inclusive market the displayed price is the real one; for an added-on market tax simply hasn't been worked out yet.
140
+
141
+ **Step 2: Once the shipping address is entered**
142
+
143
+ The address decides the real rate. Tax lines are written against each item, the delivery charge, and any taxable fee.
144
+
145
+ **Step 3: At completion**
146
+
147
+ Whatever the tax is at that moment is what's charged, and the order keeps it. A rate change next month does not rewrite an order from last month.
148
+
149
+
150
+ ## Connecting a tax service
151
+
152
+ Spree's built-in rate tables are fine when the rules are simple — one country, or a handful of clearly-defined rates. They become a liability where the rules are not: US sales tax varies by city and changes constantly, and cross-border EU VAT has thresholds that shift with your turnover.
153
+
154
+ For those, connect a real tax service. **The provider is chosen per [Market](markets.md)**, which means one store can use its own rates in a simple market and an external service in a complicated one.
155
+
156
+ ```typescript Admin SDK
157
+ const { data: providers } = await adminClient.taxProviders.list()
158
+ ```
136
159
 
137
- Tax categories and rates are managed in the Admin Panel under **Settings Taxes**, or via the Admin API.
160
+ A provider declares up front what it cannot do no US local tax, no reverse charge — so the dashboard can warn a merchant who pairs it with a market that needs it, rather than quietly under-collecting.
138
161
 
139
- ### Common Configurations
162
+ Whichever provider is in use, the result is the same: tax lines on the order, in the same shape. Your storefront code doesn't change.
140
163
 
141
- **US store with state-level sales tax:**
142
- - Create zones for each taxable state
143
- - Create tax rates per zone (e.g., 8.25% for California, 6.25% for Texas)
144
- - Set `included_in_price: false`
164
+ ## Tax-exempt customers
145
165
 
146
- **EU store with VAT:**
147
- - Create a zone for EU countries
148
- - Create tax rates per country (e.g., 19% Germany, 20% France)
149
- - Set `included_in_price: true`
150
- - Set the EU zone as the default tax zone
166
+ Business customers are often exempt, and the paperwork is real. Spree records a customer's or company's **tax identifier** a VAT number, an ABN — and can validate it. Companies can also hold exemption certificates, scoped to the country or state that issued them.
151
167
 
152
- **Digital products in the EU:**
153
- - Create a "Digital" tax category
154
- - Add tax rates for each EU country using the customer's country VAT rate
155
- - Physical products use the seller's country VAT rate
168
+ Exemption is decided when tax is worked out, not stored as a flag on the customer, so an expired certificate stops applying by itself. See [Companies & Catalogs](companies-and-catalogs.md).
156
169
 
157
- ## Related Documentation
170
+ ## Related
158
171
 
159
- - [Markets](markets.md) — Tax-inclusive pricing per market
160
- - [Addresses](addresses.md) — Zones and address-based taxation
161
- - [Adjustments](taxes-discounts-fees.md) — Tax adjustments
162
- - [Orders](orders.md) — How taxes affect order totals
163
- - [Avalara integration](../../integrations/tax/avalara.md) — Automated tax calculation for complex jurisdictions
172
+ - [Order totals](order-totals.md) — how tax rolls into what a customer pays
173
+ - [Markets](markets.md) — where tax treatment and provider are chosen
174
+ - [Addresses](addresses.md) — what determines the rate
175
+ - [Companies & Catalogs](companies-and-catalogs.md) — B2B tax identifiers and exemptions