@spree/docs 0.1.182 → 0.1.184

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 (34) 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/catalogs.md +140 -0
  6. package/dist/developer/core-concepts/channels.md +0 -4
  7. package/dist/developer/core-concepts/commissions.md +253 -0
  8. package/dist/developer/core-concepts/companies.md +240 -0
  9. package/dist/developer/core-concepts/customers.md +0 -3
  10. package/dist/developer/core-concepts/discounts.md +133 -0
  11. package/dist/developer/core-concepts/events.md +83 -576
  12. package/dist/developer/core-concepts/fees.md +144 -0
  13. package/dist/developer/core-concepts/imports-exports.md +105 -679
  14. package/dist/developer/core-concepts/inventory.md +114 -248
  15. package/dist/developer/core-concepts/markets.md +9 -12
  16. package/dist/developer/core-concepts/media.md +127 -16
  17. package/dist/developer/core-concepts/metafields.md +123 -200
  18. package/dist/developer/core-concepts/order-totals.md +110 -0
  19. package/dist/developer/core-concepts/orders.md +1 -1
  20. package/dist/developer/core-concepts/payments.md +11 -14
  21. package/dist/developer/core-concepts/pricing.md +11 -13
  22. package/dist/developer/core-concepts/products.md +191 -62
  23. package/dist/developer/core-concepts/promotions.md +12 -11
  24. package/dist/developer/core-concepts/search-filtering.md +2 -4
  25. package/dist/developer/core-concepts/sellers.md +210 -0
  26. package/dist/developer/core-concepts/staff-roles.md +56 -23
  27. package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
  28. package/dist/developer/core-concepts/taxes.md +125 -113
  29. package/dist/developer/core-concepts/translations.md +61 -68
  30. package/dist/developer/core-concepts/webhooks.md +25 -59
  31. package/dist/developer/how-to/custom-promotion.md +3 -3
  32. package/package.json +1 -1
  33. package/dist/developer/core-concepts/companies-and-catalogs.md +0 -81
  34. package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
@@ -1,16 +1,88 @@
1
1
  ---
2
2
  title: Media
3
3
  sidebarTitle: "Media"
4
- description: Manage product media in Spree images, hosted and external video, named variants, focal points, and how media is uploaded, resized, and served via the Store API.
4
+ description: The store-wide media libraryuploading once and reusing everywhere, images and video, focal points, and how files are resized and served.
5
5
  ---
6
6
 
7
- import { Since } from '/snippets/since.mdx';
8
-
9
7
  ## Overview
10
8
 
11
- Spree handles uploads, processing, and delivery for product media. Images are automatically converted to WebP format and preprocessed into multiple sizes for optimal performance.
9
+ Spree handles uploading, processing and serving the images and video a store shows. Files are converted to WebP and preprocessed into several sizes, so a listing page isn't downloading full-resolution photographs.
10
+
11
+ Files live in a **store-wide media library**. A file is uploaded once and can then be placed wherever it's needed — on a product, on a category, inside a description — without being uploaded again.
12
+
13
+ ```mermaid
14
+ flowchart LR
15
+ Upload["Upload once"] --> Library["Media library"]
16
+ Library --> P["On a product"]
17
+ Library --> C["On a category"]
18
+ Library --> D["Inside a description"]
19
+ ```
20
+
21
+ ## The media library
22
+
23
+ Every file in a store is in the library, whether or not it's currently placed on anything. That means a file can be uploaded ahead of time, browsed, searched, and reused.
24
+
25
+ ```typescript Admin SDK
26
+ // Browse everything in the store
27
+ const { data: media } = await adminClient.media.list()
28
+
29
+ // Where is this file being used?
30
+ const { data: usage } = await adminClient.media.usage('med_xxx')
31
+ ```
32
+
33
+ | Field | What it tells you |
34
+ |---|---|
35
+ | `attached` | Whether the file is placed on anything yet |
36
+ | `filename`, `content_type`, `byte_size` | What the file actually is |
37
+ | `viewable_type` / `viewable_id` | What it's placed on, if anything |
38
+ | `media_type` | `image`, `video` or `external_video` |
39
+ | `alt` | Alt text, for accessibility and SEO |
40
+ | `position` | Order within a gallery |
41
+
42
+ ### Reuse shares the file, not the record
43
+
44
+ Placing a library file on a product creates a **new media row that shares the same underlying file**:
45
+
46
+ ```typescript Admin SDK
47
+ await adminClient.products.media.create('prod_xxx', {
48
+ source_media_id: 'med_xxx',
49
+ alt: 'Worn over a navy jumper',
50
+ position: 2,
51
+ })
52
+ ```
53
+
54
+ The copy gets its own alt text, its own position, and its own variant links — because the same photograph might be the third image on one product and the hero on another, with different alt text describing what matters in each context. What it doesn't get is a second copy of the bytes.
55
+
56
+ > **NOTE:** This is why there's no single "shared asset" record to manage. Each placement is independently editable, while storage is shared. Detaching a photo from one product has no effect on any other.
57
+
58
+ ### Deleting, safely
12
59
 
13
- ## Product Media
60
+ A file in use can't be deleted by accident:
61
+
62
+ ```typescript Admin SDK
63
+ // Returns 422 with the list of places it's used
64
+ await adminClient.media.delete('med_xxx')
65
+
66
+ // Remove it from everywhere, then delete
67
+ await adminClient.media.delete('med_xxx', { detach: true })
68
+ ```
69
+
70
+ The dashboard shows you the usage list and asks before doing the second one. Check `usage` before offering a delete in your own tooling.
71
+
72
+ ## What can carry media
73
+
74
+ | Owner | Typical use |
75
+ |---|---|
76
+ | **Product** | The main gallery |
77
+ | **Variant** | Images specific to one colour or size |
78
+ | **Category** | A banner or tile for a navigation page |
79
+ | **Collection** | The same, for a merchandising group |
80
+
81
+ Categories and collections also have simple `image` and `square_image` slots. Setting one places the file; clearing it **removes the placement but keeps the file in the library**, so nothing is destroyed by tidying up a page.
82
+
83
+ Sellers and stores keep their branding as plain attachments — a logo isn't merchandising, and it doesn't belong in a library people browse for product photos.
84
+
85
+ ## Product media
14
86
 
15
87
  A media record carries:
16
88
 
@@ -21,9 +93,9 @@ A media record carries:
21
93
  - **Preprocessed named variants** for fast delivery
22
94
  - **`variant_ids`** — which product variants the media represents. An empty array means it represents the product as a whole.
23
95
 
24
- ### Product-level Gallery
96
+ ### Product-level gallery
25
97
 
26
- In Spree 5.5 the **product** is the default owner of media. Before 5.5, every image was pinned to a specific variant (usually the master), and sharing the same image across variants meant re-uploading the file. From 5.5 onward, an image lives on the product, and any subset of variants can reference it through `variant_ids` — without duplicating the underlying file.
98
+ Media belongs to the **product**, and any subset of its variants can reference the same file through `variant_ids` so one photograph can represent three colourways without being uploaded three times.
27
99
 
28
100
  #### Uploading a product-level image
29
101
 
@@ -106,7 +178,7 @@ The Store API's `media` field on a product returns its gallery — product-level
106
178
 
107
179
  This dual rendering means existing storefronts keep working during the upgrade; new uploads attach to the product, and you opt into a [one-shot migration](/v5/developer/upgrades/5.4-to-5.5) to re-home legacy variant-pinned data when convenient.
108
180
 
109
- ### Video
181
+ ### Video
110
182
 
111
183
  A product gallery can hold video as well as images. Spree supports both ways merchants usually have it:
112
184
 
@@ -186,7 +258,7 @@ Where the poster comes from, in order:
186
258
 
187
259
  Spree does not extract a frame from an uploaded video, so give hosted video and Vimeo links a poster if you want them to show a still.
188
260
 
189
- ### Focal Point
261
+ ### Focal Point
190
262
 
191
263
  `focal_point_x` and `focal_point_y` mark the part of an image that must stay in frame when a storefront crops it to a different shape. Both are fractions between 0 and 1, measured from the top left, so `{ x: 0.5, y: 0.5 }` is dead centre — which is also what a storefront should assume when they are null.
192
264
 
@@ -199,7 +271,7 @@ await client.products.media.update('prod_86Rf07xd4z', 'media_k5nR8xLq', {
199
271
 
200
272
  Spree stores the focal point and serves it; the cropping itself is the storefront's decision, since only it knows the shape it needs.
201
273
 
202
- ### Named Variant Sizes
274
+ ### Named Variant Sizes
203
275
 
204
276
  When an image is uploaded, Spree automatically generates optimized versions in the background:
205
277
 
@@ -319,15 +391,54 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=media,vari
319
391
  | `focal_point_x` | number \| null | Horizontal focal point (0.0–1.0) |
320
392
  | `focal_point_y` | number \| null | Vertical focal point (0.0–1.0) |
321
393
  | `external_video_url` | string \| null | External video URL (YouTube/Vimeo) |
322
- | `video_provider` | string \| null | `youtube` or `vimeo`, derived from the link |
323
- | `video_embed_url` | string \| null | Embeddable player URL, derived from the link |
324
- | `video_url` | string \| null | Uploaded video file URL |
325
- | `poster_url` | string \| null | Still frame for a video |
394
+ | `video_provider` | string \| null | `youtube` or `vimeo`, derived from the link |
395
+ | `video_embed_url` | string \| null | Embeddable player URL, derived from the link |
396
+ | `video_url` | string \| null | Uploaded video file URL |
397
+ | `poster_url` | string \| null | Still frame for a video |
326
398
  | `original_url` | string \| null | Full-size image URL (inline disposition) |
327
399
  | `mini_url` ... `xlarge_url` | string \| null | Named variant URLs |
328
- | `download_url` | string \| null | Same blob as `original_url` but with `Content-Disposition: attachment`. Admin API only. |
329
400
 
330
- ## Image Processing
401
+ The Admin API adds the fields the library needs:
402
+
403
+ | Field | Type | Description |
404
+ |---|---|---|
405
+ | `attached` | boolean | Whether the file is placed on anything, or sitting unused in the library |
406
+ | `viewable_type` / `viewable_id` | string \| null | What it's placed on — `product`, `variant`, `category`, `collection` |
407
+ | `filename`, `content_type`, `byte_size` | | What the file actually is |
408
+ | `embed_url` | string \| null | The rendition used inside rich text — sized to fit, not cropped |
409
+ | `signed_id` | string \| null | Lets a plain attachment field adopt this file without re-uploading |
410
+ | `download_url` | string \| null | The same file with `Content-Disposition: attachment` |
411
+
412
+ > **NOTE:** Two behaviours to expect when rendering a gallery:
413
+ >
414
+ > **`original_url` is null for any playable video** — there's no still image to size. Use `poster_url`.
415
+ >
416
+ > **A video's sized URLs come from its poster.** If a video has a poster, every `*_url` resolves from that frame, so a grid of mixed images and video renders uniformly.
417
+
418
+ ## Images in descriptions
419
+
420
+ Rich text fields — a product description, a category's copy — can carry embedded images. In the dashboard, the editor opens the media library so a merchant picks an existing file or uploads a new one.
421
+
422
+ Embedded images use their own rendition, sized to fit rather than cropped to a square. A size chart or a diagram keeps its proportions, where a gallery thumbnail would have had its edges cut off.
423
+
424
+ > **WARNING:** An embedded image is a plain image URL in the HTML — nothing records which description uses which file.
425
+ >
426
+ > Spree's usage check does its best by searching descriptions for the file, but it can't be exhaustive. Deleting a library file can leave a broken image in a description, so treat the usage list as a warning rather than proof a file is unused.
427
+
428
+ ## Who can manage media
429
+
430
+ Media is its own permission, separate from products:
431
+
432
+ | Action | Requires |
433
+ |---|---|
434
+ | A product's own gallery — add, reorder, remove | Permission to edit **products** |
435
+ | Browsing the library, checking usage, deleting a file outright | **Media** permission |
436
+
437
+ The reasoning: reaching a file through a product you can already edit isn't the same as enumerating every file in the store. Someone who manages one product's photos shouldn't automatically be able to browse — or delete — everything the business has ever uploaded.
438
+
439
+ Note the asymmetry that follows. Removing an image from a product removes the *placement* and needs only product permission; deleting the file everywhere is a library action and needs the media key.
440
+
441
+ ## Image processing
331
442
 
332
443
  Spree uses [libvips](https://www.libvips.org/) for image processing. Images are automatically:
333
444
 
@@ -1,148 +1,125 @@
1
1
  ---
2
- title: Metafields
3
- description: Add custom structured data to products, orders, and other resources with type-safe metafields
2
+ title: Custom Fields
3
+ description: Add your own structured, typed data to products, orders and other records searchable, filterable, and editable in the dashboard.
4
4
  ---
5
5
 
6
- import { Since } from '/snippets/since.mdx';
7
-
8
-
9
6
  ## Overview
10
7
 
11
- Metafields provide a flexible, type-safe system for adding custom structured attributes to Spree models. Unlike [metadata](../customization/metadata.md) which is simple JSON storage, metafields are schema-defined with strong typing, validation, and visibility controls.
8
+ Sooner or later you need to store something Spree doesn't have a field for. A fabric composition. A care instruction. A gift message. An ID from the system you sync with.
9
+
10
+ Custom fields are how you do that without changing the database. You declare a field once — its name, its type, who can see it — and from then on it can be set on any record of that kind, edited in the dashboard, returned by the API, and searched or filtered like any built-in field.
12
11
 
13
- Use metafields for:
12
+ > **NOTE:** Custom fields were called **metafields** before Spree 6.0. The API and SDK have always called them custom fields; now the rest of Spree does too.
14
13
 
15
- - Product specifications (manufacturer, material, dimensions)
16
- - Custom business logic fields
17
- - Integration data from external systems
18
- - Order-specific custom attributes
14
+ ## Definitions and values
19
15
 
20
- ## Architecture
16
+ There are two halves, and it helps to keep them straight:
21
17
 
22
18
  ```mermaid
23
19
  erDiagram
24
- MetafieldDefinition ||--o{ Metafield : "defines schema for"
25
- Product ||--o{ Metafield : "has many"
26
- Variant ||--o{ Metafield : "has many"
27
- Order ||--o{ Metafield : "has many"
20
+ CustomFieldDefinition ||--o{ CustomField : "gives shape to"
21
+ Product ||--o{ CustomField : "has values"
22
+ Order ||--o{ CustomField : "has values"
23
+ Customer ||--o{ CustomField : "has values"
28
24
 
29
- MetafieldDefinition {
25
+ CustomFieldDefinition {
30
26
  string namespace
31
27
  string key
32
- string name
33
- string metafield_type
28
+ string label
29
+ string field_type
34
30
  string resource_type
35
- string display_on
31
+ boolean storefront_visible
36
32
  boolean searchable
37
33
  boolean sortable
38
34
  }
39
35
 
40
- Metafield {
41
- string type
42
- text value
43
- string resource_type
36
+ CustomField {
37
+ string key
38
+ string label
39
+ string field_type
40
+ any value
44
41
  }
45
42
  ```
46
43
 
47
- ```mermaid
48
- flowchart LR
49
- subgraph Definition
50
- A[MetafieldDefinition] --> B["namespace: properties"]
51
- A --> C["key: manufacturer"]
52
- A --> D["type: ShortText"]
53
- A --> E["resource_type: Product"]
54
- A --> F["display_on: both"]
55
- end
56
-
57
- subgraph Instance
58
- G[Metafield] --> H["value: Wilson"]
59
- G --> I["resource: Product #1"]
60
- end
61
-
62
- A -.->|"defines schema"| G
63
- ```
64
-
65
- - **MetafieldDefinition** — the blueprint that defines the data type, target resource, and visibility
66
- - **Metafield** — stores the actual value for a specific resource instance
67
-
68
- ## Data Types
44
+ - A **definition** is the declaration: "products have a Material, it is short text, shoppers may see it." You create it once.
45
+ - A **custom field** is one record's answer: this product's material is `100% Cotton`.
69
46
 
70
- | Type | Description | Example Values |
71
- |------|-------------|----------------|
72
- | Short Text | Brief text fields | SKU codes, brand names, tags |
73
- | Long Text | Longer text content | Care instructions, notes |
74
- | Rich Text | Formatted HTML content | Product descriptions with formatting |
75
- | Number | Numeric values | Weight, quantity, ratings |
76
- | Boolean | True/false flags | Is featured, requires signature |
77
- | JSON | Structured data | Configuration, complex objects |
47
+ Because the definition carries the type and the label, the dashboard can build an editing form for it automatically, and your storefront gets a value it can trust the shape of.
78
48
 
79
- ## Visibility Control
49
+ ## Field types
80
50
 
81
- Metafields support two visibility levels via the `display_on` attribute:
51
+ | Type | For | Example |
52
+ |---|---|---|
53
+ | `short_text` | A word or a line | `Wilson`, `100% Cotton` |
54
+ | `long_text` | A paragraph | Care instructions |
55
+ | `rich_text` | Formatted HTML | A spec sheet with lists and bold |
56
+ | `number` | A quantity or measure | `3.5`, `120` |
57
+ | `boolean` | A yes or no | Requires a signature |
58
+ | `json` | Structured data | A sizing chart |
82
59
 
83
- | Visibility | Store API | Admin API | Use Case |
84
- |------------|:---------:|:---------:|----------|
85
- | `both` | Yes | Yes | Public product specifications |
86
- | `back_end` | No | Yes | Internal notes, integration IDs |
60
+ ## Who can see it
87
61
 
88
- ## Search, sort & filter
62
+ `storefront_visible` decides whether a field ever leaves the back office:
89
63
 
90
- Product metafield definitions can opt into SearchProvider participation:
64
+ | `storefront_visible` | Store API | Admin API | Use it for |
65
+ |---|:---:|:---:|---|
66
+ | `true` | Yes | Yes | Anything a shopper should see — materials, specs |
67
+ | `false` | No | Yes | Internal notes, supplier codes, sync IDs |
91
68
 
92
- | Flag | Effect | Allowed field types |
93
- |------|--------|---------------------|
94
- | `searchable` | Values are included in product text search | `short_text`, `long_text`, `number` |
95
- | `sortable` | Listings can sort with `sort=cf_{namespace}_{key}` (prefix `-` for descending), e.g. `cf_custom_label` | `short_text`, `number` |
69
+ > **WARNING:** `storefront_visible: false` genuinely withholds the field from the Store API — it is not hidden in the response, it is absent. Treat it as the boundary between what a customer may read and what only staff may.
96
70
 
97
- Both default to `false`. Set them on the definition (Admin UI, Admin API, or seeds). For an existing catalog with Meilisearch, run `rake spree:search:reindex` after toggling flags so index settings and product documents pick up new `cf_*` fields.
71
+ ## Declaring a field
98
72
 
99
- ### Filtering
100
73
 
101
- Any definition with either flag set also becomes filterable on product listings, using its `cf_*` key as a Ransack predicate. The Store and Admin APIs accept the same keys and the same `sort` values:
102
-
103
- ```
104
- GET /api/v3/store/products?q[cf_custom_material_i_cont]=wool
105
- GET /api/v3/admin/products?q[cf_custom_weight_gteq]=3.5
106
- GET /api/v3/store/products?sort=-cf_custom_weight
74
+ ```typescript Admin SDK
75
+ const definition = await adminClient.customFieldDefinitions.create({
76
+ resource_type: 'Spree::Product',
77
+ namespace: 'properties',
78
+ key: 'material',
79
+ label: 'Material',
80
+ field_type: 'short_text',
81
+ storefront_visible: true,
82
+ searchable: true,
83
+ })
107
84
  ```
108
85
 
109
- | Field type | Predicates |
110
- |------------|------------|
111
- | `short_text`, `long_text` | `i_cont`, `cont`, `eq`, `not_eq`, `start`, `end`, `present`, `blank` |
112
- | `number` | `eq`, `gt`, `gteq`, `lt`, `lteq`, `present`, `blank` |
113
-
114
- Use `i_cont` for case-insensitive matching — plain `cont` is case-sensitive on PostgreSQL.
115
-
116
- Predicates that don't match the definition's field type are ignored rather than rejected, as are keys matching no definition. Meilisearch supports the equality, range, and `present`/`blank` predicates; substring predicates (`i_cont`, `cont`, `start`, `end`) apply only to the database provider.
117
-
118
- CSV exports honor the same predicates, so exporting a filtered product list matches what the admin sees.
119
-
120
- In the admin dashboard, these fields appear in the products table's column selector, Sort dropdown, and filter panel — hidden by default, so admins opt in per column.
121
-
122
- ## Supported Resources
86
+ ```bash CLI
87
+ spree api post /custom_field_definitions -d '{
88
+ "resource_type": "Spree::Product",
89
+ "namespace": "properties",
90
+ "key": "material",
91
+ "label": "Material",
92
+ "field_type": "short_text",
93
+ "storefront_visible": true,
94
+ "searchable": true
95
+ }'
96
+ ```
123
97
 
124
- Metafields can be attached to most Spree resources including Products, Variants, Orders, Line Items, Taxons, Payments, Shipments, Gift Cards, Store Credits, and more.
125
98
 
126
- > **INFO:** Custom resources can also support metafields. See the [Customization Quickstart](../customization/quickstart.md) for details.
99
+ `namespace` keeps groups of fields apart, so an integration's `product_id` never collides with yours. Together they form the key you'll see in responses: `properties.material`. Both are normalized to snake_case.
127
100
 
128
- ## Namespaces
101
+ Definitions can also be managed in the dashboard under **Settings → Custom Fields**, which is where merchants usually add them.
129
102
 
130
- Namespaces organize metafields into logical groups and prevent key conflicts:
103
+ ## Setting and reading values
131
104
 
132
- | Namespace | Example Keys | Purpose |
133
- |-----------|-------------|---------|
134
- | `properties` | `manufacturer`, `material`, `fit` | Product specifications |
135
- | `shopify` | `product_id`, `variant_id` | Integration data |
136
- | `flags` | `featured`, `requires_approval` | Feature flags |
137
- | `custom` | `gift_message`, `delivery_notes` | Business-specific fields |
138
105
 
139
- > **INFO:** Namespace and key are automatically normalized to snake_case.
106
+ ```typescript Admin SDK
107
+ // Set a value on a product
108
+ await adminClient.products.customFields.create('prod_xxx', {
109
+ custom_field_definition_id: definition.id,
110
+ value: '100% Cotton',
111
+ })
112
+ ```
140
113
 
141
- ## Store API
114
+ ```bash CLI
115
+ spree api post /products/prod_xxx/custom_fields -d '{
116
+ "custom_field_definition_id": "cfdef_xxx",
117
+ "value": "100% Cotton"
118
+ }'
119
+ ```
142
120
 
143
- Metafields with `display_on` set to `both` are included in Store API responses when you [request the `custom_fields` expand](../../api-reference/store-api/relations.md):
144
121
 
145
- > **INFO:** Metafields in API are called Custom Fields as we plan to rename Metafields to Custom Fields completely in Spree 6.0.
122
+ Reading them from a storefront is an expand on whatever you already fetch:
146
123
 
147
124
 
148
125
  ```typescript Store SDK
@@ -150,44 +127,29 @@ const product = await client.products.get('spree-tote', {
150
127
  expand: ['custom_fields'],
151
128
  })
152
129
 
153
- product.custom_fields?.forEach(field => {
154
- console.log(field.key) // "properties.manufacturer"
155
- console.log(field.label) // "Manufacturer"
156
- console.log(field.value) // "Wilson"
157
- console.log(field.field_type) // "short_text"
158
- })
159
- ```
160
-
161
- ```typescript Admin SDK
162
- const product = await adminClient.products.get('prod_86Rf07xd4z', {
163
- expand: ['custom_fields'],
130
+ product.custom_fields?.forEach((field) => {
131
+ field.key // "properties.material"
132
+ field.label // "Material"
133
+ field.value // "100% Cotton"
134
+ field.field_type // "short_text"
164
135
  })
165
136
  ```
166
137
 
167
138
  ```bash cURL
168
139
  curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=custom_fields' \
169
- -H 'X-Spree-Api-Key: pk_xxx'
140
+ -H 'X-Spree-API-Key: pk_xxx'
170
141
  ```
171
142
 
172
143
 
173
- **Response:**
174
-
175
- ```json
144
+ ```json Response
176
145
  {
177
146
  "id": "prod_86Rf07xd4z",
178
- "name": "Spree T-Shirt",
147
+ "name": "Spree Tote",
179
148
  "custom_fields": [
180
149
  {
181
150
  "id": "cf_k5nR8xLq",
182
- "label": "Manufacturer",
183
- "key": "properties.manufacturer",
184
- "field_type": "short_text",
185
- "value": "Wilson"
186
- },
187
- {
188
- "id": "cf_m3Rp9wXz",
189
- "label": "Material",
190
151
  "key": "properties.material",
152
+ "label": "Material",
191
153
  "field_type": "short_text",
192
154
  "value": "100% Cotton"
193
155
  }
@@ -195,96 +157,57 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=custom_fie
195
157
  }
196
158
  ```
197
159
 
198
- > **NOTE:** The `display_on` attribute is intentionally excluded from Store API responses for security.
199
-
200
- ## Admin Management
201
-
202
- ### Managing Definitions
160
+ Each value arrives with its own label and type, so you can render a spec table straight from the array without hardcoding which fields exist.
203
161
 
204
- Navigate to **Settings → Metafield Definitions** in the Admin Panel to create and manage metafield definitions. Select the resource type, enter namespace and key, choose the data type, and set visibility.
162
+ ## Searching, sorting and filtering
205
163
 
206
- Definitions are also [managed via the Admin API](../../api-reference/admin-api/endpoints.md). `storefront_visible: true` is equivalent to `display_on: both` it exposes the field to the Store API:
164
+ A custom field can take part in product listings but only if you ask, because indexing everything by default would be wasteful.
207
165
 
166
+ | Flag | What it enables | Works with |
167
+ |---|---|---|
168
+ | `searchable` | The value is matched by product text search | `short_text`, `long_text`, `number` |
169
+ | `sortable` | Listings can be ordered by it | `short_text`, `number` |
208
170
 
209
- ```typescript Admin SDK
210
- import { createAdminClient } from '@spree/admin-sdk'
211
-
212
- const client = createAdminClient({
213
- baseUrl: 'https://store.example.com',
214
- secretKey: 'sk_xxx',
215
- })
216
-
217
- const definition = await client.customFieldDefinitions.create({
218
- resource_type: 'Spree::Product',
219
- namespace: 'properties',
220
- key: 'manufacturer',
221
- label: 'Manufacturer',
222
- field_type: 'short_text',
223
- storefront_visible: true,
224
- })
171
+ Setting either flag also makes the field **filterable**. Filter and sort with the field's `cf_` key, on both APIs:
225
172
 
226
- await client.customFieldDefinitions.update(definition.id, { storefront_visible: false })
227
- await client.customFieldDefinitions.delete(definition.id)
228
173
  ```
229
-
230
- ```bash CLI
231
- spree api post /custom_field_definitions -d '{
232
- "resource_type": "Spree::Product",
233
- "namespace": "properties",
234
- "key": "manufacturer",
235
- "label": "Manufacturer",
236
- "field_type": "short_text",
237
- "storefront_visible": true
238
- }'
174
+ GET /api/v3/store/products?q[cf_properties_material_i_cont]=wool
175
+ GET /api/v3/store/products?sort=-cf_properties_weight
176
+ GET /api/v3/admin/products?q[cf_properties_weight_gteq]=3.5
239
177
  ```
240
178
 
179
+ | Field type | Comparisons available |
180
+ |---|---|
181
+ | `short_text`, `long_text` | `i_cont`, `cont`, `eq`, `not_eq`, `start`, `end`, `present`, `blank` |
182
+ | `number` | `eq`, `gt`, `gteq`, `lt`, `lteq`, `present`, `blank` |
241
183
 
242
- ### Managing Values
243
-
244
- When editing a resource (e.g., a product), metafields appear in a dedicated section. The admin panel automatically builds forms for all defined metafields.
245
-
246
- To set a value programmatically, use the resource's nested `customFields` accessor (parent ID first):
247
-
248
-
249
- ```typescript Admin SDK
250
- await client.products.customFields.create('prod_xxx', {
251
- custom_field_definition_id: 'cfdef_xxx',
252
- value: 'Wilson',
253
- })
254
- ```
184
+ Use `i_cont` for case-insensitive matching. A comparison that doesn't suit the field's type is ignored rather than rejected, so a stale filter in a saved view can't break a page.
255
185
 
256
- ```bash CLI
257
- spree api post /products/prod_xxx/custom_fields -d '{
258
- "custom_field_definition_id": "cfdef_xxx",
259
- "value": "Wilson"
260
- }'
261
- ```
186
+ > **INFO:** If you use Meilisearch, re-index after changing these flags so the new fields are picked up. Substring matching (`i_cont`, `cont`, `start`, `end`) is a database-provider feature; Meilisearch handles equality, ranges and presence. See [Search & Filtering](search-filtering.md).
262
187
 
188
+ In the dashboard, fields with these flags become available in the product table's column picker, sort menu and filter panel — off by default, so merchants opt in per column.
263
189
 
264
- ## Metafields vs Metadata
190
+ ## What can carry custom fields
265
191
 
266
- Spree has two permanent, complementary systems for custom data **metadata for machines, metafields for humans**. They serve different purposes and are not interchangeable. Neither is going away.
192
+ Most things you'd want to annotate: products, variants, orders, line items, customers, categories, payments, fulfillments, gift cards and store credits, among others.
267
193
 
268
- | Feature | Metafields | [Metadata](../customization/metadata.md) |
269
- |---------|-----------|----------|
270
- | **Purpose** | Merchant-defined structured attributes | Developer escape hatch — integration IDs, sync state |
271
- | **Schema** | Defined via MetafieldDefinitions | Schemaless JSON — no definition required |
272
- | **Validation** | Type-specific (text, number, boolean, etc.) | None — accepts any JSON-serializable data |
273
- | **Visibility** | Configurable (admin-only or public) | Write-only in Store API, readable in Admin API |
274
- | **Admin UI** | Dedicated management forms | JSON preview |
275
- | **Data Types** | 6 specific types | Any JSON value |
276
- | **Organization** | Namespaced (`namespace.key`) | Flat key-value structure |
277
- | **Queryable** | Via SQL joins, Ransack scopes, search providers | Via JSONB operators (PostgreSQL) |
194
+ ## Custom fields or metadata?
278
195
 
279
- **Use Metafields** when you need type validation, visibility control, admin UI forms, or organized namespacing.
196
+ Spree has two ways to store your own data, and they are not competing — they solve different problems.
280
197
 
281
- **Use [Metadata](../customization/metadata.md)** for external system IDs, tracking attribution, syncing with integrations, or simple write-and-forget data that only backend systems need to read.
198
+ | | Custom fields | [Metadata](../customization/metadata.md) |
199
+ |---|---|---|
200
+ | **For** | Data a merchant curates | Data your code keeps |
201
+ | **Shape** | Declared and typed | Any JSON, no declaration |
202
+ | **Dashboard** | Proper edit forms | Raw JSON |
203
+ | **Visibility** | Configurable per field | Never read by the Store API |
204
+ | **Searchable** | Yes, opt in | Not through the product search |
282
205
 
283
- > **WARNING:** Product Properties are deprecated and will be removed in Spree 6.0. For new projects, always use Metafields. For existing projects, plan to migrate using the [migration guide](/v5/developer/upgrades/5.1-to-5.2#migrate-to-metafields-or-keep-using-product-properties).
206
+ Put it simply: **custom fields are for people, metadata is for machines.** If a merchant should type it, define a custom field. If it is a sync token or an external ID that only your integration reads, use metadata.
284
207
 
285
- ## Related Documentation
208
+ ## Related
286
209
 
287
- - [Metadata](../customization/metadata.md) — Simple key-value metadata
288
- - [Products](products.md) — Product catalog
289
- - [Events](events.md) — Subscribe to metafield events
290
- - [Admin SDK](../sdk/admin/resources.md) — Manage definitions and values from TypeScript
210
+ - [Metadata](../customization/metadata.md) — the machine-readable alternative
211
+ - [Products](products.md) — the most common place for custom fields
212
+ - [Search & Filtering](search-filtering.md) — how filters and search work
213
+ - [Admin SDK](../sdk/admin/resources.md) — managing definitions and values in TypeScript