Promociones y cupones
Crea un cupón y un descuento automático, y aplícalos en el carrito.
Crea el cupón BIENVENIDA10 con 10 % de descuento desde S/ 100 y un envío
gratis desde S/ 150. Luego aplica el cupón en el carrito de tu tienda headless.
Requisitos
- Un token de Admin en
ECOMIQ_TOKEN, con los permisospromotion:create,promotion:updateypromotion:read. Ver Inicio rápido. - Un carrito en tu tienda headless. Ver Tienda headless.
Cupón o descuento automático
- Descuento automático (
/): se aplica solo cuando el carrito cumple sus condiciones. Por ejemplo, envío gratis desde S/ 150.v1/ discounts - Cupón (
/): se aplica cuando el comprador escribe uno de sus códigos. Por ejemplo,v1/ coupons BIENVENIDA10.
Los dos se configuran igual. type y target dicen qué descuentan:
| Para dar | type | target | amount |
|---|---|---|---|
| 10 % en todo el pedido | percentage | order | 10 |
| S/ 20 menos en ciertos productos | fixedAmount | item | 20 |
| 50 % del envío | percentage | shipping | 50 |
| Envío gratis | freeShipping | shipping | 0 |
| 3x2 en polos | buyXGetY | item | No va. |
Con order, el descuento se reparte entre todos los artículos. Con item,
cubre solo los productos de sus condiciones. GET / lista cupones
y descuentos juntos.
Crea el cupón
Este cupón da 10 % en compras desde S/ 100, una vez por cliente y hasta 1000 usos en 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"
}| Campo | Qué hace |
|---|---|
status | active lo habilita. Sin él, queda en borrador y no se aplica. |
validFrom, validTo | Desde y hasta cuándo vale, en UTC. 05:00Z es medianoche en Lima. |
rules | Las condiciones del carrito. |
usageLimit | Usos en total, sumando todos sus códigos. |
customerUsageLimit | Usos por cliente. |
combineWithOtherDiscounts | Si se suma a otra promoción. |
codes | Hasta 500 códigos de hasta 100 caracteres. Se guardan en mayúsculas. |
codeUsageLimit | Usos de cada código. |
Un código no se repite en la tienda. Para programar un cupón, envía active
con un validFrom futuro.
Crea el descuento automático
Este descuento deja el envío en cero cuando el subtotal llega a 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"
}Condiciones
La promoción se aplica solo si el carrito cumple todas las reglas de rules.
| Para exigir | Regla |
|---|---|
| Un monto mínimo | { "field": "cartSubtotal", "operator": "gte", "decimalValues": [150] } |
| Unidades mínimas | { "field": "cartTotalQuantity", "operator": "gte", "integerValues": [3] } |
| Ciertos productos | { "field": "productId", "operator": "in", "resourceIds": ["7204558912004325302"] } |
| Ciertos clientes | { "field": "customerId", "operator": "in", "resourceIds": ["7204558912004325301"] } |
| Un grupo de clientes | { "field": "customerGroupId", "operator": "in", "resourceIds": ["7204558912004325310"] } |
| Un método de pago | { "field": "paymentMethodCode", "operator": "in", "codes": ["yape"] } |
variantId,categoryId,brandIdycollectionIdfuncionan igual queproductId, coneq,neq,inynotIn.- Montos y cantidades aceptan
eq,neq,gt,gte,lt,inynotIn. customerId,customerGroupIdypaymentMethodCodeaceptan soloin, una regla de cada uno por promoción. Una promoción se limita a clientes o a grupos, no a ambos.inynotInaceptan hasta 500 valores.- El subtotal se mide antes de descuentos.
Con target: "order", una regla de producto solo pide que el producto esté en
el carrito. Con target: "item", el descuento cubre solo esos productos, y
maxQuantity limita cuántas unidades.
Un cupón con regla de cliente o de grupo solo funciona en un carrito creado con la sesión de ese cliente. Ver Cuentas de clientes.
Compra X, lleva Y
Para un 3x2, usa type: "buyXGetY" y el objeto buyXGetY. Con este cuerpo, de
cada tres Polos de algodón, el más barato sale gratis:
{
"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
}
}| Campo | Qué define |
|---|---|
buyQuantity | Unidades que debe comprar. Para pedir un monto, usa buyAmount. |
buyRules | Qué productos cuentan para la compra. |
getQuantity | Unidades que reciben el descuento. |
getRules | Qué productos reciben el descuento. |
discountType, discountAmount | El descuento de cada unidad. 100 % la deja gratis. |
maxApplicationsPerOrder | Veces que se aplica por pedido. Sin él, no hay tope. |
buyRules y getRules solo aceptan IDs. Una unidad de regalo no cuenta para
la compra.
Decide si se suman
Si un carrito cumple varias promociones, primero se aplica el cupón y luego los
descuentos automáticos, del más reciente al más antiguo. Dos promociones se
suman solo si ambas tienen combineWithOtherDiscounts: true.
En este ejemplo las dos lo tienen: un comprador con BIENVENIDA10 y S/ 150 en
el carrito recibe el 10 % y el envío gratis. Con false en el cupón, solo
recibiría el 10 %. Un artículo nunca baja de cero.
Para editar, envía todo
PUT / y PUT / reemplazan la
promoción. Si omites status, vuelve a borrador; si omites una fecha o un
límite, se borra. Solo rules, codes, maxQuantity y buyXGetY se
conservan si no los envías.
Crea más códigos
Un cupón puede tener muchos códigos, por ejemplo uno de un solo uso por comprador. Tú generas los códigos.
| Cuántos | Operación | Respuesta |
|---|---|---|
| Uno | POST / | 201 con el código. |
| Hasta 100 | POST / | 200 con un resultado por código. |
| Más de 100 | POST / | 202 con una operación. |
usageLimit es opcional; con 1, el código sirve una sola vez:
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
}Hasta 100 códigos
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."
}
]
}
]
}Cada código se crea por separado: si uno falla, los demás siguen. results[]
va en el orden que enviaste. Si pierdes la respuesta, reintenta con la misma
Idempotency-Key. Con PromotionCode.Exists, genera otro código; con
PromotionCode.InvalidCode, usa entre 1 y 100 caracteres.
Más de 100 códigos
Sube un CSV con la columna code y, si quieres, 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 @codigos.csv{
"id": "7204558912004325440",
"status": "pending",
"resource": "promotion-codes",
"format": "csv",
"createdOn": "2026-10-05T15:30:00Z"
}Sigue la operación como en Importación de productos, donde están los formatos, los límites y cómo leer las filas rechazadas.
Aplica el código en el carrito
Envía el código que escribió el comprador, en mayúsculas o minúsculas. La
llamada lleva x-Store-Domain y no usa token. cart es el carrito de
Tienda headless.
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" }'El carrito tiene dos Polos de algodón a S/ 79. El 10 % de S/ 158 es 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
}
}| Campo | Qué muestra |
|---|---|
promotionCodes | Los códigos aplicados. |
totals.discountTotal | Lo que descuentan, en negativo. |
totals.shippingTotal | El envío con descuento. shippingOriginalTotal es el de antes. |
totals.total | Lo que paga el comprador. |
Cada cambio del carrito recalcula los descuentos. El envío gratis aparece en
shippingTotal cuando el comprador elige un envío.
Sin el mínimo no hay error
Si el carrito no cumple las condiciones, el código igual se agrega a
promotionCodes, pero no descuenta nada hasta que el carrito las cumpla.
Para mostrar «te faltan S/ 21», lee las rules del cupón desde tu backend con
GET /:
el carrito no las trae.
Para quitar un código, usa DELETE. La respuesta es el carrito recalculado:
curl -X DELETE https://storefront.ecomiq.pe/v1/carts/<cart-token>/promotion-codes/BIENVENIDA10 \
-H "x-Store-Domain: mi-tienda"El checkout vuelve a revisar
POST / recalcula los descuentos. Si un código
venció o se agotó entretanto, responde 409 con
Promotions.CodeNoLongerApplies y el código en params.code, y lo quita del
carrito: muestra el nuevo total y vuelve a pedir el pago.
El uso del código se reserva al empezar el pago, así que dos compras no se llevan el último a la vez; si el pedido no se crea, el uso vuelve a quedar libre. En un carrito sin sesión, el límite por cliente se cuenta con el correo de la compra, pero ese correo no da acceso a las promociones de un cliente.
Errores
Al aplicar un código que no sirve, la API responde un error y no lo agrega:
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Promotions.CodeLimitReached": ["Promotion code 'BIENVENIDA10' reached its usage limit."]
},
"code": "Promotions.CodeLimitReached"
}Elige el mensaje para el comprador según code. Los intentos tienen un límite
por minuto; con 429, espera lo que indique Retry-After.
code | Estado | Qué pasó |
|---|---|---|
Promotions.CodeNotFound | 400 | El código no existe. |
Promotions.CodeLimitReached | 400 | El cupón se agotó: ya se usó todas las veces. |
Promotions.CustomerUsageLimitReached | 400 | El cliente ya usó el cupón el máximo de veces. |
Promotions.CodeInactive | 400 | El cupón está inactivo, aún no empieza o venció. |
Promotions.CodeNotAssigned | 400 | El cupón es para otros clientes o grupos. |
Promotions.CodeInvalid | 400 | El código es de una promoción automática. |
Carts.NotActive | 400 | El carrito ya se compró, se canceló o venció. |
Checkout.InProgress | 409 | El pago del carrito está en curso. |
| Sin cuerpo | 404 | El carrito no existe. |
Al crear promociones en Admin API:
code | Estado | Qué hacer |
|---|---|---|
Coupon.CodeRequired | 400 | Envía al menos un código en codes. |
Promotion.NotFound | 404 | Revisa el ID del cupón. |
Promotion.Exists | 409 | Otra promoción usa ese name. |
PromotionCode.Exists | 409 | Uno de los códigos ya existe. |
Siguientes pasos
Última actualización: