Promotions
Code-based discounts, their conditions and effects, promotion codes, and cart usage
A promotion is a discount rule: who gets it (conditions), what it does (effects), how it
combines with others (stacking, priority) and how often it can be used. It applies only through
its promotion codes: a buyer enters a code, and there are no automatic promotions. A promotion is
a discount, not stored value.
Effects
import type { PromotionEffect } from "arky-sdk";
const effects: PromotionEffect[] = [
{
type: "item_percentage",
id: crypto.randomUUID(),
target: { type: "categories", category_ids: [accessoriesCategoryId] },
basis_points: 1_000,
},
{
type: "delivery_fixed",
id: crypto.randomUUID(),
amounts: [
{ amount: 500, currency: "eur" },
{ amount: 1_000, currency: "bam" },
],
},
];
- Effects are
item_percentage,item_fixed,order_percentage,order_fixed,delivery_percentage,delivery_fixedandbuy_x_get_y. Each has an id the app picks, unique within its promotion. - Item effects name a target: products, product variants, booking services, customer group offerings, customer groups, catalogs (the lines priced from them), categories, or all eligible items.
item_fixedtakes its amount off each matching unit (scope: { type: "per_unit" }) or once, split across the matching units (scope: { type: "once_per_order" }). Order effects work once across all lines that take promotions, and delivery effects once across the cart’s deliveries.buy_x_get_yrepeats as often as the cart allows: each timebuy.quantitymatching units andget.quantityreward units are there, the cheapest reward units get its percentage off.- Percentages are basis points:
100is 1%,10_000is 100%. Fixed amounts list one amount per currency, each currency once, in integer minor units. - Effects apply in three rounds: every applied promotion’s item effects, then their order effects,
then their delivery effects. Within a round, promotions go from the highest
prioritydown, and each works on what earlier discounts left, so a line never goes below zero. - Only lines priced from the cart’s catalog take promotions, and a manual price only when it allows them. Offer lines, purchase-access lines and a group line that switches a member take none. A promotion on a group or offering reduces the group line, never again the lines it generates.
Conditions
All conditions must hold together: signed_in, customer (verified customers only),
customer_group, customer_group_offering, company, company_location, market,
sales_channel and minimum_subtotal. A customer group condition holds for the buyer’s groups that
count; a company condition holds for the company’s own carts and its locations’.
minimum_subtotal lists one amount per currency and holds when the lines that take promotions add up
to it, at their prices before any discount and without separately added tax.
All the fixed amounts and the minimum of one promotion list the same currencies. A promotion with fixed amounts or a minimum applies only in carts whose currency they list; a promotion with only percentages applies in any currency.
Create, Read, Update, Delete
/v1/stores/{store_id}/promotions sdk.eshop.promotion.create() /v1/stores/{store_id}/promotions sdk.eshop.promotion.find() /v1/stores/{store_id}/promotions/{id} sdk.eshop.promotion.update() /v1/stores/{store_id}/promotions/{id}?expected_updated_at= sdk.eshop.promotion.delete() /v1/stores/{store_id}/promotion-codes sdk.eshop.promotionCode.create() /v1/stores/{store_id}/promotion-codes sdk.eshop.promotionCode.find() /v1/stores/{store_id}/promotion-codes/by-code/{code} sdk.eshop.promotionCode.getByCode() const promotion = await sdk.eshop.promotion.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "summer-20",
blocks: [{ id: crypto.randomUUID(), key: "name", type: "localized_text", value: { en: "Summer sale" } }],
conditions: [{ type: "minimum_subtotal", amounts: [{ amount: 5_000, currency: "eur" }] }],
effects: [{ type: "order_percentage", id: crypto.randomUUID(), basis_points: 2_000 }],
stacking: { type: "exclusive" },
priority: 10,
max_uses: 1_000,
max_uses_per_customer: 1,
schedule: { type: "scheduled", starts_at: Date.UTC(2026, 5, 1), ends_at: Date.UTC(2026, 8, 1) },
status: { type: "active" },
});
const code = await sdk.eshop.promotionCode.create({
store_id: storeId,
id: crypto.randomUUID(),
promotion_id: promotion.id,
code: "SUMMER20",
max_uses: null,
status: { type: "active" },
});
- The key is unique in the store (
409 PROMOTION.KEY_TAKEN), and so is thepriority(409 PROMOTION.PRIORITY_TAKEN). A code is unique in the store (409 PROMOTION_CODE.CODE_TAKEN). blockshold what a storefront or an email shows for the promotion, such as its name and terms. Every email that carries an order lists each promotion the order applied with its currentblocksatorder.promotions, read when the email is made and never stored on the order.scheduleis{ type: "always" }or{ type: "scheduled", starts_at, ends_at }, whereends_atmay benullfor “from then on”. Codes share their promotion’s dates.- A promotion is eligible while it is
active, inside its schedule and undermax_uses, its conditions hold, and the cart carries one of its active codes under that code’s ownmax_uses. max_uses_per_customercounts per email: a signed-in customer by their verified email, a guest by the checkout’s contact email, so a guest checkout using such a promotion needs one.- Uses are counted when checkout is accepted. An order the buyer never pays gives its uses back once all its lines end expired or payment failed; any other cancellation gives nothing back.
- An update sends the
expected_updated_atit read and the complete conditions and effects. - A code can be deleted while only carts name it; deleting takes it off those carts. A promotion can be deleted while only its own codes name it; a promotion an order names is archived instead.
- Webhooks post
promotion.created,promotion.updated,promotion.deleted,promotion_code.createdandpromotion_code.deleted. Uses taken or given back don’t postpromotion.updated.
In The Cart
The storefront sets the codes on the cart and reads the discount from the quote:
const cart = arky.eshop.cart({ buyer: { type: "customer" }, catalog_id: catalog.id });
await cart.setPromotionCodes({ promotion_codes: ["SUMMER20"] });
const quote = await cart.quote({ contact_email: "ana@example.com" });
console.log(quote.totals.discount, quote.promotions);
- A cart holds either one
exclusivepromotion’s code or any number ofcombinablepromotions’ codes. Adding a code that breaks this is refused with409 PROMOTION.STACKING_CONFLICT, and so is a quote or checkout whose codes break it, for example after a stacking edit, until the buyer removes one. - A code limited to one use per customer counts a guest by the contact email, so a guest’s quote
sends
contact_emailonce it is known; without it such a code’s quote is refused with409 CART.PROMOTION_EMAIL_REQUIRED. The checkout flow sends it aftersetContact. - Storefront code never computes its own discounted total. The quote and checkout check each promotion against the cart, and the order keeps each promotion it applied with its key and code.
To show a promotion’s name or terms on the storefront, read its blocks by id:
/v1/storefront/promotions/{id} arky.eshop.promotion.get() const shown = await arky.eshop.promotion.get({ id: quote.promotions[0].promotion_id });
The storefront reads an active or archived promotion’s id, key and blocks only, never its
conditions or counters.