← Files WixARCHIVED FILE
skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-discount-rule.md
19.4 KB · Oct 8, 2026 · 12:02 UTC
---
name: "Pricing: Create Discount Rule"
description: Configures automatic discount rules using the eCommerce Discount Rules API. Covers percentage and fixed-amount discounts, scope targeting (catalog-wide, specific collections, or individual products), scheduling active periods, and the find-by-name + update pattern.
layer: config
---
# Setup Discount Rules
## Prerequisites
- Wix Stores (or another eCommerce business solution) installed on the site
- At least one product in the catalog
## Required APIs
- [Create Discount Rule](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/create-discount-rule)
- [Get Discount Rule](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/get-discount-rule)
- [Query Discount Rules](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/query-discount-rules)
- [Update Discount Rule](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/update-discount-rule)
- [Delete Discount Rule](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/delete-discount-rule)
---
## Critical: `discounts` structure
**The API always returns the full normalized structure**, `values` wrapper included. Never assume a simplified form. Each entry of `discounts.values` looks like:
```json
{
"targetType": "SPECIFIC_ITEMS",
"specificItemsInfo": {
"scopes": [
{
"id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CATALOG_ITEM",
"catalogItemFilter": {
"catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
}
}
]
},
"discountType": "PERCENTAGE",
"percentage": 20
}
```
When updating a rule, always reuse the `discounts.values` entries as returned from the query/get, modifying only the specific fields you need. **Do not reconstruct from scratch unless creating a new rule.**
---
## Step 1: Query existing discount rules
**Endpoint**: `POST https://www.wixapis.com/ecom/v1/discount-rules/query`
**Request** — list all rules:
```json
{
"query": {
"cursorPaging": {
"limit": 100
}
}
}
```
**Request** — find by name (exact match):
```json
{
"query": {
"filter": {
"name": { "$eq": "Summer Sale" }
},
"cursorPaging": { "limit": 10 }
}
}
```
Filterable fields: `id`, `name`, `active`, `revision`, `created_date`, `updated_date`, `active_time_info.start`, `active_time_info.end`
**Response**:
```json
{
"discountRules": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"revision": "1",
"name": "Summer Sale 10%",
"active": true,
"activeTimeInfo": {
"start": "2026-06-01T00:00:00.000Z",
"end": "2026-08-31T23:59:59.000Z"
},
"discounts": { "values": [
{
"targetType": "SPECIFIC_ITEMS",
"specificItemsInfo": {
"scopes": [
{
"id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CATALOG_ITEM",
"catalogItemFilter": {
"catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
}
}
]
},
"discountType": "PERCENTAGE",
"percentage": 10
}
] }
}
],
"pagingMetadata": {
"cursors": {},
"hasNext": false
}
}
```
Note existing rules and their scopes to avoid stacking conflicts.
---
## Guardrails — run before creating or updating any discount
These checks guard every discount creation (rule **or** coupon). Run them before the create/update calls below. **User input overrides all caps** — if the merchant explicitly asks for a value beyond a threshold, honor it and document the override in your reasoning.
**Conflict / stacking** (uses the Step 1 query of active rules; for coupons, also query active coupons):
- **Same-scope overlap** — a new rule conflicts with an existing active rule when scopes overlap (both `CATALOG`; `CATALOG` vs `COLLECTION`; same `COLLECTION`/`SPECIFIC_PRODUCTS` id). Wix stacks automatic rules → warn and offer to deactivate the existing one.
- **Time overlap** — overlapping `activeTimeInfo` on the same scope (`existingStart < newEnd AND existingEnd > newStart`) → warn about the overlap window.
- **Cross-mechanism (automatic + coupon)** — automatic discounts and coupons both apply at checkout. If creating a rule, query active coupons on overlapping scope (and vice-versa); warn with the combined effective discount. (Only one coupon per checkout, but automatic rules are unlimited.)
**Margin / plausibility:**
- **Global cap** — default max discount **25%**; flag anything higher (proceed only on explicit request).
- **Minimum margin** — default floor **15%**; if cost data is available, `effective_margin = (price − cost − discount_amount) / price × 100` must stay ≥ 15% (block + explain otherwise).
- **% plausibility** — > 50% warn (show a $100 → $(100−pct) example); = 100% block unless confirmed ("makes the product free"); > 100% always block.
- **Stacking margin** — when a new discount overlaps existing active ones, evaluate the *combined* discount against the cap and margin floor, not just the new one in isolation.
| Scenario | Action |
|---|---|
| Discount ≤ 25% and margin ≥ 15%, no scope overlap | Proceed |
| Scope/time/cross-mechanism overlap | Warn; offer to deactivate existing or confirm stacking |
| Discount 26–50% (no override) | Warn, ask to confirm |
| Discount > 50% | Warn with $ example, confirm |
| Discount = 100% | Block unless confirmed |
| Discount > 100% | Block always |
| Combined/stacked discount exceeds cap or margin floor | Warn about cumulative effect |
| Any threshold, explicit user override | Proceed, document the override in reasoning |
When a discount *isn't* applying as expected, see [Troubleshoot: Discount Not Applying](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/skills/pricing-discount-not-applying).
---
## Step 2: Create a percentage discount rule
**Endpoint**: `POST https://www.wixapis.com/ecom/v1/discount-rules`
**Request** — 20% off all products:
```json
{
"discountRule": {
"name": "Flash Sale 20% Off",
"active": true,
"activeTimeInfo": {
"start": "2026-05-01T00:00:00.000Z",
"end": "2026-05-03T23:59:59.000Z"
},
"discounts": { "values": [
{
"targetType": "SPECIFIC_ITEMS",
"specificItemsInfo": {
"scopes": [
{
"id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CATALOG_ITEM",
"catalogItemFilter": {
"catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
}
}
]
},
"discountType": "PERCENTAGE",
"percentage": 20
}
] }
}
}
```
**Request** — 15% off a specific collection:
```json
{
"discountRule": {
"name": "Summer Collection Sale",
"active": true,
"discounts": { "values": [
{
"targetType": "SPECIFIC_ITEMS",
"specificItemsInfo": {
"scopes": [
{
"id": "collections_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CUSTOM_FILTER",
"customFilter": {
"appId": "215238eb-22a5-4c36-9e7b-e7c08025e04e",
"params": {
"collectionIds": ["collection-uuid-here"]
}
}
}
]
},
"discountType": "PERCENTAGE",
"percentage": 15
}
] }
}
}
```
---
## Step 3: Create a fixed-amount discount rule
**Request** — $5 off specific products:
```json
{
"discountRule": {
"name": "$5 Off Selected Items",
"active": true,
"discounts": { "values": [
{
"targetType": "SPECIFIC_ITEMS",
"specificItemsInfo": {
"scopes": [
{
"id": "specific_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CATALOG_ITEM",
"catalogItemFilter": {
"catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e",
"catalogItemIds": ["product-uuid-here"]
}
}
]
},
"discountType": "FIXED_AMOUNT",
"fixedAmount": "5.00"
}
] }
}
}
```
---
## Step 4: Update a discount rule
Always fetch the rule first (via Get or Query), then modify only the fields you need. The `mask` field tells the API which fields to update — omit it to replace all writable fields.
**Endpoint**: `PATCH https://www.wixapis.com/ecom/v1/discount-rules/{discountRule.id}`
**Required**: `discountRule.id`, `discountRule.revision` (must match current revision)
**Request** — change percentage on an existing rule (full discounts replacement):
```json
{
"discountRule": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"revision": "1",
"discounts": { "values": [
{
"targetType": "SPECIFIC_ITEMS",
"specificItemsInfo": {
"scopes": [
{
"id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CATALOG_ITEM",
"catalogItemFilter": {
"catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
}
}
]
},
"discountType": "PERCENTAGE",
"percentage": 25
}
] }
},
"mask": { "paths": ["discounts"] }
}
```
**Request** — change only `active` status (field mask for partial update):
```json
{
"discountRule": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"revision": "2",
"active": false
},
"mask": { "paths": ["active"] }
}
```
---
## Step 5: Find by name and update (complete pattern)
The safe pattern for "find a rule by name and update its percentage":
1. Query with name filter → get the rule's `id`, `revision`, and `discounts.values` entries
2. Modify only `percentage` on each entry, keeping all other fields intact
3. PATCH with the modified `discounts` and `mask: { paths: ["discounts"] }`
**Step 5a — Query by name**:
```json
POST https://www.wixapis.com/ecom/v1/discount-rules/query
{
"query": {
"filter": { "name": { "$eq": "My Rule Name" } },
"cursorPaging": { "limit": 1 }
}
}
```
Extract from response: `discountRules[0].id`, `discountRules[0].revision`, `discountRules[0].discounts`
**Step 5b — Update percentage** (modify the returned discounts in-place):
Take the `discounts.values` entries from the query response and update only `percentage` on each entry:
```json
PATCH https://www.wixapis.com/ecom/v1/discount-rules/{id}
{
"discountRule": {
"id": "<id from query>",
"revision": "<revision from query>",
"discounts": { "values": [
{
"targetType": "SPECIFIC_ITEMS",
"specificItemsInfo": { "<scopes unchanged from query response>" },
"discountType": "PERCENTAGE",
"percentage": 10
}
] }
},
"mask": { "paths": ["discounts"] }
}
```
> **Important**: Copy `targetType`, `specificItemsInfo.scopes` verbatim from the query response — do not reconstruct them. Only change `discountType` and `percentage`/`fixedAmount`, keeping them at the entry root.
---
## Step 6: Deactivate or delete a discount rule
To deactivate without deleting:
```json
{
"discountRule": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"revision": "2",
"active": false
},
"mask": { "paths": ["active"] }
}
```
To delete permanently:
**Endpoint**: `DELETE https://www.wixapis.com/ecom/v1/discount-rules/{discountRuleId}`
---
## Key field rules
| Field | Required | Notes |
|---|---|---|
| `name` | Yes | Internal name for the rule. Filterable in query. |
| `active` | Yes | Whether the rule is currently applied |
| `activeTimeInfo.start` | No | ISO 8601 start time. Omit for immediate activation |
| `activeTimeInfo.end` | No | ISO 8601 end time. Omit for no expiration |
| `discounts.values[].targetType` | Yes | Always `"SPECIFIC_ITEMS"` for standard rules |
| `discounts.values[].specificItemsInfo.scopes[]` | Yes | Array of scope objects — see Scope types below |
| `discounts.values[].discountType` | Yes | `"PERCENTAGE"` or `"FIXED_AMOUNT"` or `"FIXED_PRICE"` — at the entry root, not inside a `discount` object |
| `discounts.values[].percentage` | If PERCENTAGE | Number 0.1-100 |
| `discounts.values[].fixedAmount` | If FIXED_AMOUNT | Decimal string (e.g., `"5.00"`) |
| `revision` | On update/delete | Must match current value — fetch first |
| `mask.paths[]` | On update | Recommended — list fields being changed (e.g., `["discounts"]`, `["active"]`) |
## Scope types
| Scope | `type` | `id` prefix | When to use |
|---|---|---|---|
| All products | `"CATALOG_ITEM"` | `all_<appId>` | `catalogItemFilter.catalogAppId` only, no `catalogItemIds` |
| Specific products | `"CATALOG_ITEM"` | `specific_<appId>` | `catalogItemFilter.catalogAppId` + `catalogItemFilter.catalogItemIds` |
| Collection | `"CUSTOM_FILTER"` | `collections_<appId>` | `customFilter.appId` + `customFilter.params.collectionIds` |
**Store catalog app ID** (required in all scopes): `215238eb-22a5-4c36-9e7b-e7c08025e04e`
## Recommendation → API Mapping
When creating a discount rule from a recommendation output, use this mapping to convert the recommendation's simplified JSON into the actual Discount Rules API payload.
### Constants
- **Store catalog app ID**: `215238eb-22a5-4c36-9e7b-e7c08025e04e` — used in all scope constructions below.
- **Initial state**: Recommendations create rules as `active: false` with `status: "PENDING"`. The merchant must approve before the rule goes live.
### Scope mapping
The recommendation's `scope` field maps to the API's internal scope structure. The scope ID uses a prefix convention:
| Recommendation scope | API scope type | Scope ID prefix | How to build |
|---|---|---|---|
| `SITE` | `CATALOG_ITEM` | `all_` | Set `catalogItemFilter.catalogAppId` to the store catalog app ID. No item IDs. |
| `ITEMS` | `CATALOG_ITEM` | `specific_` | Set `catalogItemFilter.catalogAppId` + `catalogItemFilter.catalogItemIds` to the product UUIDs from `productIds`. |
| `CATEGORY` | `CUSTOM_FILTER` | `collections_` | Set `customFilter.appId` to the store catalog app ID + `customFilter.params.collectionIds` to the category UUIDs from `categoryIds`. |
**Example — SITE scope**:
```json
{
"scope": {
"id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CATALOG_ITEM",
"catalogItemFilter": {
"catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
}
}
}
```
**Example — ITEMS scope** (with product IDs):
```json
{
"scope": {
"id": "specific_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CATALOG_ITEM",
"catalogItemFilter": {
"catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e",
"catalogItemIds": ["product-uuid-1", "product-uuid-2"]
}
}
}
```
**Example — CATEGORY scope** (with collection IDs):
```json
{
"scope": {
"id": "collections_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CUSTOM_FILTER",
"customFilter": {
"appId": "215238eb-22a5-4c36-9e7b-e7c08025e04e",
"params": {
"collectionIds": ["collection-uuid-1"]
}
}
}
}
```
### Discount type mapping
| Recommendation `discountType` | API field to set | Value format |
|---|---|---|
| `PERCENTAGE` | `discounts.values[].percentage` | Integer (e.g., `15`) |
| `FIXED_AMOUNT` | `discounts.values[].fixedAmount` | String (e.g., `"5.00"`) |
| `FIXED_PRICE` | `discounts.values[].fixedPrice` | String (e.g., `"29.99"`) |
All discount entries use `targetType: "SPECIFIC_ITEMS"` with the scope wrapped in `specificItemsInfo.scopes[]`.
### Trigger mapping (conditions)
Triggers determine WHEN the discount activates. They are built from the recommendation's `conditions` fields. **If no conditions exist (both minSubTotal and minItemQuantity are 0), do NOT set a trigger — the discount applies unconditionally.**
| Condition | Trigger type | How to build |
|---|---|---|
| `minItemQuantity > 0` only | `ITEM_QUANTITY_RANGE` | Set `itemQuantityRange.from` to the value. No upper bound. Include the same scope as the discount target. |
| `minSubTotal > 0` only | `SUBTOTAL_RANGE` | Set `subtotalRange.from` to the value as a string. No upper bound. Include the same scope. |
| Both conditions > 0 | `AND` | Combine both triggers in `and.triggers[]` array. |
| Neither condition | No trigger | Leave trigger field unset entirely. |
**Example — minSubTotal trigger** (upsell boost: spend $200+):
```json
{
"trigger": {
"triggerType": "SUBTOTAL_RANGE",
"subtotalRange": {
"from": "200",
"scopes": [
{
"id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CATALOG_ITEM",
"catalogItemFilter": {
"catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
}
}
]
}
}
}
```
**Example — minItemQuantity trigger** (bundle: buy 3+):
```json
{
"trigger": {
"triggerType": "ITEM_QUANTITY_RANGE",
"itemQuantityRange": {
"from": 3,
"scopes": [
{
"id": "collections_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CUSTOM_FILTER",
"customFilter": {
"appId": "215238eb-22a5-4c36-9e7b-e7c08025e04e",
"params": {
"collectionIds": ["category-uuid"]
}
}
}
]
}
}
}
```
**Example — AND trigger** (both conditions):
```json
{
"trigger": {
"triggerType": "AND",
"and": {
"triggers": [
{
"triggerType": "ITEM_QUANTITY_RANGE",
"itemQuantityRange": { "from": 2, "scopes": [/* same scope */] }
},
{
"triggerType": "SUBTOTAL_RANGE",
"subtotalRange": { "from": "100", "scopes": [/* same scope */] }
}
]
}
}
}
```
### Date handling
| Recommendation field | API mapping |
|---|---|
| `startDate` is a date string (e.g., `"2026-06-01"`) | Convert to ISO 8601 timestamp: `activeTimeInfo.start` |
| `startDate` is empty `""` | Default to current time (now) |
| `endDate` is a date string | Convert to ISO 8601 timestamp: `activeTimeInfo.end` |
| `endDate` is empty `""` | Omit `activeTimeInfo.end` — rule has no expiration |
---
## Error Handling
| Error | Cause | Fix |
|---|---|---|
| `DISCOUNT_RULE_NOT_FOUND` | The discount rule ID doesn't exist | Re-query discount rules to get current IDs |
| `REVISION_MISMATCH` | The `revision` doesn't match the current version | Re-fetch the rule to get the latest revision, then retry |
| `INVALID_DISCOUNT_TYPE` | Unsupported discount type | Use `PERCENTAGE` or `FIXED_AMOUNT` |
| `Expected an object` (400) | `discounts` sent as a bare array | Wrap the entries — `"discounts": { "values": [ … ] }` |
| `DISCOUNT_TYPE_AND_DISCOUNT_VALUE_MISMATCH` | `discountType` and the value field nested inside a `discount` object | Move both to the discount entry root, alongside `targetType` |
| Both `productIds` and `categoryIds` set | Scope mutual exclusivity violation | Use only one: ITEMS with productIds OR CATEGORY with categoryIds |
| `productIds` empty when scope is ITEMS | Missing required IDs | Query products and provide at least 1 product UUID |
| `categoryIds` empty when scope is CATEGORY | Missing required IDs | Call getCategoryIds to convert category names to GUIDs |
## References
- [Discount Rules API](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/introduction)
- [Coupons API](https://dev.wix.com/docs/api-reference/business-solutions/coupons/coupons/introduction)
SHA-256: 8216640dae0dace32f547cadcfdc40b36a22e1f16184c33b7e1d887f291d936a