discounts structureThe API always returns the full normalized structure, values wrapper included. Never assume a simplified form. Each entry of discounts.values looks like:
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:
Request — find by name (exact match):
Filterable fields: id, name, active, revision, created_date, updated_date, active_time_info.start, active_time_info.end
Response:
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.
Endpoint: POST https://www.wixapis.com/ecom/v1/discount-rules
Request — 20% off all products:
Request — 15% off a specific collection:
Request — $5 off specific products:
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):
Request — change only active status (field mask for partial update):
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:
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:
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.
To deactivate without deleting:
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:
Example — ITEMS scope (with product IDs):
Example — CATEGORY scope (with collection IDs):
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+):
Example — minItemQuantity trigger (bundle: buy 3+):
Example — AND trigger (both conditions):
| 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 |
Last updated: 9 August 2026