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
- Get Discount Rule
- Query Discount Rules
- Update Discount Rule
- 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:
{
"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:
{
"query": {
"cursorPaging": {
"limit": 100
}
}
}Request — find by name (exact match):
{
"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:
{
"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;CATALOGvsCOLLECTION; sameCOLLECTION/SPECIFIC_PRODUCTSid). Wix stacks automatic rules → warn and offer to deactivate the existing one. - Time overlap — overlapping
activeTimeInfoon 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 × 100must 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.
Step 2: Create a percentage discount rule
Endpoint: POST https://www.wixapis.com/ecom/v1/discount-rules
Request — 20% off all products:
{
"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:
{
"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:
{
"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):
{
"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):
{
"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":
- Query with name filter → get the rule's
id,revision, anddiscounts.valuesentries - Modify only
percentageon each entry, keeping all other fields intact - PATCH with the modified
discountsandmask: { paths: ["discounts"] }
Step 5a — Query by name:
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:
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.scopesverbatim from the query response — do not reconstruct them. Only changediscountTypeandpercentage/fixedAmount, keeping them at the entry root.
Step 6: Deactivate or delete a discount rule
To deactivate without deleting:
{
"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: falsewithstatus: "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:
{
"scope": {
"id": "all_215238eb-22a5-4c36-9e7b-e7c08025e04e",
"type": "CATALOG_ITEM",
"catalogItemFilter": {
"catalogAppId": "215238eb-22a5-4c36-9e7b-e7c08025e04e"
}
}
}Example — ITEMS scope (with product IDs):
{
"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):
{
"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+):
{
"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+):
{
"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):
{
"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 |