@spree/docs 0.1.230 → 0.1.232

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 (46) hide show
  1. package/dist/api-reference/store.yaml +28 -0
  2. package/dist/developer/agentic/agent-skills.md +1 -1
  3. package/dist/developer/cli/quickstart.md +2 -2
  4. package/dist/developer/core-concepts/addresses.md +13 -0
  5. package/dist/developer/core-concepts/calculators.md +6 -1
  6. package/dist/developer/core-concepts/carts.md +19 -1
  7. package/dist/developer/core-concepts/catalogs.md +51 -2
  8. package/dist/developer/core-concepts/channels.md +20 -1
  9. package/dist/developer/core-concepts/commissions.md +70 -1
  10. package/dist/developer/core-concepts/companies.md +74 -0
  11. package/dist/developer/core-concepts/customers.md +17 -5
  12. package/dist/developer/core-concepts/data-privacy.md +20 -7
  13. package/dist/developer/core-concepts/delivery-setup.md +57 -0
  14. package/dist/developer/core-concepts/discounts.md +7 -0
  15. package/dist/developer/core-concepts/events.md +13 -7
  16. package/dist/developer/core-concepts/fees.md +20 -0
  17. package/dist/developer/core-concepts/fulfillments.md +3 -3
  18. package/dist/developer/core-concepts/imports-exports.md +21 -5
  19. package/dist/developer/core-concepts/inventory.md +73 -1
  20. package/dist/developer/core-concepts/markets.md +5 -6
  21. package/dist/developer/core-concepts/media.md +66 -1
  22. package/dist/developer/core-concepts/metafields.md +1 -1
  23. package/dist/developer/core-concepts/order-totals.md +18 -1
  24. package/dist/developer/core-concepts/orders.md +2 -1
  25. package/dist/developer/core-concepts/payments.md +100 -20
  26. package/dist/developer/core-concepts/pricing.md +13 -8
  27. package/dist/developer/core-concepts/products.md +52 -1
  28. package/dist/developer/core-concepts/reporting.md +5 -5
  29. package/dist/developer/core-concepts/returns-exchanges-claims.md +41 -2
  30. package/dist/developer/core-concepts/search-filtering.md +1 -2
  31. package/dist/developer/core-concepts/sellers.md +109 -2
  32. package/dist/developer/core-concepts/slugs.md +1 -1
  33. package/dist/developer/core-concepts/staff-roles.md +23 -3
  34. package/dist/developer/core-concepts/store-credits-gift-cards.md +60 -23
  35. package/dist/developer/core-concepts/stores.md +21 -0
  36. package/dist/developer/core-concepts/taxes.md +43 -0
  37. package/dist/developer/core-concepts/translations.md +9 -0
  38. package/dist/developer/core-concepts/webhooks.md +45 -3
  39. package/dist/developer/create-spree-app/quickstart.md +10 -10
  40. package/dist/developer/dashboard/deployment.md +4 -4
  41. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +1 -1
  42. package/dist/developer/deployment/docker.md +2 -2
  43. package/dist/developer/how-to/custom-promotion.md +33 -13
  44. package/dist/developer/multi-tenant/quickstart.md +6 -6
  45. package/dist/developer/storefront/nextjs/testing.md +2 -2
  46. package/package.json +1 -1
@@ -15251,6 +15251,34 @@ components:
15251
15251
  required:
15252
15252
  - id
15253
15253
  x-typelizer: true
15254
+ StoreCreditEvent:
15255
+ type: object
15256
+ properties:
15257
+ id:
15258
+ type: string
15259
+ action:
15260
+ type: string
15261
+ authorization_code:
15262
+ type: string
15263
+ nullable: true
15264
+ display_action:
15265
+ type: string
15266
+ nullable: true
15267
+ amount:
15268
+ type: string
15269
+ display_amount:
15270
+ type: string
15271
+ created_at:
15272
+ type: string
15273
+ required:
15274
+ - id
15275
+ - action
15276
+ - authorization_code
15277
+ - display_action
15278
+ - amount
15279
+ - display_amount
15280
+ - created_at
15281
+ x-typelizer: true
15254
15282
  StoreCredit:
15255
15283
  type: object
15256
15284
  properties:
@@ -52,7 +52,7 @@ The plugin delivers everything the skills CLI does **plus** `/spree:doctor`, `/s
52
52
 
53
53
  The skills detect your project flavor before running commands:
54
54
 
55
- - **create-spree-app projects** (Docker, Rails app in `backend/`) — commands route through the [Spree CLI](../cli/quickstart.md) (`spree migrate`, `spree console`, …).
55
+ - **create-spree-app projects** (Docker, Rails app in `server/`) — commands route through the [Spree CLI](../cli/quickstart.md) (`spree migrate`, `spree console`, …).
56
56
  - **Classic Rails apps** (Spree gems in a plain Rails app, no Docker — typical for apps created before Spree 5.4) — the skills fall back to native commands (`bin/rails`, `bundle exec rake`) from the app root.
57
57
 
58
58
  ## Updating
@@ -114,13 +114,13 @@ Production upgrades only need the third stage — `bundle install` and `db:migra
114
114
 
115
115
  ### `spree eject`
116
116
 
117
- Switch from the prebuilt Docker image to building from your local `backend/` directory. This lets you customize the Rails app — add gems, override models, add migrations, etc.
117
+ Switch from the prebuilt Docker image to building from your local `server/` directory. This lets you customize the Rails app — add gems, override models, add migrations, etc.
118
118
 
119
119
  ```bash
120
120
  spree eject
121
121
  ```
122
122
 
123
- After ejecting, the Docker image is built from `backend/Dockerfile`. Edit files in `backend/` and run `spree dev` to rebuild and restart with your changes.
123
+ After ejecting, the Docker image is built from `server/Dockerfile`. Edit files in `server/` and run `spree dev` to rebuild and restart with your changes.
124
124
 
125
125
  See [Customizing the Backend](../create-spree-app/quickstart.md#customizing-the-backend) for details on what you can customize.
126
126
 
@@ -126,6 +126,7 @@ curl -X POST 'https://api.mystore.com/api/v3/store/customers/me/addresses' \
126
126
 
127
127
  On a cart you can point at a saved address or send a new one inline:
128
128
 
129
+
129
130
  ```typescript Store SDK
130
131
  // Reuse something from the address book
131
132
  await client.carts.update(cartId, {
@@ -148,6 +149,18 @@ await client.carts.update(cartId, {
148
149
  })
149
150
  ```
150
151
 
152
+ ```bash cURL
153
+ curl -X PATCH 'https://api.mystore.com/api/v3/store/carts/cart_xxx' \
154
+ -H 'X-Spree-API-Key: pk_xxx' \
155
+ -H 'Authorization: Bearer $CUSTOMER_JWT' \
156
+ -H 'Content-Type: application/json' \
157
+ -d '{
158
+ "shipping_address_id": "addr_xxx",
159
+ "billing_address_id": "addr_yyy"
160
+ }'
161
+ ```
162
+
163
+
151
164
  Setting the shipping address is what makes delivery options and tax appear, so do it before asking the customer to choose delivery. The updated cart comes back with new totals — you don't need a second request.
152
165
 
153
166
  > **WARNING:** An address is **copied onto the order** when the cart completes, not linked to. Editing a saved address next year does not rewrite last year's invoice — which is exactly what you want when a customer moves house.
@@ -94,7 +94,7 @@ const calculators = await adminClient.deliveryMethods.calculators()
94
94
  await adminClient.deliveryMethods.create({
95
95
  name: 'Standard shipping',
96
96
  delivery_zone_id: 'dz_xxx',
97
- calculator_type: 'Spree::Calculator::Shipping::FlatRate',
97
+ calculator_type: 'flat_rate',
98
98
  calculator_attributes: {
99
99
  preferences: { amount: '7.00', currency: 'USD' },
100
100
  },
@@ -105,6 +105,11 @@ await adminClient.deliveryMethods.create({
105
105
  spree api get /delivery_methods/calculators
106
106
  ```
107
107
 
108
+ ```bash cURL
109
+ curl 'https://api.mystore.com/api/v3/admin/delivery_methods/calculators' \
110
+ -H 'X-Spree-API-Key: sk_xxx'
111
+ ```
112
+
108
113
 
109
114
  Each calculator declares which settings it takes, so a form can be built for it without knowing the calculator in advance — which is how the dashboard renders these.
110
115
 
@@ -101,15 +101,24 @@ Every change returns the whole cart with new totals, so you never need a second
101
101
 
102
102
  When a guest signs in, attach their cart to the account:
103
103
 
104
+
104
105
  ```typescript Store SDK
105
106
  await client.carts.associate(cartId, { spreeToken: 'abc123' })
106
107
  ```
107
108
 
109
+ ```bash cURL
110
+ curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/associate' \
111
+ -H 'X-Spree-API-Key: pk_xxx' \
112
+ -H 'Authorization: Bearer $CUSTOMER_JWT' \
113
+ -H 'X-Spree-Token: abc123'
114
+ ```
115
+
116
+
108
117
  ## Checkout requirements
109
118
 
110
119
  A cart tells you what it still needs before it can be completed:
111
120
 
112
- ```json
121
+ ```json Response
113
122
  {
114
123
  "requirements": [
115
124
  {
@@ -176,12 +185,21 @@ For providers like Stripe, create a payment session and confirm it with their ow
176
185
 
177
186
  Discount codes, gift cards and store credit can be applied any time before completion:
178
187
 
188
+
179
189
  ```typescript Store SDK
180
190
  await client.carts.discountCodes.apply(cartId, 'SUMMER20')
181
191
  await client.carts.giftCards.apply(cartId, 'GIFT-XXXX')
182
192
  await client.carts.storeCredits.apply(cartId)
183
193
  ```
184
194
 
195
+ ```bash cURL
196
+ curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/discount_codes' \
197
+ -H 'X-Spree-API-Key: pk_xxx' \
198
+ -H 'Content-Type: application/json' \
199
+ -d '{ "code": "SUMMER20" }'
200
+ ```
201
+
202
+
185
203
  ## What completion guarantees
186
204
 
187
205
  Completing a cart moves real money, so Spree is careful about it:
@@ -21,6 +21,8 @@ erDiagram
21
21
  Catalog ||--o{ CatalogProduct : "assortment"
22
22
  Catalog ||--o{ CatalogAssignment : "audiences"
23
23
  Catalog ||--o| PriceList : "owns (optional)"
24
+ Catalog ||--o{ CatalogQuantityRule : "per-SKU terms"
25
+ Catalog ||--o{ CatalogOrderMinimum : "cart minimum"
24
26
  CatalogAssignment }o--|| CustomerGroup : "one of"
25
27
  CatalogAssignment }o--|| Company : "one of"
26
28
 
@@ -28,6 +30,8 @@ erDiagram
28
30
  string name
29
31
  boolean active
30
32
  integer position
33
+ integer minimum_order_quantity
34
+ integer order_multiple
31
35
  }
32
36
  ```
33
37
 
@@ -48,14 +52,29 @@ That's worth pausing on, because it's the one behaviour that surprises people: a
48
52
 
49
53
  ## Creating one
50
54
 
55
+
51
56
  ```typescript Admin SDK
52
57
  const catalog = await adminClient.catalogs.create({
53
58
  name: 'Wholesale',
54
- price_list_id: 'plist_xxx',
55
- active: true,
59
+ price_list: { name: 'Wholesale pricing' },
56
60
  })
61
+
62
+ await adminClient.catalogs.activate(catalog.id)
63
+ ```
64
+
65
+ ```bash cURL
66
+ # Create the agreement with its pricing inline
67
+ curl -X POST 'https://api.mystore.com/api/v3/admin/catalogs' \
68
+ -H 'X-Spree-API-Key: sk_xxx' \
69
+ -H 'Content-Type: application/json' \
70
+ -d '{ "name": "Wholesale", "price_list": { "name": "Wholesale pricing" } }'
71
+
72
+ # Catalogs are born inactive — going live is its own act
73
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/catalogs/cat_xxx/activate' \
74
+ -H 'X-Spree-API-Key: sk_xxx'
57
75
  ```
58
76
 
77
+
59
78
  The price list is optional. A catalog with an assortment and no price list restricts the range at normal prices; a catalog with a price list and no assortment adjusts prices without hiding anything.
60
79
 
61
80
  A price list is either **standalone** (matched by its own rules) or **owned by exactly one catalog**. A catalog creates the list it prices through, so the two are configured together on the catalog page — an owned list never appears in the price-lists index, since it has no rules or audience of its own.
@@ -64,6 +83,7 @@ Removing a catalog's pricing (`price_list: null`), or deleting the catalog itsel
64
83
 
65
84
  ### Filling the assortment
66
85
 
86
+
67
87
  ```typescript Admin SDK
68
88
  // Add products (a bare array of IDs)
69
89
  await adminClient.catalogs.products.create(catalog.id, ['prod_xxx', 'prod_yyy'])
@@ -73,14 +93,34 @@ const { data: products } = await adminClient.catalogs.products.list(catalog.id)
73
93
  await adminClient.catalogs.products.delete(catalog.id, 'prod_xxx')
74
94
  ```
75
95
 
96
+ ```bash cURL
97
+ # Add products to the assortment
98
+ curl -X POST 'https://api.mystore.com/api/v3/admin/catalogs/cat_xxx/products' \
99
+ -H 'X-Spree-API-Key: sk_xxx' \
100
+ -H 'Content-Type: application/json' \
101
+ -d '{ "product_ids": ["prod_xxx", "prod_yyy"] }'
102
+
103
+ # List them
104
+ curl 'https://api.mystore.com/api/v3/admin/catalogs/cat_xxx/products' \
105
+ -H 'X-Spree-API-Key: sk_xxx'
106
+ ```
107
+
108
+
76
109
  Membership is all a catalog holds — it decides *what* a buyer sees, never the order they see it in. Presentation order stays with categories and collections.
77
110
 
78
111
  When a catalog should restrict to exactly what its price list covers, there's a shortcut that copies those products in rather than making someone add them by hand:
79
112
 
113
+
80
114
  ```typescript Admin SDK
81
115
  const { added_count } = await adminClient.catalogs.importProducts(catalog.id)
82
116
  ```
83
117
 
118
+ ```bash cURL
119
+ curl -X POST 'https://api.mystore.com/api/v3/admin/catalogs/cat_xxx/import_products' \
120
+ -H 'X-Spree-API-Key: sk_xxx'
121
+ ```
122
+
123
+
84
124
  ## Choosing the audience
85
125
 
86
126
  A catalog is assigned to a buyer audience:
@@ -92,6 +132,7 @@ A catalog is assigned to a buyer audience:
92
132
 
93
133
  A [Channel](channels.md) is not assigned — it names its catalog directly via `default_catalog_id`, which applies to everyone buying through it when nothing narrower does.
94
134
 
135
+
95
136
  ```typescript Admin SDK
96
137
  await adminClient.catalogs.assign(catalog.id, {
97
138
  assignable_type: 'customer_group',
@@ -99,6 +140,14 @@ await adminClient.catalogs.assign(catalog.id, {
99
140
  })
100
141
  ```
101
142
 
143
+ ```bash cURL
144
+ curl -X POST 'https://api.mystore.com/api/v3/admin/catalogs/cat_xxx/assign' \
145
+ -H 'X-Spree-API-Key: sk_xxx' \
146
+ -H 'Content-Type: application/json' \
147
+ -d '{ "assignable_type": "customer_group", "assignable_id": "cgrp_xxx" }'
148
+ ```
149
+
150
+
102
151
  > **NOTE:** **A company assignment covers the whole subtree.** Assign the group-wide catalog once at the root of a [company tree](companies.md) and every division below inherits it — no re-assigning per branch. A branch can still add its own catalog on top.
103
152
 
104
153
  ## What a shopper ends up seeing
@@ -9,6 +9,25 @@ Channels segment a single [Store](stores.md) into distinct selling surfaces. A c
9
9
 
10
10
  Every store ships with one default channel named *Online Store*. You can add more from **Settings → Sales channels** in the admin dashboard.
11
11
 
12
+ ```mermaid
13
+ erDiagram
14
+ Store ||--o{ Channel : "has many"
15
+ Channel ||--o{ ProductPublication : "publishes"
16
+ ProductPublication }o--|| Product : "lists"
17
+ Channel ||--o{ Order : "attributes"
18
+ Channel ||--o{ OrderRoutingRule : "routes by"
19
+ Channel ||--o{ ChannelStockLocation : "serves from"
20
+ Channel }o--o| Catalog : "default assortment"
21
+ Channel ||--o{ ApiKey : "authenticates"
22
+
23
+ Channel {
24
+ string name
25
+ string code
26
+ boolean active
27
+ boolean default
28
+ }
29
+ ```
30
+
12
31
  ## Channel Attributes
13
32
 
14
33
  | Attribute | Description | Example |
@@ -206,7 +225,7 @@ The write contract is **full-set**: the array represents the complete desired st
206
225
 
207
226
  - **Dashboard** — new products are auto-published on the store's default channel. The merchant can untick channels via the Publishing card post-create.
208
227
  - **Admin API** — new products are **not** auto-published. The caller supplies `product_publications: [{ channel_id }]` on create, or calls `POST /admin/channels/:id/add_products` afterwards.
209
- - **Sample data** (`bin/rake spree:load_sample_data`) — all loaded products are explicitly published on the default channel.
228
+ - **Sample data** (`spree sample-data`) — all loaded products are explicitly published on the default channel.
210
229
 
211
230
  ## Related Documentation
212
231
 
@@ -31,6 +31,7 @@ flowchart LR
31
31
 
32
32
  A rate says what to charge. It's either a percentage of the sale or a flat amount.
33
33
 
34
+
34
35
  ```typescript Admin SDK
35
36
  await adminClient.commissionRates.create({
36
37
  name: 'Electronics',
@@ -41,6 +42,20 @@ await adminClient.commissionRates.create({
41
42
  })
42
43
  ```
43
44
 
45
+ ```bash cURL
46
+ curl -X POST 'https://api.mystore.com/api/v3/admin/commission_rates' \
47
+ -H 'X-Spree-API-Key: sk_xxx' \
48
+ -H 'Content-Type: application/json' \
49
+ -d '{
50
+ "name": "Electronics",
51
+ "code": "electronics",
52
+ "kind": "percentage",
53
+ "value": "12.5",
54
+ "enabled": true
55
+ }'
56
+ ```
57
+
58
+
44
59
  | Attribute | Description |
45
60
  |---|---|
46
61
  | `name` | What operators call it |
@@ -84,6 +99,7 @@ Rules narrow when a rate applies. A rate can hold several, and **all of them mus
84
99
 
85
100
  So "electronics **and** these three sellers" is two rules on one rate.
86
101
 
102
+
87
103
  ```typescript Admin SDK
88
104
  // Every rule kind, with the schema describing its settings — so an editor
89
105
  // is built from what this marketplace actually has, not a hardcoded list
@@ -97,6 +113,23 @@ await adminClient.commissionRates.update('crate_xxx', {
97
113
  })
98
114
  ```
99
115
 
116
+ ```bash cURL
117
+ # Discover the rule kinds this marketplace has
118
+ curl 'https://api.mystore.com/api/v3/admin/commission_rates/rule_types' \
119
+ -H 'X-Spree-API-Key: sk_xxx'
120
+
121
+ # Attach rules to a rate
122
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/commission_rates/crate_xxx' \
123
+ -H 'X-Spree-API-Key: sk_xxx' \
124
+ -H 'Content-Type: application/json' \
125
+ -d '{
126
+ "rules": [
127
+ { "type": "category_rule", "preferences": { "category_ids": ["ctg_xxx"] } }
128
+ ]
129
+ }'
130
+ ```
131
+
132
+
100
133
  Rules are replaced wholesale on update — send the full set you want.
101
134
 
102
135
  ### The four rule types
@@ -164,6 +197,7 @@ Discounts come off either way. Commission is charged on what the customer actual
164
197
 
165
198
  Money is stated per currency, never converted:
166
199
 
200
+
167
201
  ```typescript Admin SDK
168
202
  // A percentage, with a floor and a cap in each currency
169
203
  await adminClient.commissionRates.create({
@@ -185,6 +219,34 @@ await adminClient.commissionRates.create({
185
219
  })
186
220
  ```
187
221
 
222
+ ```bash cURL
223
+ # A percentage, with a floor and a cap in each currency
224
+ curl -X POST 'https://api.mystore.com/api/v3/admin/commission_rates' \
225
+ -H 'X-Spree-API-Key: sk_xxx' \
226
+ -H 'Content-Type: application/json' \
227
+ -d '{
228
+ "name": "Standard",
229
+ "kind": "percentage",
230
+ "value": 10,
231
+ "bounds": {
232
+ "USD": { "min_amount": 1, "max_amount": 50 },
233
+ "EUR": { "min_amount": 1, "max_amount": 45 }
234
+ }
235
+ }'
236
+
237
+ # A flat fee, stated per currency
238
+ curl -X POST 'https://api.mystore.com/api/v3/admin/commission_rates' \
239
+ -H 'X-Spree-API-Key: sk_xxx' \
240
+ -H 'Content-Type: application/json' \
241
+ -d '{
242
+ "name": "Listing fee",
243
+ "kind": "fixed",
244
+ "value": 0,
245
+ "amounts": { "USD": "5.00", "GBP": "4.00" }
246
+ }'
247
+ ```
248
+
249
+
188
250
  - **`amounts`** — what a flat fee charges, per currency
189
251
  - **`bounds`** — the floor and cap a percentage charges within, per currency
190
252
 
@@ -229,12 +291,19 @@ When an order is placed, the resolved rate is applied and the result frozen as a
229
291
  | `line_item_id` / `fulfillment_id` | What was commissioned — exactly one |
230
292
  | `commission_rate_id` | The rate, if it still exists |
231
293
 
294
+
232
295
  ```typescript Admin SDK
233
296
  const { data: lines } = await adminClient.commissionLines.list({
234
- filter: { seller_id_eq: 'sel_xxx' },
297
+ seller_id_eq: 'sel_xxx',
235
298
  })
236
299
  ```
237
300
 
301
+ ```bash cURL
302
+ curl 'https://api.mystore.com/api/v3/admin/commission_lines?q[seller_id_eq]=sel_xxx' \
303
+ -H 'X-Spree-API-Key: sk_xxx'
304
+ ```
305
+
306
+
238
307
  **Every field is a snapshot.** Editing the rate afterwards changes nothing here, and neither does deleting it — a retired rate is soft-deleted precisely so "which rate charged this" stays answerable.
239
308
 
240
309
  There is **no write path**. Lines are read-only in the API, and correcting a charge means recording a reversal rather than editing history — the discipline any ledger needs.
@@ -59,6 +59,7 @@ So a buyer attached to "Acme Europe" can act for Sales and Operations beneath it
59
59
 
60
60
  This is why authorization always asks "does this person have standing on this node *or any of its ancestors*", never "is this person a member of exactly this node".
61
61
 
62
+
62
63
  ```typescript Store SDK
63
64
  // Which companies can this customer act for?
64
65
  const { data: memberships } = await client.account.companies()
@@ -69,6 +70,13 @@ memberships.forEach((m) => {
69
70
  })
70
71
  ```
71
72
 
73
+ ```bash cURL
74
+ curl 'https://api.mystore.com/api/v3/store/account/companies' \
75
+ -H 'X-Spree-API-Key: pk_xxx' \
76
+ -H 'Authorization: Bearer $CUSTOMER_JWT'
77
+ ```
78
+
79
+
72
80
  > **NOTE:** **In open source, every member can do everything within their standing** — buy, see the subtree's orders, and manage addresses and members. There are no company roles; the `role` label on a membership is cosmetic.
73
81
  >
74
82
  > To restrict what individual people may do, see [company governance](#company-governance) below.
@@ -77,12 +85,22 @@ memberships.forEach((m) => {
77
85
 
78
86
  Both the dashboard and the storefront add members the same way — by email:
79
87
 
88
+
80
89
  ```typescript Store SDK
81
90
  const result = await client.companies.members.create('comp_xxx', {
82
91
  customer_email: 'buyer@acme.com',
83
92
  })
84
93
  ```
85
94
 
95
+ ```bash cURL
96
+ curl -X POST 'https://api.mystore.com/api/v3/store/companies/comp_xxx/members' \
97
+ -H 'X-Spree-API-Key: pk_xxx' \
98
+ -H 'Authorization: Bearer $CUSTOMER_JWT' \
99
+ -H 'Content-Type: application/json' \
100
+ -d '{ "customer_email": "buyer@acme.com" }'
101
+ ```
102
+
103
+
86
104
  What comes back depends on whether that email is already a customer:
87
105
 
88
106
  - **An existing customer** becomes a member straight away.
@@ -92,6 +110,7 @@ You can tell which by the ID prefix — `cmem_` for a membership, `cinv_` for an
92
110
 
93
111
  Accepting an invitation either signs an existing customer in, or registers a new account with the invited email. Either way it ends as a membership:
94
112
 
113
+
95
114
  ```typescript Store SDK
96
115
  // The token comes from the invitation email — no sign-in needed to read it
97
116
  const invitation = await client.companyInvitations.lookup(token)
@@ -104,31 +123,60 @@ await client.companyInvitations.accept(token, {
104
123
  })
105
124
  ```
106
125
 
126
+ ```bash cURL
127
+ # Reading an invitation needs no sign-in — just the emailed token
128
+ curl 'https://api.mystore.com/api/v3/store/company_invitations/TOKEN' \
129
+ -H 'X-Spree-API-Key: pk_xxx'
130
+
131
+ curl -X POST 'https://api.mystore.com/api/v3/store/company_invitations/TOKEN/accept' \
132
+ -H 'X-Spree-API-Key: pk_xxx'
133
+ ```
134
+
135
+
107
136
  Memberships are always active and always backed by a real customer. Anything still waiting lives on the invitation, so there's no such thing as a half-active member.
108
137
 
109
138
  ## Buying for a company
110
139
 
111
140
  A cart and an order can name a company. That's what turns a personal purchase into a company one — it decides which addresses are on offer, whose tax registration applies, and who else will see the order.
112
141
 
142
+
113
143
  ```typescript Store SDK
114
144
  await client.carts.update(cartId, { company_id: 'comp_xxx' })
115
145
  ```
116
146
 
147
+ ```bash cURL
148
+ curl -X PATCH 'https://api.mystore.com/api/v3/store/carts/cart_xxx' \
149
+ -H 'X-Spree-API-Key: pk_xxx' \
150
+ -H 'Authorization: Bearer $CUSTOMER_JWT' \
151
+ -H 'Content-Type: application/json' \
152
+ -d '{ "company_id": "comp_xxx" }'
153
+ ```
154
+
155
+
117
156
  A buyer with exactly one membership doesn't need to choose; it resolves on its own. A buyer who belongs to several names the node — and it must be one they have standing on.
118
157
 
119
158
  The company is **frozen onto the order** at completion, like the addresses and prices. Reorganize the tree next year and last year's order still explains itself.
120
159
 
160
+
121
161
  ```typescript Store SDK
122
162
  // Everything anyone in the subtree has bought
123
163
  const { data: orders } = await client.companies.orders.list('comp_xxx')
124
164
  ```
125
165
 
166
+ ```bash cURL
167
+ curl 'https://api.mystore.com/api/v3/store/companies/comp_xxx/orders' \
168
+ -H 'X-Spree-API-Key: pk_xxx' \
169
+ -H 'Authorization: Bearer $CUSTOMER_JWT'
170
+ ```
171
+
172
+
126
173
  That subtree rollup is the feature a finance team actually asks for: one place showing what the whole organization spent, without chasing individual accounts.
127
174
 
128
175
  ## The shared address book
129
176
 
130
177
  A company keeps its own addresses — ship-to sites, a billing address — separate from any individual's. They're labelled, and one of each kind can be the default.
131
178
 
179
+
132
180
  ```typescript Store SDK
133
181
  await client.companies.addresses.create('comp_xxx', {
134
182
  label: 'Northern Warehouse',
@@ -142,12 +190,22 @@ await client.companies.addresses.create('comp_xxx', {
142
190
  })
143
191
  ```
144
192
 
193
+ ```bash cURL
194
+ curl -X POST 'https://api.mystore.com/api/v3/store/companies/comp_xxx/addresses' \
195
+ -H 'X-Spree-API-Key: pk_xxx' \
196
+ -H 'Authorization: Bearer $CUSTOMER_JWT' \
197
+ -H 'Content-Type: application/json' \
198
+ -d '{ "label": "Northern Warehouse", "first_name": "Goods", "last_name": "Inwards" }'
199
+ ```
200
+
201
+
145
202
  Note that a delivery site is just an address. Ten warehouses do not mean ten company nodes — nodes exist for organizational and legal structure, addresses for where things go.
146
203
 
147
204
  ## Tax
148
205
 
149
206
  Tax registrations and exemption certificates live **only on legal entities**, and a purchase resolves tax through the node's nearest `company` ancestor.
150
207
 
208
+
151
209
  ```typescript Admin SDK
152
210
  await adminClient.companies.taxIdentifiers.create('comp_xxx', {
153
211
  kind: 'eu_vat',
@@ -158,6 +216,14 @@ await adminClient.companies.taxIdentifiers.create('comp_xxx', {
158
216
  await adminClient.companies.taxIdentifiers.validate('comp_xxx', 'txid_xxx')
159
217
  ```
160
218
 
219
+ ```bash cURL
220
+ curl -X POST 'https://api.mystore.com/api/v3/admin/companies/comp_xxx/tax_identifiers' \
221
+ -H 'X-Spree-API-Key: sk_xxx' \
222
+ -H 'Content-Type: application/json' \
223
+ -d '{ "kind": "eu_vat", "value": "NL123456789B01" }'
224
+ ```
225
+
226
+
161
227
  > **WARNING:** The walk stops at the first `company` node, **whether or not it holds a registration**. A subsidiary with no VAT number of its own therefore has none — it never quietly borrows its parent's, because in most jurisdictions that would be a false declaration.
162
228
 
163
229
  Exemption certificates hang off a legal entity and are scoped to the country or state that issued them. Check whether a certificate is `active` rather than reading its status — a verified certificate stops exempting once it expires, and `active` accounts for that.
@@ -216,6 +282,7 @@ Payment terms and net invoicing, quotes, and a packaged buyer portal are on the
216
282
 
217
283
  ## Managing companies as staff
218
284
 
285
+
219
286
  ```typescript Admin SDK
220
287
  // Roots only
221
288
  const { data: roots } = await adminClient.companies.list({ parent_id_null: 1 })
@@ -230,6 +297,13 @@ const division = await adminClient.companies.create({
230
297
  })
231
298
  ```
232
299
 
300
+ ```bash cURL
301
+ # Roots only
302
+ curl 'https://api.mystore.com/api/v3/admin/companies?q[parent_id_null]=1' \
303
+ -H 'X-Spree-API-Key: sk_xxx'
304
+ ```
305
+
306
+
233
307
  Moving a branch is an update with a new `parent_id`. Deleting a node removes its whole subtree, and is refused outright once any order exists beneath it — history doesn't get deleted to tidy up an org chart.
234
308
 
235
309
  Each row carries `children_count` and `members_count`. `children_count` is the node's direct sub-units. `members_count` is the number of distinct customers holding a membership on the node or on any node below it, so somebody who is a member of both a parent and one of its sub-units is counted once rather than twice.
@@ -10,28 +10,40 @@ Customers interact with your store through the Store API. They can register, log
10
10
  ```mermaid
11
11
  erDiagram
12
12
  Customer ||--o{ Order : "places"
13
- Customer ||--o{ Address : "has many"
13
+ Customer ||--o{ Address : "address book"
14
+ Customer ||--o| Address : "default ship/bill"
14
15
  Customer ||--o{ Wishlist : "has many"
15
16
  Customer ||--o{ StoreCredit : "has many"
16
- Customer ||--o{ PaymentSource : "has many"
17
+ Customer ||--o{ GiftCard : "holds"
18
+ Customer ||--o{ CompanyMembership : "buys for"
19
+ CompanyMembership }o--|| Company : "belongs to"
20
+ Customer }o--o{ CustomerGroup : "segmented by"
17
21
 
18
22
  Customer {
19
- string id
20
23
  string email
21
24
  string first_name
22
25
  string last_name
26
+ string phone
23
27
  }
24
28
 
25
29
  Order {
26
30
  string number
27
- string state
31
+ string payment_status
32
+ string fulfillment_status
28
33
  }
29
34
 
30
35
  Address {
36
+ string label
31
37
  string first_name
32
38
  string last_name
39
+ string company
33
40
  string address1
41
+ string address2
34
42
  string city
43
+ string postal_code
44
+ string state_code
45
+ string country_code
46
+ string phone
35
47
  }
36
48
 
37
49
  Wishlist {
@@ -71,7 +83,7 @@ curl -X POST 'https://api.mystore.com/api/v3/store/customers' \
71
83
 
72
84
  > **NOTE:** The Store API flow above is **customer self-registration** — the customer sets a password and gets a JWT back. To [create a customer record from the back office](../sdk/admin/resources.md) (importers, CRM sync, phone orders), use the [Admin SDK](../sdk/admin/quickstart.md) instead. No password is set, so it's a managed record rather than a sign-up:
73
85
  >
74
- > ```typescript
86
+ > ```typescript Admin SDK
75
87
  import { createAdminClient } from '@spree/admin-sdk'
76
88
 
77
89
  const client = createAdminClient({