Skip to main content

Getting Started with Promotions

Welcome to the final guide in the series. You've built a catalog, priced it, and taxed it — now let's make it sing with promotions. This guide walks you through creating and managing Promotions on the Hii Retail platform using the Promotion Management API v3. By the end, you'll understand how a promotion is structured, the difference between central and local promotions, and how to create, query, update, and delete them.

Which Promotion API?

Hii Retail has two generations of promotion APIs. The original Promotion Input API / Promotion Query API carry promotions as an XML string for compatibility with legacy POS systems. The Promotion Management API v3 covered in this guide is a fully structured JSON API that replaces them, and is the way to integrate going forward. Build against v3 for all new integrations.

Overview

What is a Promotion?

A Promotion is a rule that reduces the price the customer pays — a campaign price, a percentage off a category, a "buy 2 for the price of 1" bundle, a loyalty-member discount, and so on. You define promotions through the Management API; the Promotion Engine then evaluates them against each basket at the point of sale and returns the calculated discounts to the POS.

You describe what the promotion should do. The engine decides how and when it applies.

┌──────────────────────────────────────────────────────────────────┐
│ Promotion │
│ id · validFrom/validTo · status · conflictResolutionType │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Activators (optional gate — must pass before offers evaluate) │
│ e.g. loyalty, coupon, staff, channel, B2B │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Offer │ │
│ │ Conditions (what must be in the basket) │ │
│ │ e.g. item set "soft drinks", min quantity 2 │ │
│ │ Reward (what the customer gets) │ │
│ │ e.g. 20% off the matched items │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ …or a simpler campaignPrices list: a flat new price per item │
│ │
└──────────────────────────────────────────────────────────────────┘

Key building blocks

ConceptDescription
ActivatorAn optional gate that must be satisfied before any offers are evaluated — loyalty membership, a coupon code, a staff transaction, a sales channel, or a B2B customer. Multiple activators are combined with logical AND.
OfferA condition–reward pair. A promotion can hold several offers.
ConditionWhat must be present in the basket — an item set (matched by category, item ID, brand, etc.) with optional minQuantity/minAmount, or a totalAmount threshold.
RewardWhat the customer gets — a reduced percent or amount, a discounted/package price, or a total-basket discount.
Campaign priceA simpler alternative to offers: a flat new unit price for specific items, with no conditions to evaluate.
conflictResolutionTypeDetermines how this promotion competes with others when several match the same items (see below).

Central vs local promotions

The API exposes three resources. Which one you use depends on who owns the promotion and how its scope is determined.

Promotion Management API v3
┌───────────────────────────┬───────────────────────────────────────┐
│ /promotions │ Central promotions │
│ │ Scope set in the body: │
│ │ businessUnitId OR businessUnitGroupId│
├───────────────────────────┼───────────────────────────────────────┤
│ /local-promotions │ Store-owned promotions │
│ /local-campaigns │ Scope (business unit) is taken from │
│ │ the access token, not the body │
└───────────────────────────┴───────────────────────────────────────┘
  • Central promotions (/promotions) target either a single business unit (businessUnitId) or a whole business unit group (businessUnitGroupId), set in the request body. A central promotion manager may manage any promotion in the tenant.
  • Local promotions and campaigns (/local-promotions, /local-campaigns) apply only to the caller's own store. The business unit is taken authoritatively from the token, never from the body — a store manager can only ever change their own store's promotions.

This guide focuses on central promotions; the local endpoints behave identically except for where the business unit comes from.

Conflict resolution

When more than one promotion matches the same items, the conflictResolutionType decides which wins. The set of types is configurable per tenant, and each type has a defined relationship to every other type. Common types include:

TypeTypical use
CAMPAIGNCampaign prices
SALEStandard sale offers
MIXMATCHMulti-buy / bundle deals
TOTAL_DISCOUNTBasket-total discounts
STAFFStaff discounts
B2BNegotiated B2B agreement prices
Same-type conflicts

Two promotions sharing the same conflictResolutionType always resolve with the Best Deal strategy — the engine applies whichever gives the customer the lower price. You can override the default relationship between different types through Customer Controlled Configuration — see Promotion Configuration at the end of this guide.

Prerequisites

Before you begin, ensure you have:

  1. Business Units created: At least one Business Unit or Business Unit Group to scope promotions to (see Business Units guide)
  2. Items and prices: The items your promotions target should already exist with an active sales price (Items guide, Prices guide)
  3. Authentication token: A valid JWT bearer token whose client holds the appropriate Promotion Engine role (see Authentication below)
We'll keep promoting "Fanta Orange"

This guide continues the running example from earlier in the series: item 5449000051578 (Fanta Orange 500ml, priced at €14.99), in category beverages-soft-drinks, in the Business Unit Group acme-retail-group. If you followed the Items and Prices guides, you can run every example below as-is.

API Base URL

All Promotion Management API requests are sent to:

https://promotion-api.retailsvc.com/api/v3

The full, interactive endpoint reference is published at Promotion Management API v3.

Authentication

Every request must carry a JWT bearer token in the Authorization header:

Roles and permissions

Access is role-based. Rather than granting individual permissions, you assign one of the predefined Promotion Engine roles to your OCMS client (or to IAM users, for interactive access). Each role carries the permissions the API checks per operation:

RoleGrantsScope
promotion-adminCreate, update, delete, and query — across central promotions, local promotions, and local campaignsEverything
promotion-createpro.promotion.createCreate/update central promotions
promotion-viewpro.promotion.queryRead central promotions
local-promotion-adminAll pro.local-promotion.*Manage local promotions
local-promotion-create / local-promotion-viewCreate/update or readLocal promotions
local-campaign-adminAll pro.local-campaign.*Manage local campaigns
local-campaign-create / local-campaign-viewCreate/update or readLocal campaigns

For a back-office integration that manages everything, promotion-admin is the usual choice. The create permission covers both creating and updating a promotion.

Step 1: Create a Campaign Price

The simplest promotion: a flat new price for specific items during a campaign window. No conditions to evaluate. This example runs a summer campaign that drops Fanta Orange from €14.99 to €11.99 across the whole Business Unit Group, so it propagates to every store in the group.

Permission: pro.promotion.create (role promotion-create or promotion-admin).

Required fields

FieldTypeDescription
idstringUnique identifier for this promotion (max 250 characters)
revisionintegerVersion of the entity. Use an epoch timestamp in milliseconds. A later write must use a higher value or it is ignored as outdated.
conflictResolutionTypestringHow this promotion competes with others (e.g. CAMPAIGN)
statusstringACTIVE or DELETED
namestringDisplay name shown in frontend applications
validFromdatetimeWhen the promotion becomes effective (ISO 8601). Time defaults to 00:00:00.000.
businessUnitGroupId or businessUnitIdstringThe scope of the promotion
curl -X POST 'https://promotion-api.retailsvc.com/api/v3/promotions' \
-H 'Authorization: Bearer <your-access-token>' \
-H 'Content-Type: application/json' \
-d '{
"id": "SUMMER-CAMPAIGN-2026",
"revision": 1780272000000,
"businessUnitGroupId": "acme-retail-group",
"conflictResolutionType": "CAMPAIGN",
"status": "ACTIVE",
"name": "Summer Campaign",
"receiptText": "Summer Sale",
"validFrom": "2026-06-01T00:00:00Z",
"validTo": "2026-08-31T23:59:59Z",
"campaignPrices": [
{ "itemId": "5449000051578", "unitPrice": 11.99 }
]
}'

Response

A successful request returns 202 Accepted.

Asynchronous Processing

The Promotion Management API processes requests asynchronously. A 202 Accepted response means your promotion has been queued for processing — it is not instantly queryable or active at the till. Build for eventual consistency, and confirm the result by reading the promotion back (see Step 5) rather than assuming it is live on the next call.

Step 2: Create an Offer-Based Promotion

Most promotions go beyond a flat price. An offer pairs a condition (what must be in the basket) with a reward (what the customer gets). This example gives 20% off every item in the beverages-soft-drinks category.

Permission: pro.promotion.create (role promotion-create or promotion-admin).

Anatomy of an offer

ElementField pathDescription
Offeroffers[]A condition–reward pair. A promotion may hold several.
Item setoffers[].conditions[].itemSet.itemSetIdA named set of matched items. The reward refers back to it.
Filteroffers[].conditions[].itemSet.filters[]What goes in the set — itemCategories, itemIds, brands, colors, sizes, seasons, modelIds, …
Quantity / amount gateoffers[].conditions[].itemSet.minQuantity / minAmountOptional threshold the set must reach before the offer triggers
Rewardoffers[].rewardWhat the customer gets — e.g. reducedPercent, reducedAmount, discountedPrice
Reward targetoffers[].reward.*.targetItemSets[]Which item set(s) the reward applies to, by itemSetId
curl -X POST 'https://promotion-api.retailsvc.com/api/v3/promotions' \
-H 'Authorization: Bearer <your-access-token>' \
-H 'Content-Type: application/json' \
-d '{
"id": "SOFT-DRINKS-WEEK-2026",
"revision": 1748736000000,
"businessUnitGroupId": "acme-retail-group",
"conflictResolutionType": "SALE",
"status": "ACTIVE",
"name": "Soft Drinks Week - 20% off",
"receiptText": "Soft Drinks -20%",
"validFrom": "2026-06-10T00:00:00Z",
"validTo": "2026-06-16T23:59:59Z",
"offers": [
{
"id": "offer-soft-drinks-20",
"conditions": [
{
"itemSet": {
"itemSetId": "SET-SOFT-DRINKS",
"filters": [
{ "itemCategories": ["beverages-soft-drinks"] }
]
}
}
],
"reward": {
"reducedPercent": {
"percent": 20.0,
"targetItemSets": ["SET-SOFT-DRINKS"]
}
}
}
]
}'
Targeting items

Combine multiple conditions within an offer to require several item sets at once (e.g. "5 bags of cement AND 1 mixer"), and use minQuantity/minAmount to gate on quantity or spend. See the Promotion Management API reference for the full filter and reward catalogue.

Code examples below use cURL

Steps 1 and 2 show the full request in all five languages. The remaining operations differ only by URL and payload, so they're shown as cURL — translate them with the same client setup you used above.

Step 3: Create a Local Promotion

Local promotions apply only to the store that owns them. The request body is the same shape as a central promotion, but you send it to /local-promotions and you omit businessUnitId/businessUnitGroupId — the business unit is taken from the access token.

Permission: pro.local-promotion.create (role local-promotion-create, local-promotion-admin, or promotion-admin). The store the promotion applies to is the one in your token.

curl -X POST 'https://promotion-api.retailsvc.com/api/v3/local-promotions' \
-H 'Authorization: Bearer <your-access-token>' \
-H 'Content-Type: application/json' \
-d '{
"id": "STORE-WEEKEND-SOFT-DRINKS",
"revision": 1748736000000,
"conflictResolutionType": "SALE",
"status": "ACTIVE",
"name": "Weekend 10% off soft drinks",
"validFrom": "2026-06-13T00:00:00Z",
"validTo": "2026-06-14T23:59:59Z",
"offers": [
{
"id": "offer-weekend-10",
"conditions": [
{ "itemSet": { "itemSetId": "SET-SOFT-DRINKS", "filters": [{ "itemCategories": ["beverages-soft-drinks"] }] } }
],
"reward": { "reducedPercent": { "percent": 10.0, "targetItemSets": ["SET-SOFT-DRINKS"] } }
}
]
}'
Local campaigns

Local campaign prices work the same way — send a campaignPrices payload to /local-campaigns. The store scope still comes from the token.

Step 4: List and Query Promotions

GET /promotions returns promotions for the tenant. Use the filter query parameter to narrow the result with an RSQL expression. Combine predicates with ; (AND) and , (OR), and group with parentheses.

Permission: pro.promotion.query (role promotion-view or promotion-admin).

# All active promotions in a group that are valid from June 2026 onwards
curl -G 'https://promotion-api.retailsvc.com/api/v3/promotions' \
-H 'Authorization: Bearer <your-access-token>' \
--data-urlencode 'filter=status==ACTIVE;businessUnitGroupId==acme-retail-group;validFrom=ge=2026-06-01T00:00:00Z' \
--data-urlencode 'limit=200'

Common searchable fields include id, name, reportingId, businessUnitId, businessUnitGroupId, validFrom, validTo, status, and conflictResolutionType. See the API reference for the full list of fields and operators.

Response and pagination

The response wraps the results in a metadata object plus an items array:

{
"metadata": {
"nextCursor": "eyJpZCI6IlNVTU1FUi1DQU1QQUlHTi0yMDI2In0",
"hasNextPage": true
},
"items": [
{ "id": "SUMMER-CAMPAIGN-2026", "status": "ACTIVE", "...": "..." },
{ "id": "SOFT-DRINKS-WEEK-2026", "status": "ACTIVE", "...": "..." }
]
}

Results are paged. limit defaults to 200 and cannot exceed 200. When metadata.hasNextPage is true, pass metadata.nextCursor back as the cursor parameter to fetch the next page, and repeat until hasNextPage is false:

curl -G 'https://promotion-api.retailsvc.com/api/v3/promotions' \
-H 'Authorization: Bearer <your-access-token>' \
--data-urlencode 'filter=status==ACTIVE;businessUnitGroupId==acme-retail-group' \
--data-urlencode 'cursor=eyJpZCI6IlNVTU1FUi1DQU1QQUlHTi0yMDI2In0'
Don't stop at the first page

If you only read the first response you'll silently miss everything beyond the 200-item limit. Always loop on hasNextPage when you expect more than a couple of hundred promotions.

Step 5: Verify and Fetch a Promotion

Because writes are asynchronous, reading a promotion back by its id is how you confirm processing succeeded — there is no push notification when a promotion finishes (or fails) processing. After a create or update, poll this endpoint until the promotion appears with the expected revision and status.

Permission: pro.promotion.query (role promotion-view or promotion-admin).

curl 'https://promotion-api.retailsvc.com/api/v3/promotions/SUMMER-CAMPAIGN-2026' \
-H 'Authorization: Bearer <your-access-token>'

A 200 OK with the promotion body confirms it landed; a 404 Not Found means it either hasn't finished processing yet or was rejected — wait briefly and retry, and double-check the payload if it never appears.

Step 6: Update a Promotion

There is no separate update endpoint. To change a promotion, POST the same id again with a higher revision — the API performs an upsert and the newer revision replaces the older one. A write whose revision is lower than the stored value is treated as outdated and ignored.

Permission: pro.promotion.create (role promotion-create or promotion-admin).

curl -X POST 'https://promotion-api.retailsvc.com/api/v3/promotions' \
-H 'Authorization: Bearer <your-access-token>' \
-H 'Content-Type: application/json' \
-d '{
"id": "SUMMER-CAMPAIGN-2026",
"revision": 1782864000000,
"businessUnitGroupId": "acme-retail-group",
"conflictResolutionType": "CAMPAIGN",
"status": "ACTIVE",
"name": "Summer Campaign (extended)",
"validFrom": "2026-06-01T00:00:00Z",
"validTo": "2026-09-15T23:59:59Z",
"campaignPrices": [
{ "itemId": "5449000051578", "unitPrice": 10.99 }
]
}'
Use epoch milliseconds for revision

If you set revision to the current time in epoch milliseconds on every write, you get correct ordering for free — each update is naturally higher than the last. If you omit revision, Hii Retail assigns one for you, but providing it yourself gives you deterministic versioning. (Note 1782864000000 above is later than the 1780272000000 used at create time.)

Step 7: Delete a Promotion

Delete a promotion by its id:

Permission: pro.promotion.delete (role promotion-admin).

curl -X DELETE 'https://promotion-api.retailsvc.com/api/v3/promotions/SUMMER-CAMPAIGN-2026' \
-H 'Authorization: Bearer <your-access-token>'

Like create and update, deletion is asynchronous: the call returns 202 Accepted to confirm the request was queued, and the promotion stops applying once processing completes. Read it back (Step 5) to confirm it's gone.

Promotion Configuration

Beyond individual promotions, you can tune how the engine resolves conflicts between conflict-resolution types for your tenant. This is done through Customer Controlled Configuration using the configuration kind pro.configuration.v1.

The configuration describes the relationships between conflict resolution types, letting you override the default Best Deal behaviour for a pair of conflicting promotions with:

  • LEFT_BEFORE_RIGHT — apply the left type first, then the right
  • LEFT_DISQUALIFIES_RIGHT — if the left type applies, the right is not applied at all

"Left" and "right" refer to the conflict resolution types named in the promotion definitions. The JSON schema is published in the Hii Retail JSON Schema Registry. For how to submit customer-controlled configuration, see the Customer Controlled Configuration documentation.

Testing

You have a test tenant alongside your production tenant. The test environment lives on the retailsvc-test.com domains — this is consistent across all Hii Retail services. To test, swap the hosts:

ProductionTest
Token endpointhttps://auth.retailsvc.com/oauth2/tokenhttps://auth.retailsvc-test.com/oauth2/token
Management APIhttps://promotion-api.retailsvc.com/api/v3https://promotion-api.retailsvc-test.com/api/v3

Next Steps

Now that you can manage promotions, you can:

  1. Add activators: Gate promotions behind loyalty membership, coupons, staff transactions, or sales channels
  2. Build multi-buy deals: Use multiple conditions and quantityLimit to create bundles and "buy X get Y" offers
  3. Tune conflict resolution: Submit a pro.configuration.v1 configuration to control how overlapping promotions interact
  4. Automate: Sync promotions from your campaign-planning or ERP system into the Management API

Done! You've completed the Getting Started series. From here, explore the Promotion Management API v3 Reference for the full set of reward types, activators, and item filters.