Promotions and coupons
Create a coupon and an automatic discount, and apply them in the cart.
Create the BIENVENIDA10 coupon for 10% off orders from S/ 100 and free
shipping from S/ 150. Then apply the coupon in your headless store's cart.
Requirements
- An Admin token in
ECOMIQ_TOKENwith thepromotion:create,promotion:update, andpromotion:readpermissions. See Quickstart. - A cart in your headless store. See Headless store.
Coupon or automatic discount
- Automatic discount (
/): applies on its own when the cart meets its conditions. For example, free shipping from S/ 150.v1/ discounts - Coupon (
/): applies when the buyer enters one of its codes. For example,v1/ coupons BIENVENIDA10.
Both are configured the same way. type and target say what they discount:
| To give | type | target | amount |
|---|---|---|---|
| 10% off the whole order | percentage | order | 10 |
| S/ 20 off certain products | fixedAmount | item | 20 |
| 50% off shipping | percentage | shipping | 50 |
| Free shipping | freeShipping | shipping | 0 |
| Buy 3, pay 2 on t-shirts | buyXGetY | item | Not sent. |
With order, the discount is split across all items. With item, it covers
only the products in its conditions. GET / lists coupons and
discounts together.
Create the coupon
This coupon gives 10% off orders from S/ 100, once per customer and up to 1000 uses in total.
curl -X POST https://api.ecomiq.pe/api/v1/coupons \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Bienvenida 10 %",
"status": "active",
"type": "percentage",
"target": "order",
"amount": 10,
"validFrom": "2026-10-01T05:00:00Z",
"validTo": "2027-01-01T04:59:59Z",
"usageLimit": 1000,
"customerUsageLimit": 1,
"combineWithOtherDiscounts": true,
"rules": [
{ "field": "cartSubtotal", "operator": "gte", "decimalValues": [100] }
],
"codes": ["BIENVENIDA10"]
}'{
"id": "7204558912004325430"
}| Field | What it does |
|---|---|
status | active enables it. Without it, it stays a draft and never applies. |
validFrom, validTo | When it starts and ends, in UTC. 05:00Z is midnight in Lima. |
rules | The cart conditions. |
usageLimit | Total uses across all its codes. |
customerUsageLimit | Uses per customer. |
combineWithOtherDiscounts | Whether it stacks with another promotion. |
codes | Up to 500 codes of up to 100 characters. Stored in uppercase. |
codeUsageLimit | Uses per code. |
A code is unique within the store. To schedule a coupon, send active with a
future validFrom.
Create the automatic discount
This discount brings shipping to zero when the subtotal reaches S/ 150.
curl -X POST https://api.ecomiq.pe/api/v1/discounts \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Envío gratis desde S/ 150",
"status": "active",
"type": "freeShipping",
"target": "shipping",
"amount": 0,
"combineWithOtherDiscounts": true,
"rules": [
{ "field": "cartSubtotal", "operator": "gte", "decimalValues": [150] }
]
}'{
"id": "7204558912004325431"
}Conditions
The promotion applies only when the cart meets every rule in rules.
| To require | Rule |
|---|---|
| A minimum amount | { "field": "cartSubtotal", "operator": "gte", "decimalValues": [150] } |
| A minimum quantity | { "field": "cartTotalQuantity", "operator": "gte", "integerValues": [3] } |
| Certain products | { "field": "productId", "operator": "in", "resourceIds": ["7204558912004325302"] } |
| Certain customers | { "field": "customerId", "operator": "in", "resourceIds": ["7204558912004325301"] } |
| A customer group | { "field": "customerGroupId", "operator": "in", "resourceIds": ["7204558912004325310"] } |
| A payment method | { "field": "paymentMethodCode", "operator": "in", "codes": ["yape"] } |
variantId,categoryId,brandId, andcollectionIdwork likeproductId, witheq,neq,in, andnotIn.- Amounts and quantities accept
eq,neq,gt,gte,lt,in, andnotIn. customerId,customerGroupId, andpaymentMethodCodeaccept onlyin, one rule of each per promotion. A promotion is limited to customers or to groups, not both.inandnotInaccept up to 500 values.- The subtotal is measured before discounts.
With target: "order", a product rule only requires the product to be in the
cart. With target: "item", the discount covers only those products, and
maxQuantity caps how many units.
A coupon with a customer or group rule works only in a cart created with that customer's session. See Customer accounts.
Buy X, get Y
For a buy 3, pay 2 deal, use type: "buyXGetY" and the buyXGetY object.
With this body, the cheapest of every three cotton t-shirts is free:
{
"name": "3x2 en polos",
"status": "active",
"type": "buyXGetY",
"target": "item",
"buyXGetY": {
"buyQuantity": 2,
"buyRules": [
{
"field": "productId",
"operator": "in",
"resourceIds": ["7204558912004325302"]
}
],
"getQuantity": 1,
"getRules": [
{
"field": "productId",
"operator": "in",
"resourceIds": ["7204558912004325302"]
}
],
"discountType": "percentage",
"discountAmount": 100
}
}| Field | What it defines |
|---|---|
buyQuantity | Units the buyer must purchase. To require an amount, use buyAmount. |
buyRules | Which products count toward the purchase. |
getQuantity | Units that get the discount. |
getRules | Which products get the discount. |
discountType, discountAmount | The discount per unit. 100% makes it free. |
maxApplicationsPerOrder | Times it applies per order. Without it, there's no cap. |
buyRules and getRules accept only IDs. A free unit doesn't count toward the
purchase.
Decide whether they stack
When a cart qualifies for several promotions, the coupon applies first, then
automatic discounts from newest to oldest. Two promotions stack only if both
have combineWithOtherDiscounts: true.
Both do in this example: a buyer with BIENVENIDA10 and S/ 150 in the cart
gets 10% off and free shipping. With false on the coupon, they'd get only the
10%. An item never goes below zero.
To edit, send everything
PUT / and PUT / replace the
promotion. If you omit status, it goes back to draft; if you omit a date or
a limit, it's cleared. Only rules, codes, maxQuantity, and buyXGetY
are kept when you don't send them.
Create more codes
A coupon can have many codes, such as one single-use code per buyer. You generate the codes.
| How many | Operation | Response |
|---|---|---|
| One | POST / | 201 with the code. |
| Up to 100 | POST / | 200 with one result per code. |
| More than 100 | POST / | 202 with an operation. |
usageLimit is optional; with 1, the code works once:
curl -X POST https://api.ecomiq.pe/api/v1/coupons/7204558912004325430/codes \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "code": "BIENVENIDA-ANDREA", "usageLimit": 1 }'{
"id": "7204558912004325432",
"code": "BIENVENIDA-ANDREA",
"usageLimit": 1,
"usageCount": 0,
"createdOn": "2026-10-05T15:30:00Z",
"updatedOn": null
}Up to 100 codes
curl -X POST https://api.ecomiq.pe/api/v1/coupons/7204558912004325430/codes/batch \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: bienvenida-lote-001" \
-d '{
"codes": ["BIENVENIDA-7K2P9Q", "BIENVENIDA-M4XH2D", "BIENVENIDA-Q8ZR6T"],
"usageLimit": 1
}'{
"results": [
{ "index": 0, "id": "7204558912004325433", "status": "created", "errors": [] },
{ "index": 1, "id": "7204558912004325434", "status": "created", "errors": [] },
{
"index": 2,
"id": null,
"status": "failed",
"errors": [
{
"field": "code",
"code": "PromotionCode.Exists",
"message": "Promotion code 'BIENVENIDA-Q8ZR6T' already exists."
}
]
}
]
}Each code is created separately: if one fails, the rest go through. results[]
follows the order you sent. If you lose the response, retry with the same
Idempotency-Key. On PromotionCode.Exists, generate another code; on
PromotionCode.InvalidCode, use 1 to 100 characters.
More than 100 codes
Upload a CSV with a code column and, optionally, usageLimit:
code,usageLimit
BIENVENIDA-7K2P9Q,1
BIENVENIDA-M4XH2D,1
BIENVENIDA-Q8ZR6T,1curl -X POST https://api.ecomiq.pe/api/v1/coupons/7204558912004325430/codes/imports \
-H "Authorization: Bearer $ECOMIQ_TOKEN" \
-H "Content-Type: text/csv" \
-H "Idempotency-Key: bienvenida-archivo-001" \
--data-binary @codes.csv{
"id": "7204558912004325440",
"status": "pending",
"resource": "promotion-codes",
"format": "csv",
"createdOn": "2026-10-05T15:30:00Z"
}Track the operation as in Product import, which covers formats, limits, and how to read rejected rows.
Apply the code in the cart
Send the code the buyer entered, in any case. The call carries
x-Store-Domain and no token. cart is the cart from
Headless store.
curl -X POST https://storefront.ecomiq.pe/v1/carts/<cart-token>/promotion-codes \
-H "x-Store-Domain: mi-tienda" \
-H "Content-Type: application/json" \
-d '{ "code": "bienvenida10" }'The cart holds two cotton t-shirts at S/ 79. 10% of S/ 158 is S/ 15.80:
{
"currencyCode": "PEN",
"promotionCodes": ["BIENVENIDA10"],
"items": [
{
"id": "7204558912004325450",
"title": "Polo de algodón",
"variantSku": "POL-001-AZ-M",
"quantity": 2,
"unitPrice": 79
}
],
"totals": {
"subtotal": 158,
"discountTotal": -15.8,
"shippingOriginalTotal": 0,
"shippingTotal": 0,
"total": 142.2
}
}| Field | What it shows |
|---|---|
promotionCodes | The applied codes. |
totals.discountTotal | The discount amount, as a negative number. |
totals.shippingTotal | Shipping after discounts. shippingOriginalTotal is the original. |
totals.total | What the buyer pays. |
Every cart change recalculates discounts. Free shipping shows up in
shippingTotal once the buyer picks a shipping option.
No minimum, no error
If the cart doesn't meet the conditions, the code is still added to
promotionCodes, but it discounts nothing until the cart qualifies.
To show "add S/ 21 more," read the coupon's rules from your backend with
GET /:
the cart doesn't include them.
To remove a code, use DELETE. The response is the recalculated cart:
curl -X DELETE https://storefront.ecomiq.pe/v1/carts/<cart-token>/promotion-codes/BIENVENIDA10 \
-H "x-Store-Domain: mi-tienda"Checkout checks again
POST / recalculates discounts. If a code
expired or ran out in the meantime, it returns 409 with
Promotions.CodeNoLongerApplies and the code in params.code, and removes it
from the cart: show the new total and ask for payment again.
A use of the code is reserved when payment starts, so two purchases can't take the last one at the same time; if the order isn't created, the use is freed again. In a cart without a session, the per-customer limit counts the order email, but that email doesn't unlock a customer's promotions.
Errors
When a code doesn't work, the API returns an error and doesn't add it:
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Promotions.CodeLimitReached": ["Promotion code 'BIENVENIDA10' reached its usage limit."]
},
"code": "Promotions.CodeLimitReached"
}Pick the buyer-facing message by code. Attempts are limited per minute; with
429, wait for the time in Retry-After.
code | Status | What happened |
|---|---|---|
Promotions.CodeNotFound | 400 | The code doesn't exist. |
Promotions.CodeLimitReached | 400 | The coupon ran out: it was used every time it allows. |
Promotions.CustomerUsageLimitReached | 400 | The customer has used the coupon the maximum number of times. |
Promotions.CodeInactive | 400 | The coupon is inactive, hasn't started, or expired. |
Promotions.CodeNotAssigned | 400 | The coupon is for other customers or groups. |
Promotions.CodeInvalid | 400 | The code belongs to an automatic promotion. |
Carts.NotActive | 400 | The cart was already purchased, canceled, or expired. |
Checkout.InProgress | 409 | The cart's payment is in progress. |
| No body | 404 | The cart doesn't exist. |
When creating promotions in Admin API:
code | Status | What to do |
|---|---|---|
Coupon.CodeRequired | 400 | Send at least one code in codes. |
Promotion.NotFound | 404 | Check the coupon ID. |
Promotion.Exists | 409 | Another promotion uses that name. |
PromotionCode.Exists | 409 | One of the codes already exists. |
Next steps
Last updated: