API reference

Reward endpoints

Endpoints for listing available rewards and active campaigns.


List rewards

GET /api/v1/rewards

Retrieve the rewards that are currently active, within their availability window, and have remaining stock. This list is not paginated. data is the bare array, unlike every other index endpoint on this API.

Query parameters

ParameterTypeRequiredDefaultDescription
affordablebooleanNofalseReturn only rewards the customer can pay for. Needs identifier; if that resolves to no customer or no wallet, the full catalogue comes back unfiltered
identifierstringNoA loyalty card code or other identifier value, used to read the balance affordable filters on

Response

{
    "data": [
        {
            "id": 1,
            "name": "10% Discount",
            "description": "Get 10% off your next purchase",
            "type": "discount",
            "point_cost": 500,
            "image_url": "string",
            "remaining_stock": 50,
            "total_stock": 0,
            "available_from": "2026-03-01",
            "available_until": "2026-12-31",
            "branches": [
                {
                    "external_id": "shop-1",
                    "name": "Ghent",
                    "type": "physical"
                }
            ]
        }
    ]
}

Response fields

FieldDescription
remaining_stockRedemptions still available. null means unlimited.
total_stockRedemptions the reward started with. null means unlimited.
available_fromYYYY-MM-DD, not a timestamp. These two dates are the exception on this API
available_untilYYYY-MM-DD. null means the reward does not expire.
branchesThe branches the reward may be redeemed at, each { "external_id", "name", "type" }, in the order you keep them. null means every branch.

The type-specific detail of a reward (a discount's value, a free product's reference) is not on this endpoint. It arrives as type_specific_data when the reward is redeemed; see Redemption endpoints.

Filtered by availability

This endpoint only returns rewards that are currently available. Expired, out-of-stock, or inactive rewards are excluded.


List campaigns

GET /api/v1/campaigns

Retrieve a paginated list of campaigns whose status is active: both those already running and those still upcoming.

Query parameters

ParameterTypeRequiredDescription
branchstringNoYour own key for one branch: the external_id you gave it, at most 64 characters. Answers with the campaigns that run there: the ones scoped to that branch, plus every campaign that names no branch at all. Pass none for the campaigns that apply to a purchase recorded without a branch. A key matching none of your branches answers 422 BRANCH_NOT_FOUND rather than the unfiltered list, so a typo cannot pass for a report

Paginate with ?page=. It is read from the query string by the paginator itself, so it never appears in the parameter table above.

Page size is fixed at 15

This endpoint does not honour per_page. Of the list endpoints only GET /products accepts it.

Breaking type change: `multiplier` is now nullable

multiplier was always an integer; it is now null for customer-moment and fixed-points campaigns. recurrence_type and recurrence_config became nullable in the same change. A strict client that types all three as always-present will fail to parse the two campaign families introduced alongside them. Widen the types before consuming this endpoint.

The list is double-wrapped: the outer data is the standard response envelope, and the paginator sits inside it, so campaigns live at data.data.

Response

{
    "data": {
        "data": [
            {
                "id": 1,
                "name": "10% Discount",
                "family": "transaction",
                "moment": null,
                "config": [
                    {
                        "combineerbaar": false,
                        "reward_type": "multiplier",
                        "scope": "whole_purchase"
                    }
                ],
                "version": 1,
                "multiplier": 2,
                "recurrence_type": "days_of_week",
                "recurrence_config": [
                    {
                        "days": "saturday"
                    }
                ],
                "schedule_summary": "Every Saturday and Sunday",
                "starts_at": "2024-03-01",
                "ends_at": "2024-06-30",
                "status": {
                    "value": "active",
                    "label": "Active"
                },
                "min_transaction_amount": 25,
                "branches": [
                    {
                        "external_id": "shop-1",
                        "name": "Ghent",
                        "type": "physical"
                    }
                ],
                "created_at": "2024-02-20T09:15:00+00:00",
                "updated_at": "2024-02-20T09:15:00+00:00"
            }
        ],
        "links": {
            "first": "https://puntjes.app/api/v1/campaigns?page=1",
            "last": "https://puntjes.app/api/v1/campaigns?page=1",
            "prev": null,
            "next": null
        },
        "meta": {
            "current_page": 1,
            "last_page": 1,
            "per_page": 15,
            "total": 2
        }
    }
}

Response fields

FieldTypeDescription
familystringtransaction (triggered by a purchase) or customer_moment (triggered by the customer)
momentstring|nullFor customer_moment: birthday, anniversary, first_purchase, nth_purchase or win_back
configobject|nullFamily-specific settings. See below
versionintegerBumped on every material edit. Stamped onto the points a purchase earns.
multiplierinteger|nullWhole multiple, minimum 2: 2 = 2x, 3 = 3x. null unless this campaign uses a multiplier.
recurrence_typestring|nulldate_range, days_of_week or specific_dates. null when the campaign has no schedule.
recurrence_configobject|nullConfiguration for the recurrence type. null when there is no recurrence.
schedule_summarystringHuman-readable schedule. "Always active" when there is no recurrence. Not translated server-side.
starts_atstringCalendar date, YYYY-MM-DD
ends_atstring|nullCalendar date, YYYY-MM-DD. null means open-ended.
statusobject{ "value": "active", "label": "Active" }. label is English on every request. See below
min_transaction_amountnumber|nullMinimum purchase for the campaign to apply, in euros. See below
branchesarray|nullThe branches this campaign runs at, each { "external_id", "name", "type" }. null means all of them. See below
created_atstring|nullISO 8601 timestamp
updated_atstring|nullISO 8601 timestamp

status.label is always English

There is no request locale on this API and no server-side translation, so label carries the English word whichever language your integration speaks. Map value yourself. It is one of active, inactive or completed, and those three are what the Puntjes portal translates.

min_transaction_amount is in euros, not cents

This is the one field on the API that is not expressed in cents. 25.0 means EUR 25.00, not EUR 0.25. Every other amount (total_amount, unit_price, price_cents and the statistics figures) remains an integer number of cents.

Read the scope from `branches`, not from `config.branch_ids`

branches carries the external_id of every branch the campaign runs at, which is the key your own systems already use. config.branch_ids holds the same scope as Puntjes primary keys: config is passed through as stored, and those ids are meaningless outside the portal. branches is null when the campaign names no branch, which means it runs at every one of them, including purchases recorded without a branch. An empty array means the opposite: the campaign is scoped, and every branch it named has since been deleted, so it now matches nothing.

The config object

For family: "transaction":

KeyDescription
scopewhole_purchase or products
reward_typemultiplier (uses the multiplier field) or fixed_points (uses points)
pointsPoints per matching unit, for fixed_points
max_points_per_purchaseCeiling on what one purchase may earn from this campaign. Absent or null means no cap.
productsFor products scope: a fixed snapshot of { "sku", "name" } pairs
combineerbaartrue when this campaign may stack with others; otherwise only the strongest pays

For family: "customer_moment": the moment's own settings (for example every_n and min_amount on nth_purchase, years on anniversary, months on win_back) plus a gift object describing what the moment hands out.

Campaigns written before this contract

A campaign created by an earlier release may have a null or partial config. Such a row behaves as a whole-purchase, non-combinable campaign with a multiplier. Read the defaults; do not assume the keys are present.

Errors

CodeStatusDescription
BRANCH_NOT_FOUND422branch matches none of your branches, deactivated ones included. The filter is refused rather than ignored, so a misspelled key cannot pass for a report covering every branch