> Portal Navigation:
> 
> - Append `.md` to any URL under `https://dev.wix.com/docs/` to get its markdown version.
> - Pages are either content pages (article or reference text) or menu pages (a list of links to child pages).
> - To get a menu page, truncate any URL to a parent path and append `.md` (e.g. `https://dev.wix.com/docs/sdk.md`, `https://dev.wix.com/docs/sdk/core-modules.md`).
> - Top-level index of all portals: https://dev.wix.com/docs/llms.txt
> - Full concatenated docs: https://dev.wix.com/docs/llms-full.txt

## Resource: Pricing: Create Discount Rule

## Article: Pricing: Create Discount Rule

## Article Link: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/skills/pricing-create-discount-rule.md

## Article Content:

# 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.md)
- [Get Discount Rule](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/get-discount-rule.md)
- [Query Discount Rules](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/query-discount-rules.md)
- [Update Discount Rule](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/update-discount-rule.md)
- [Delete Discount Rule](https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/extensions/discounts/discount-rules/delete-discount-rule.md)

---

## Critical: `discounts` structure

**The API always returns the full normalized structure.** Never assume a simplified form. Each discount entry 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"
        }
      }
    ]
  },
  "discount": {
    "discountType": "PERCENTAGE",
    "percentage": 20
  }
}
```

When updating a rule, always use the `discounts` array 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`

> **Paging**: This API uses **cursor paging** (`cursorPaging`), not offset paging. Using `paging` instead of `cursorPaging` will fail.

**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": [
        {
          "targetType": "SPECIFIC_ITEMS",
          "specificItemsInfo": {
            "scopes": [
              {
                "id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
                "type": "CATALOG_ITEM",
                "catalogItemFilter": {
                  "catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
                }
              }
            ]
          },
          "discount": {
            "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 / sanity:**
- **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).
- **% sanity** — > 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.md).

---

## 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": [
      {
        "targetType": "SPECIFIC_ITEMS",
        "specificItemsInfo": {
          "scopes": [
            {
              "id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
              "type": "CATALOG_ITEM",
              "catalogItemFilter": {
                "catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
              }
            }
          ]
        },
        "discount": {
          "discountType": "PERCENTAGE",
          "percentage": 20
        }
      }
    ],
    "settings": {
      "appliesTo": "ALL_ITEMS"
    }
  }
}
```

**Request** — 15% off a specific collection:
```json
{
  "discountRule": {
    "name": "Summer Collection Sale",
    "active": true,
    "discounts": [
      {
        "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"]
                }
              }
            }
          ]
        },
        "discount": {
          "discountType": "PERCENTAGE",
          "percentage": 15
        }
      }
    ],
    "settings": {
      "appliesTo": "ALL_ITEMS"
    }
  }
}
```

---

## Step 3: Create a fixed-amount discount rule

**Request** — $5 off specific products:
```json
{
  "discountRule": {
    "name": "$5 Off Selected Items",
    "active": true,
    "discounts": [
      {
        "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"]
              }
            }
          ]
        },
        "discount": {
          "discountType": "FIXED_AMOUNT",
          "fixedAmount": "5.00"
        }
      }
    ],
    "settings": {
      "appliesTo": "ALL_ITEMS"
    }
  }
}
```

---

## 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": [
      {
        "targetType": "SPECIFIC_ITEMS",
        "specificItemsInfo": {
          "scopes": [
            {
              "id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
              "type": "CATALOG_ITEM",
              "catalogItemFilter": {
                "catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
              }
            }
          ]
        },
        "discount": {
          "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` array
2. Modify only the `discount.percentage` inside each discount 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` array from the query response and update only `discount.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": [
      {
        "targetType": "SPECIFIC_ITEMS",
        "specificItemsInfo": { "<scopes unchanged from query response>" },
        "discount": {
          "discountType": "PERCENTAGE",
          "percentage": 10
        }
      }
    ]
  },
  "mask": { "paths": ["discounts"] }
}
```

> **Important**: Copy `targetType`, `specificItemsInfo.scopes` verbatim from the query response — do not reconstruct them. Only change `discount.discountType` and `discount.percentage`/`discount.fixedAmount`.

---

## 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[].targetType` | Yes | Always `"SPECIFIC_ITEMS"` for standard rules |
| `discounts[].specificItemsInfo.scopes[]` | Yes | Array of scope objects — see Scope types below |
| `discounts[].discount.discountType` | Yes | `"PERCENTAGE"` or `"FIXED_AMOUNT"` or `"FIXED_PRICE"` |
| `discounts[].discount.percentage` | If PERCENTAGE | Integer 1-100 |
| `discounts[].discount.fixedAmount` | If FIXED_AMOUNT | Decimal string (e.g., `"5.00"`) |
| `settings.appliesTo` | Yes on create | Always `"ALL_ITEMS"` |
| `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` | `discount.percentage` | Integer (e.g., `15`) |
| `FIXED_AMOUNT` | `discount.fixedAmount` | String (e.g., `"5.00"`) |
| `FIXED_PRICE` | `discount.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 |

### Settings

All recommendation-created rules use these fixed settings:
```json
{
  "settings": {
    "appliesTo": "ALL_ITEMS",
    "indexOptIn": true
  }
}
```

---

## 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` |
| 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.md)
- [Coupons API](https://dev.wix.com/docs/api-reference/business-solutions/coupons/coupons/introduction.md)