@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.
- package/dist/developer/core-concepts/addresses.md +106 -198
- package/dist/developer/core-concepts/architecture.md +97 -126
- package/dist/developer/core-concepts/calculators.md +75 -252
- package/dist/developer/core-concepts/carts.md +1 -1
- package/dist/developer/core-concepts/channels.md +0 -4
- package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
- package/dist/developer/core-concepts/customers.md +0 -3
- package/dist/developer/core-concepts/discounts.md +133 -0
- package/dist/developer/core-concepts/events.md +83 -576
- package/dist/developer/core-concepts/fees.md +144 -0
- package/dist/developer/core-concepts/imports-exports.md +105 -679
- package/dist/developer/core-concepts/inventory.md +114 -248
- package/dist/developer/core-concepts/markets.md +9 -12
- package/dist/developer/core-concepts/media.md +9 -11
- package/dist/developer/core-concepts/metafields.md +123 -200
- package/dist/developer/core-concepts/order-totals.md +110 -0
- package/dist/developer/core-concepts/orders.md +1 -1
- package/dist/developer/core-concepts/payments.md +11 -14
- package/dist/developer/core-concepts/pricing.md +11 -13
- package/dist/developer/core-concepts/products.md +35 -19
- package/dist/developer/core-concepts/promotions.md +12 -11
- package/dist/developer/core-concepts/search-filtering.md +2 -4
- package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
- package/dist/developer/core-concepts/taxes.md +125 -113
- package/dist/developer/core-concepts/translations.md +61 -68
- package/dist/developer/core-concepts/webhooks.md +25 -59
- package/dist/developer/how-to/custom-promotion.md +3 -3
- package/package.json +1 -1
- 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
|
-
| **
|
|
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
|
-
|
|
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
|
-
###
|
|
184
|
+
### Channel Rule
|
|
187
185
|
|
|
188
|
-
Applies
|
|
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
|
-
|
|
|
219
|
-
| Customer |
|
|
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 : "
|
|
20
|
-
Product
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Variant
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
57
|
+
### When promotions compete
|
|
56
58
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
Every product carries one. If you don't choose, the default is used.
|
|
45
32
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
39
|
+
```typescript Admin SDK
|
|
40
|
+
const reduced = await adminClient.taxCategories.create({
|
|
41
|
+
name: 'Reduced rate',
|
|
42
|
+
tax_code: '1257L',
|
|
43
|
+
})
|
|
44
|
+
```
|
|
51
45
|
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
await client.taxCategories.delete('taxcat_xxx')
|
|
77
|
-
```
|
|
74
|
+
## Included or added on
|
|
78
75
|
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
A $17.99 item at 5%:
|
|
88
83
|
|
|
89
|
-
|
|
|
90
|
-
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
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
|
-
|
|
90
|
+
Set `included_in_price: false`.
|
|
98
91
|
|
|
99
|
-
|
|
92
|
+
**Included (EU style):**
|
|
100
93
|
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
| | |
|
|
99
|
+
|---|---|
|
|
100
|
+
| Price shown | **£17.99** |
|
|
101
|
+
| VAT inside it | £3.00 |
|
|
102
|
+
| Customer pays | **£17.99** |
|
|
109
103
|
|
|
110
|
-
|
|
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
|
-
> **
|
|
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
|
-
|
|
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
|
-
|
|
111
|
+
## Tax lines
|
|
121
112
|
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
```typescript Admin SDK
|
|
124
|
+
const { data: taxLines } = await adminClient.orders.taxLines.list('or_xxx')
|
|
125
|
+
```
|
|
127
126
|
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
|
170
|
+
## Related
|
|
158
171
|
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
161
|
-
- [
|
|
162
|
-
- [
|
|
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
|