> For the complete documentation index, see [llms.txt](https://developers.portos.sk/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.portos.sk/integrations/rewards-api.md).

# Rewards API

## Glossary

| Term            | Definition                                                                                                |
| --------------- | --------------------------------------------------------------------------------------------------------- |
| PORTOS          | The cash register solution developed by Nine Digit, s.r.o.                                                |
| External system | The 3rd party system (such as e-shop or food delivery platform) from which rewards are fetched to PORTOS. |
| Venue           | E.g. store or restaurant.                                                                                 |
| Cashier         | The Venue personnel.                                                                                      |
| Customer        | Person who purchases the goods.                                                                           |
| Reward          | The coupon or discount that customer can be rewarded with.                                                |
| Product         | The product that is being purchased by customer.                                                          |

## How it works

The *Rewards API* is a mechanism that allows importing rewards (such as coupons, deals or discounts) to the PORTOS system.

The PORTOS system acts as an "client" initiating HTTP request targeting the *external system*, that acts as an "server" and handles the following requests:

1. Fetch available rewards
2. Claim reward(s)

The PORTOS system expects that responses from *external system* follows data format described below.&#x20;

## Fetch available rewards

<mark style="color:blue;">`GET`</mark> `{external-system}{get-rewards-url}`

The PORTOS system will fetch available rewards from the external system, by initiating GET HTTP request to the external system address. External system returns response of type [`CustomerRewards`](#customerrewards), that contains the collection of rewards, that customer can be rewarded with.

### Query Parameters

| Name                                      | Type   | Description                                                                                   |
| ----------------------------------------- | ------ | --------------------------------------------------------------------------------------------- |
| version<mark style="color:red;">\*</mark> | string | The version of the exchange protocol. For versions 1.X, value is set to `1`.                  |
| key<mark style="color:red;">\*</mark>     | string | The API key.                                                                                  |
| customerId                                | string | The customer identifier. E.g. the serial number of the loyalty card, if provided by customer. |

### Headers

| Name            | Type   | Description                                                                           |
| --------------- | ------ | ------------------------------------------------------------------------------------- |
| Accept-Language | string | Specifies the desired localization for receiving messages from the API. E.g. `sk-SK`. |

{% tabs %}
{% tab title="200 " %}
{% code overflow="wrap" %}

```json
{
    "customer":
    {
        "displayName": "John Doe",
        "email": "example@portos.sk",
        "points": 1281
    },
    "maxApplicableRewards": null,
    "rewards":
    [
        {
            "id": "dda658c8-7ca1-4993-9212-f1bcd992e638",
            "title": "Najlacnejšia pizza za jeden cent!",
            "description": "Získajte najlacnejšiu pizzu za jeden cent. Zľava platí pri nákupe nad 20 eur.",
            "conditions":
            [
                {
                    "purchase":
                    {
                        "minAmountIncludingVat": 20
                    }
                }
            ],
            "items":
            [
                {
                    "target": "purchaseItem",
                    "discountType": "absolute",
                    "discountAmount": 0.01,
                    "purchaseItemLookupMode": "cheapest",
                    "purchaseItemFilter":
                    {
                        "articleCategoryLabels":
                        [
                            "PIZ"
                        ],
                        "maxQuantity": 1
                    }
                }
            ]
        },
        {
            "id": "0214d09d-844f-4677-a449-829ddbf5d1cc",
            "title": "Vymeňte 1000 bodov za 5 eurovú zľavu",
            "items":
            [
                {
                    "target": "purchase",
                    "discountType": "absolute",
                    "discountAmount": 5
                }
            ],
            "priceInPoints": 1000
        }
    ]
}
```

{% endcode %}
{% endtab %}

{% tab title="401 The bearer authorization token is invalid." %}
{% code overflow="wrap" %}

```json
{
  "message": "This is the optional message describing the error."
}
```

{% endcode %}
{% endtab %}

{% tab title="403 The orders cannot be fetched. The reason may be provided in body." %}
{% code overflow="wrap" %}

```json
{
  "message": "A loyalty card is required for receiving rewards.",
  "code": "CUSTOMER_ID_REQUIRED"
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Claim reward(s)

<mark style="color:green;">`POST`</mark> `{external-system}{claim-rewards-url}`

After the reward is confirmed by Cashier trough the PORTOS cash register system user interface, the PORTOS system will initiate the POST HTTP request to the external system, to mark reward(s) as claimed.

### Query Parameters

| Name                                      | Type   | Description                                                                  |
| ----------------------------------------- | ------ | ---------------------------------------------------------------------------- |
| version<mark style="color:red;">\*</mark> | string | The version of the exchange protocol. For versions 1.X, value is set to `1`. |
| key<mark style="color:red;">\*</mark>     | string | The API key.                                                                 |

### Headers

| Name            | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Accept-Language | string | Specifies the desired localization for receiving messages from the API. E.g. `sk-SK`.                                                                                                                                                                                                                                                                                                                                                                      |
| Idempotency-Key | string | Optional. A unique key identifying this claim attempt (e.g. a UUID). If a claim with the same key has already been processed, the server returns the original response without applying the claim again, so the request is safe to retry. Reuse the same key only when retrying the exact same claim; reusing a key with a different set of `rewardIds` is rejected with HTTP `422`. Requests without this header are processed normally (non-idempotent). |

### Request Body

| Name                                        | Type      | Description                                         |
| ------------------------------------------- | --------- | --------------------------------------------------- |
| rewardIds<mark style="color:red;">\*</mark> | string\[] | Non-empty collection of claimed reward identifiers. |

{% tabs %}
{% tab title="200 The reward usage has been acknowledged by the external system." %}
{% code overflow="wrap" %}

```json
/* No response body is required by PORTOS. */
```

{% endcode %}
{% endtab %}

{% tab title="401 The bearer authorization token is invalid." %}

```json
{
  "message": "This is the optional message describing the error."
}
```

{% endtab %}

{% tab title="403 Error when marking reward as used. E.g. reward has already been used." %}

```json
{
  "message": "This reward is currently not available.",
  "code": "REWARD_USAGE_LIMIT_EXCEEDED",
  "rewardId": "29e61e3b-34ae-4c1f-9bd8-f4ef9925a1f8"
}
```

{% endtab %}

{% tab title="422 Unprocessable Entity" %}
The `Idempotency-Key` was already used with a different claim request.
{% endtab %}
{% endtabs %}

### Idempotency

The Portos POS application may retry a claim request whose response it did not receive. For example after a network timeout, when it cannot tell whether the original request was processed. To let it do so safely, the client can attach an **`Idempotency-Key`** header (see *Headers*) that uniquely identifies a single claim attempt.

Supporting idempotency is **optional but recommended**: without it, a retried claim is processed a second time and the customer's rewards may be claimed twice.

#### Expected server behaviour

When a claim request carries an `Idempotency-Key`, the server should:

1. **Look the key up**, scoped to the API key it was authenticated with. Keys from different API keys are independent and must never collide.
2. **First time the key is seen:** process the claim normally and **store the resulting response** (both the HTTP status and the body) together with the key.
3. **Key seen again with the same request:** return the **stored response** and do **not** apply the claim again. The response must be identical to the original (same status and body).
4. **Key seen again with a different request** (a different set of `rewardIds`): reject it with **`422 Unprocessable Entity`**. This signals that the key was reused incorrectly and protects against accidental key collisions.

Only **completed** requests should be stored. If the claim cannot be completed (e.g. the server returns `5xx` or the request never finishes) the key must **not** be recorded, so that a later retry is processed rather than replaying an outcome that never happened.

#### Retention

Stored keys should be retained for a bounded period; **24 hours** is sufficient, as the client only retries within a short window of the original attempt. Keys may be discarded after that.

## Configuration

To communicate with external system properly, the external system administrators must provide values stated below to the venue owners or PORTOS support.

| Value                               | Type     | Description                                                                                                             |
| ----------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| External system "get rewards" URL   | `string` | The URL hosted by external system, that will be requested by PORTOS to fetch rewards.                                   |
| External system "claim rewards" URL | `string` | The URL hosted by external system, that will be requested by PORTOS to claim rewards.                                   |
| API key                             | `string` | The venue-specific API key (authorization token) that will be placed in the `key` query parameter of all HTTP requests. |

## Data model

The rewards API data model is described below.

{% hint style="info" %}
All prices include taxes.
{% endhint %}

### CustomerRewards

The collection of rewards available for given customer.

<table><thead><tr><th>Name</th><th width="171">Type</th><th width="102" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><strong><code>rewards</code></strong></td><td><a href="#reward"><code>Reward</code></a><code>[]</code></td><td>true</td><td>The collection of available rewards.</td></tr><tr><td>customer</td><td><a href="#customer"><code>Customer</code></a></td><td>false</td><td>The information about customer, if available.</td></tr><tr><td><code>maxApplicableRewards</code></td><td><code>int</code></td><td>false</td><td>The maximum number of applicable rewards per given purchase.</td></tr></tbody></table>

### Customer

Information about customer.

<table><thead><tr><th>Name</th><th width="171">Type</th><th width="102" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><strong><code>displayName</code></strong></td><td><code>string</code></td><td>true</td><td>The display name of customer or customer loyalty account.</td></tr><tr><td><strong><code>points</code></strong></td><td><code>number</code></td><td>true</td><td>Represents number of loyalty points associated with customer account.</td></tr><tr><td><code>firstName</code></td><td><code>string</code></td><td>false</td><td>First name.</td></tr><tr><td><code>lastName</code></td><td><code>string</code></td><td>false</td><td>Last name.</td></tr><tr><td><code>email</code></td><td><code>string</code></td><td>false</td><td>E-mail address that can be used when sending receipt in e-mail form.</td></tr><tr><td><code>maxApplicableRewards</code></td><td><code>int</code></td><td>false</td><td>The maximum number of rewards that can be applied per purchase. The rewards can also be  limited by specifying available customers loyalty points and price in points of reward.</td></tr></tbody></table>

### Reward

Represents the reward.

<table><thead><tr><th>Name</th><th width="176">Type</th><th width="102" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><strong><code>id</code></strong></td><td><code>string</code></td><td>true</td><td>Unique reward identifier.</td></tr><tr><td><strong><code>title</code></strong></td><td><code>string</code></td><td>true</td><td>Reward title.</td></tr><tr><td><code>description</code></td><td><code>string</code></td><td>false</td><td>Short reward description.</td></tr><tr><td><code>conditions</code></td><td><a href="#rewardcondition"><code>RewardCondition</code></a><code>[]</code></td><td>false</td><td>Conditions that need to be fulfilled in order for the customer to claim the reward.</td></tr><tr><td><strong><code>items</code></strong></td><td><a href="#rewarditem"><code>RewardItem</code></a><code>[]</code></td><td>true</td><td>Collection of one or more items associated with given reward.</td></tr><tr><td><code>activationDate</code></td><td><code>string</code></td><td>false</td><td>If specified, the reward becomes applicable only after the specified date and time in ISO_8601 format, expressed in UTC.</td></tr><tr><td><code>expirationDate</code></td><td><code>string</code></td><td>false</td><td>If specified, the reward is applicable only before the specified date and time in ISO_8601 format, expressed in UTC.</td></tr><tr><td><code>minPurchaseAmountIncludingVat</code></td><td><code>number</code></td><td>false</td><td>If specified, represents the minimum purchase amount required to claim the reward.</td></tr><tr><td><code>priceInPoints</code></td><td><code>number</code></td><td>false</td><td>Specifies number of loyalty points subtracted from customer's loyalty account once the reward is claimed.</td></tr><tr><td><code>remainingUsage</code></td><td><code>number</code></td><td>false</td><td>Specifies maximum number of times can this reward be claimed by any customer.</td></tr><tr><td><code>remainingCustomerUsage</code></td><td><code>number</code></td><td>false</td><td>Specifies maximum number of times can this reward be claimed by given customer.</td></tr></tbody></table>

### RewardCondition

<table><thead><tr><th width="237">Name</th><th width="197">Type</th><th width="102" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>purchase</code></td><td><a href="#rewardpurchasecondition"><code>RewardPurchaseCondition</code></a></td><td>false</td><td>Optional conditions related to purchase.</td></tr></tbody></table>

### RewardPurchaseCondition

<table><thead><tr><th>Name</th><th width="176">Type</th><th width="102" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>minAmountIncludingVat</code></td><td><code>number</code></td><td>false</td><td>If specified, represents the minimum purchase amount required to claim the reward.</td></tr></tbody></table>

### RewardItem

Single reward item.

<table><thead><tr><th>Name</th><th width="171">Type</th><th width="102" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><strong><code>target</code></strong></td><td><code>string</code></td><td>true</td><td>One of <a href="#rewarditemtarget"><code>RewardItemTarget</code></a>.</td></tr><tr><td><code>productFilter</code></td><td><a href="#rewarditemproductfilter"><code>RewardItemProductFilter</code></a></td><td>false</td><td>Required, when <code>target</code> is <code>product</code>. Must be null otherwise.</td></tr><tr><td><code>purchaseItemFilter</code></td><td><a href="#rewarditempurchaseitemfilter"><code>RewardItemPurchaseItemFilter</code></a></td><td>false</td><td>Required, when <code>target</code> is <code>purchaseItem</code>. Must be null otherwise.</td></tr><tr><td><code>purchaseItemLookupMode</code></td><td><code>string</code></td><td>false</td><td>One of <code>PurchaseItemLookupMode.</code> when <code>target</code> is <code>purchaseItem</code>. Must be null otherwise.</td></tr><tr><td><strong><code>discountType</code></strong></td><td><code>string</code></td><td>true</td><td>One of <a href="#rewarditemdiscounttype"><code>RewardItemDiscountType</code></a>. If <code>target</code> is equal to <code>purchase</code>, value must be <code>prcentage</code> or <code>absolute</code>.</td></tr><tr><td><code>discountAmount</code></td><td><code>number</code></td><td>false</td><td>Positive, non-zero number. Minimal value is <code>0.01</code>. Required, when <code>discountType</code> is <code>absolute</code> or <code>relative</code>. Must be null otherwise. </td></tr><tr><td><code>discountRate</code></td><td><code>number</code></td><td>false</td><td>Positive, non-zero number, with value between <code>0.01</code> and <code>100</code>. Required, when <code>discountType</code> is <code>percentage</code>.  Must be null otherwise.</td></tr></tbody></table>

### RewardItemDiscountType

List of known discount types:

<table><thead><tr><th width="205">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>percentage</code></td><td>Reward provides a percentage discount. </td></tr><tr><td><code>absolute</code></td><td>Reward provides a discount that is specified by a fixed or absolute value rather than a percentage.</td></tr><tr><td><code>relative</code></td><td>Reward provides a discount by subtracting a specific value from the original retail price of a product or service.</td></tr></tbody></table>

### RewardItemTarget

The target that reward item applies to.

<table><thead><tr><th width="210">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>purchaseItem</code></td><td>Reward provides a discount to single purchase item. Purchase item can be specified by <a href="#rewarditempurchaseitemfilter"><code>RewardItemPurchaseItemFilter</code></a>. If no suitable product is found in the purchase, reward is not applied.</td></tr><tr><td><code>product</code></td><td>Reward provides a discount for a certain product, specified by <a href="#rewarditemproductfilter"><code>RewardItemProductFilter</code></a>. If given product is not initially included in the purchase, it will be added to the purchase automatically, so that the discount can be applied.</td></tr><tr><td><code>purchase</code></td><td>Reward applies a discount to entire purchase</td></tr></tbody></table>

### RewardItemProductFilter

Model that uniquely identifies product.

<table><thead><tr><th width="199">Name</th><th width="171">Type</th><th width="102" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>pluId</code></td><td><a href="/pages/uaghKxVt9eOU4jW7e9Ht#pluid"><code>PluId</code></a></td><td>false</td><td>At least one of identifier fields (<code>pluId</code> or <code>id</code>) must be specified.</td></tr><tr><td><code>id</code></td><td><code>string</code></td><td>false</td><td>At least one of identifier fields (<code>pluId</code> or <code>id</code>) must be specified.</td></tr></tbody></table>

### RewardItemPurchaseItemFilter

Model that selects purchase item.

<table><thead><tr><th width="199">Name</th><th width="171">Type</th><th width="102" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>pluIds</code></td><td><a href="/pages/uaghKxVt9eOU4jW7e9Ht#pluid"><code>PluId</code></a><code>[]</code></td><td>false</td><td><code>TicketItem.Plu.Code</code> and <code>TicketItem.Plu.StockName</code>must be equal to one of provided values, if value is not null or empty.</td></tr><tr><td><code>articleCategoryLabels</code></td><td><code>string[]</code></td><td>false</td><td><code>TicketItem.Plu.ArticleCategoryLabel</code> must be equal to one of provided values, if value is not null or empty.</td></tr><tr><td><code>minUnitPriceIncludingVat</code></td><td><code>number</code></td><td>false</td><td>If specified, <code>TicketItem.UnitPriceIncludingVat</code> must be greater than or equal to provided value.</td></tr><tr><td><code>maxUnitPriceIncludingVat</code></td><td><code>number</code></td><td>false</td><td>If specified, <code>TicketItem.UnitPriceIncludingVat</code> must be less than or equal to provided value.</td></tr><tr><td><code>minQuantity</code></td><td><code>number</code></td><td>false</td><td>If specified, <code>TicketItem.Quantity</code> must be greater than or equal to provided value.</td></tr><tr><td><code>maxQuantity</code></td><td><code>number</code></td><td>false</td><td>If specified, <code>TicketItem.Quantity</code> must be less than or equal to provided value.</td></tr></tbody></table>

### PurchaseItemLookupMode

Specifies, what strategy is used to look up single purchase item among all items in purchase, that reward will be applied to.&#x20;

<table><thead><tr><th width="205">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>cheapest</code></td><td>Prefers least expensive product, based on it's unit price.</td></tr><tr><td><code>mostExpensive</code></td><td>Prefers most expensive product, based on it's unit price.</td></tr></tbody></table>

## Error models

### Error

Error model received for non-2XX HTTP statuses.

<table><thead><tr><th>Name</th><th width="171">Type</th><th width="102" data-type="checkbox">Required</th><th>Description</th></tr></thead><tbody><tr><td><strong><code>message</code></strong></td><td><code>string</code></td><td>true</td><td>Human readable message describing the error.</td></tr><tr><td><code>rewardId</code></td><td><code>string</code></td><td>false</td><td>Id of <a href="#reward"><code>Reward</code></a>, if error is reward-specific.</td></tr><tr><td><code>code</code></td><td><code>string</code></td><td>false</td><td>Available, if error matches one of <a href="#errorcode">known error codes</a>. </td></tr></tbody></table>

### ErrorCode

The third-party server can return an [`Error`](#error) with one of the following codes:

<table><thead><tr><th width="258.3333333333333">Error code</th><th>Description</th></tr></thead><tbody><tr><td><code>CUSTOMER_ID_REQUIRED</code></td><td>Should be used when PORTOS attempts to fetch rewards without providing loyalty card id, but loyalty card is required by the third-party server.</td></tr><tr><td><code>UNKNOWN_CUSTOMER_ID</code></td><td>Customer loyalty card is not recognized.</td></tr><tr><td><code>REWARD_NOT_FOUND</code></td><td>Reward is not found.</td></tr><tr><td><code>REWARD_NOT_AVAILABLE</code></td><td>Reward is currently not available. E.g. current date and time does not match with <a href="#reward"><code>Rewards</code></a> <code>ActivationDate</code> and <code>ExpirationDate</code>.</td></tr><tr><td><code>REWARD_USAGE_LIMIT_EXCEEDED</code></td><td>The reward usage limit has been exceeded.</td></tr><tr><td><code>REWARD_CUSTOMER_USAGE_LIMIT_EXCEEDED</code></td><td>The reward usage limit has been exceeded for the specified customer.</td></tr><tr><td><code>INSSUFICIENT_LOYALTY_POINTS</code></td><td>The customer lacks sufficient loyalty points to claim the reward.</td></tr></tbody></table>

## Versioning

This API uses [semantic versioning](https://semver.org/). Given a version number `MAJOR.MINOR`, increment the:

* MAJOR version: incompatible API changes are introduced.
* MINOR version: functionality is added in a backward compatible manner.

## Changelog

### Version 1.0 (2026-07-17)

Added:

* Optional idempotency for the claim route through the `Idempotency-Key` request header, so a client can safely retry a claim whose response was lost without claiming the rewards twice. The header is optional and backward compatible. Existing integrations are unaffected.

Changed:

* The protocol has been declared stable.

### Version 1.0-beta.2 (2023-05-29)

* Added:
  * Accept-Language header added to HTTP requests.
* Changed:
  * `RewardRule` renamed to `RewardCondition`.

### Version 1.0-beta.1 (2023-05-26)

* Initial version.
