discounts structureThe API always returns the full normalized structure. Never assume a simplified form. Each discount entry looks like:
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.
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:
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.
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 arraydiscount.percentage inside each discount 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 array from the query response and update only discount.percentage on each entry:
Important: Copy targetType, specificItemsInfo.scopes verbatim from the query response — do not reconstruct them. Only change discount.discountType and discount.percentage/discount.fixedAmount.
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[].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 | 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 | 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[].
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 |
All recommendation-created rules use these fixed settings:
| 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 |