Skip to content
Customers & sales

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_TOKEN with the promotion:create, promotion:update, and promotion:read permissions. See Quickstart.
  • A cart in your headless store. See Headless store.

Coupon or automatic discount

  • Automatic discount (/v1/discounts): applies on its own when the cart meets its conditions. For example, free shipping from S/ 150.
  • Coupon (/v1/coupons): applies when the buyer enters one of its codes. For example, BIENVENIDA10.

Both are configured the same way. type and target say what they discount:

To givetypetargetamount
10% off the whole orderpercentageorder10
S/ 20 off certain productsfixedAmountitem20
50% off shippingpercentageshipping50
Free shippingfreeShippingshipping0
Buy 3, pay 2 on t-shirtsbuyXGetYitemNot sent.

With order, the discount is split across all items. With item, it covers only the products in its conditions. GET /v1/promotions 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"]
  }'
Response 201
{
  "id": "7204558912004325430"
}
FieldWhat it does
statusactive enables it. Without it, it stays a draft and never applies.
validFrom, validToWhen it starts and ends, in UTC. 05:00Z is midnight in Lima.
rulesThe cart conditions.
usageLimitTotal uses across all its codes.
customerUsageLimitUses per customer.
combineWithOtherDiscountsWhether it stacks with another promotion.
codesUp to 500 codes of up to 100 characters. Stored in uppercase.
codeUsageLimitUses 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] }
    ]
  }'
Response 201
{
  "id": "7204558912004325431"
}

Conditions

The promotion applies only when the cart meets every rule in rules.

To requireRule
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, and collectionId work like productId, with eq, neq, in, and notIn.
  • Amounts and quantities accept eq, neq, gt, gte, lt, in, and notIn.
  • customerId, customerGroupId, and paymentMethodCode accept only in, one rule of each per promotion. A promotion is limited to customers or to groups, not both.
  • in and notIn accept 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:

Body of POST /v1/discounts
{
  "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
  }
}
FieldWhat it defines
buyQuantityUnits the buyer must purchase. To require an amount, use buyAmount.
buyRulesWhich products count toward the purchase.
getQuantityUnits that get the discount.
getRulesWhich products get the discount.
discountType, discountAmountThe discount per unit. 100% makes it free.
maxApplicationsPerOrderTimes 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 /v1/coupons/{couponId} and PUT /v1/discounts/{discountId} 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 manyOperationResponse
OnePOST /v1/coupons/{couponId}/codes201 with the code.
Up to 100POST /v1/coupons/{couponId}/codes/batch200 with one result per code.
More than 100POST /v1/coupons/{couponId}/codes/imports202 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 }'
Response 201
{
  "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
  }'
Response 200
{
  "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:

codes.csv
code,usageLimit
BIENVENIDA-7K2P9Q,1
BIENVENIDA-M4XH2D,1
BIENVENIDA-Q8ZR6T,1
curl -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
Response 202
{
  "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:

Response 200 (excerpt)
{
  "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
  }
}
FieldWhat it shows
promotionCodesThe applied codes.
totals.discountTotalThe discount amount, as a negative number.
totals.shippingTotalShipping after discounts. shippingOriginalTotal is the original.
totals.totalWhat 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 /v1/coupons/{couponId}: 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 /v1/carts/{cartToken}/checkout 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:

Response 400
{
  "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.

codeStatusWhat happened
Promotions.CodeNotFound400The code doesn't exist.
Promotions.CodeLimitReached400The coupon ran out: it was used every time it allows.
Promotions.CustomerUsageLimitReached400The customer has used the coupon the maximum number of times.
Promotions.CodeInactive400The coupon is inactive, hasn't started, or expired.
Promotions.CodeNotAssigned400The coupon is for other customers or groups.
Promotions.CodeInvalid400The code belongs to an automatic promotion.
Carts.NotActive400The cart was already purchased, canceled, or expired.
Checkout.InProgress409The cart's payment is in progress.
No body404The cart doesn't exist.

When creating promotions in Admin API:

codeStatusWhat to do
Coupon.CodeRequired400Send at least one code in codes.
Promotion.NotFound404Check the coupon ID.
Promotion.Exists409Another promotion uses that name.
PromotionCode.Exists409One of the codes already exists.

Next steps

Last updated:

On this page