Subchapter 8.44
references/ecommerce/pricing-promotions/ecom-pricing-create-discount-rule.mdMarkdown19 KBView on GitHub
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.
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.
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):
CATALOG; CATALOG vs COLLECTION; same COLLECTION/SPECIFIC_PRODUCTS id). Wix stacks automatic rules → warn and offer to deactivate the existing one.activeTimeInfo on the same scope (existingStart < newEnd AND existingEnd > newStart) → warn about the overlap window.Margin / sanity:
effective_margin = (price − cost − discount_amount) / price × 100 must stay ≥ 15% (block + explain otherwise).| 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 (opens in a new tab).
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
}
] }
}
}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"
}
] }
}
}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"] }
}The safe pattern for “find a rule by name and update its percentage”:
id, revision, and discounts.values entriespercentage on each entry, keeping all other fields intactdiscounts and mask: { 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.
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}
| 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 | 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
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.
215238eb-22a5-4c36-9e7b-e7c08025e04e — used in all scope constructions below.active: false with status: "PENDING". The merchant must approve before the rule goes live.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"]
}
}
}
}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[].
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 */] }
}
]
}
}
}| 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 | 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 |
Source