API-referentie

Beloningen-endpoints

Met deze endpoints haal je de beschikbare beloningen en de actieve campagnes op.


Beloningen weergeven

GET /api/v1/rewards

Haal elke beloning op met de status actief, de goedkoopste eerst. In de lijst staan ook beloningen waarvan de datums voorbij zijn, beloningen die nog niet begonnen zijn en uitverkochte beloningen. Voor je er een aanbiedt, leg je de datum van vandaag naast available_from en available_until, en kijk je of remaining_stock boven 0 ligt, tenzij is_unlimited true is. Deze lijst is niet gepagineerd: data is de kale array, anders dan bij elk ander lijst-endpoint op deze API.

Een beloning kan ook vastleggen hoe vaak één klant ze krijgt. Voor je een beloning aanbiedt waarvan max_redemptions_per_customer ingevuld is, stuur je de identifier van de klant mee en kijk je of customer_redemptions onder dat getal ligt. POST /redemptions weigert de volgende met REDEMPTION_LIMIT_REACHED.

Query parameters

ParameterTypeVerplichtStandaardBeschrijving
affordablebooleanNeefalseGeeft alleen de beloningen terug die de klant kan betalen. Dit heeft identifier nodig: hoort daar geen klant of geen wallet bij, dan krijg je de hele catalogus ongefilterd terug
identifierstringNee—Een klantenkaartcode of een andere identifier. Ze wijst de klant aan van wie customer_redemptions de inwisselingen telt, en met affordable ook het saldo waarop die filter kijkt. Werkt ook zonder affordable

Response

{
    "data": [
        {
            "id": 1,
            "name": "10% Discount",
            "description": "Get 10% off your next purchase",
            "type": "discount",
            "point_cost": 500,
            "payment_amount": 0,
            "image_url": null,
            "remaining_stock": 50,
            "total_stock": 100,
            "available_from": "2026-03-01",
            "available_until": "2026-12-31",
            "branches": [
                {
                    "external_id": "shop-1",
                    "name": "Gent",
                    "type": "physical"
                }
            ],
            "product_reference": null,
            "is_unlimited": false,
            "discount_type": "percentage",
            "discount_value": 10,
            "max_redemptions_per_customer": 2,
            "customer_redemptions": null
        }
    ]
}

Responsevelden

VeldBeschrijving
payment_amountWat de kassa bovenop de punten aanrekent, in eurocenten. 0 betekent alleen punten, en bij een korting is het altijd 0.
remaining_stockInwisselingen die nog over zijn. 0 bij een beloning zonder limiet: kijk eerst naar is_unlimited.
total_stockAantal inwisselingen waarmee de beloning begon. null betekent onbeperkt.
available_fromYYYY-MM-DD, geen timestamp. Deze twee datums zijn de uitzondering op deze API
available_untilYYYY-MM-DD. null betekent dat de beloning niet verloopt.
branchesDe filialen waar de beloning kan worden ingewisseld, elk { "external_id", "name", "type" }, in de volgorde die jij aanhoudt. null betekent elk filiaal.
product_referenceHet artikelnummer van het product waar de beloning over gaat. Bij een discount is dat het ene product waarvan de korting afgaat; null betekent de hele aankoop. Bij een free_product is het het product dat je meegeeft; null betekent dat de winkel geen artikelnummer opgaf. Kijk eerst naar type.
is_unlimitedtrue als de beloning geen voorraadlimiet heeft. total_stock is dan null en remaining_stock is 0.
discount_typepercentage of fixed_amount bij een discount, dezelfde waarden als type_specific_data.discount_type van een inwisseling. null bij een free_product.
discount_valueHoe groot de korting is, in de eenheid van type_specific_data.discount_value: een percentage als geheel getal (10 is 10%), een vast bedrag in eurocenten (500 is € 5,00). null bij een free_product.
max_redemptions_per_customerHoe vaak één klant deze beloning kan inwisselen. null betekent geen grens. Een inwisseling die de winkel annuleerde, telt niet mee.
customer_redemptionsHoe vaak de klant van identifier deze beloning inwisselde, zonder de geannuleerde. null als je geen identifier meestuurde of niemand erbij hoort.

Met product_reference, discount_type en discount_value controleer je vóór de inwisseling: staat het product van een korting op één product wel op de open verkoop? Mag er op deze verkoop een percentage af, of alleen een vast bedrag? Reken de beloning af met type_specific_data van de inwisseling, niet met deze lijst: de inwisseling bewaart de waarden van het moment zelf, en de winkel kan de beloning later nog aanpassen; zie Verzilvering-endpoints.

Alleen gefilterd op status

Een inactieve beloning blijft eruit. Een beloning buiten haar datums of een uitverkochte beloning staat er wel in, en POST /redemptions weigert ze met REWARD_UNAVAILABLE of OUT_OF_STOCK.

De datums van een beloning zijn UTC-dagen, dus vergelijk je ze met de datum van vandaag in UTC: een beloning met available_until vandaag kun je inwisselen tot middernacht UTC, dat is 01:00 of 02:00 in Brussel. De datums van een campagne zijn Brusselse dagen. Bij elk veld staat welke kalender het gebruikt.


Campagnes weergeven

GET /api/v1/campaigns

Haal een gepagineerde lijst op van campagnes met de status actief: lopende, komende en afgelopen campagnes. Een campagne waarvan de einddatum voorbij is, houdt de status active en blijft in deze lijst. Voor je er een als lopend toont, leg je de datum van vandaag in Brussel naast starts_at en ends_at: een campagne geldt van de dag van starts_at tot en met de dag van ends_at, volgens de Brusselse kalender (Europe/Brussels), niet volgens UTC-dagen.

Query parameters

ParameterTypeVerplichtStandaardBeschrijving
branchstringNee—Je eigen sleutel voor één filiaal: de external_id die je het gaf, maximaal 64 tekens. Je krijgt de campagnes terug die daar lopen, dus de campagnes die aan dat filiaal hangen plus elke campagne die geen enkel filiaal noemt. Gebruik none voor de campagnes die gelden bij een aankoop zonder filiaal. Hoort de sleutel bij geen enkel filiaal van jou, dan volgt 422 BRANCH_NOT_FOUND in plaats van de ongefilterde lijst, zodat een typfout niet voor een rapport kan doorgaan. Een lege branch geeft hetzelfde antwoord; laat de parameter weg voor alle filialen
pageintegerNee1Welke pagina je terugkrijgt. Een waarde onder 1, of een waarde die geen geheel getal is, geeft pagina 1

De paginagrootte ligt vast op 15

Dit endpoint negeert per_page. Van de lijst-endpoints accepteert alleen GET /products die parameter.

Breaking change: `multiplier` is nu nullable

multiplier was altijd een geheel getal. Hij is nu null bij campagnes rond een klantmoment en bij campagnes met vaste punten. recurrence_type en recurrence_config werden in dezelfde wijziging nullable. Typeert je client alle drie als altijd aanwezig, dan kan hij de twee campagnefamilies die er toen bij kwamen niet lezen. Verbreed die types voordat je dit endpoint gebruikt.

De lijst is dubbel verpakt: de buitenste data is de standaard response-envelope, met daarin de paginator. Campagnes staan dus op data.data.

Response

{
    "data": {
        "data": [
            {
                "id": 1,
                "name": "10% Discount",
                "family": "transaction",
                "moment": null,
                "config": {
                    "stackable": false,
                    "reward_type": "multiplier",
                    "scope": "whole"
                },
                "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": "Gent",
                        "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
        }
    }
}

Responsevelden

VeldTypeBeschrijving
familystringtransaction (getriggerd door een aankoop) of customer_moment (getriggerd door de klant)
momentstring|nullVoor customer_moment: birthday, anniversary, first_purchase, nth_purchase of win_back
configobject|nullInstellingen per familie. Het veld type geeft aan welke vorm het object heeft: transaction, moment, anniversary, nth_purchase of win_back. Zie hieronder
versionintegerGaat omhoog bij elke inhoudelijke wijziging. Wordt meegegeven aan de punten die een aankoop oplevert.
multiplierinteger|nullHeel getal, minimaal 2: 2 = 2x, 3 = 3x. null tenzij deze campagne met een vermenigvuldiger werkt.
recurrence_typestring|nulldate_range, days_of_week of specific_dates. null wanneer de campagne geen schema heeft.
recurrence_configobject|nullInstellingen bij het herhalingstype. Het veld type herhaalt recurrence_type. null wanneer de campagne zich niet herhaalt.
schedule_summarystringHet schema in gewone taal. "Always active" wanneer de campagne zich niet herhaalt. De server vertaalt het niet.
starts_atstringKalenderdatum, YYYY-MM-DD, volgens de Brusselse kalender. De eerste dag waarop de campagne geldt
ends_atstring|nullKalenderdatum, YYYY-MM-DD, volgens de Brusselse kalender. De laatste dag waarop de campagne geldt; vanaf de dag erna geldt ze niet meer voor een aankoop, terwijl haar status op active blijft. null betekent: geen einddatum.
statusobject{ "value": "active", "label": "Active" }. label is altijd Engels. Zie hieronder
min_transaction_amountnumber|nullMinimale aankoop waarbij de campagne geldt, in euro's. Zie hieronder
branchesarray|nullDe filialen waar deze campagne loopt, elk { "external_id", "name", "type" }. null betekent alle filialen. Zie hieronder
created_atstring|nullISO 8601-timestamp
updated_atstring|nullISO 8601-timestamp

status.label is altijd Engels

Deze API kent geen taal per request en vertaalt niets aan de serverkant, dus staat er in label het Engelse woord, welke taal je integratie ook spreekt. Vertaal value zelf. Op dit endpoint is dat altijd active: de lijst bevat alleen actieve campagnes, en een afgelopen campagne houdt die status. Vergelijk dus de datums.

min_transaction_amount staat in euro's, niet in eurocenten

Dit is het enige veld op deze API dat niet in eurocenten staat. 25.0 betekent EUR 25,00, niet EUR 0,25. Elk ander bedrag (total_amount, unit_price, price_cents en de statistiekcijfers) blijft een geheel aantal eurocenten.

Lees het bereik uit `branches`, niet uit `config.branch_ids`

branches bevat de external_id van elk filiaal waar de campagne loopt: de sleutel die je eigen systemen al gebruiken. config.branch_ids beschrijft hetzelfde bereik met de interne sleutels van Puntjes. config gaat ongewijzigd door zoals het is opgeslagen, en die ids zeggen buiten het portaal niets. branches is null wanneer de campagne geen filiaal noemt; dan loopt hij overal, ook bij aankopen die zonder filiaal zijn geregistreerd. Een lege array betekent het omgekeerde: de campagne heeft wel een bereik, maar elk filiaal dat hij noemde is intussen verwijderd, dus hij komt nergens meer overeen.

Het config-object

Voor family: "transaction":

SleutelBeschrijving
scopewhole, whole_purchase of products. whole en whole_purchase betekenen hetzelfde: de hele aankoop. Een campagne uit een oudere release kan whole_purchase bevatten
reward_typemultiplier (gebruikt het veld multiplier) of fixed_points (gebruikt points)
pointsPunten per eenheid die meetelt, bij fixed_points
max_points_per_purchaseMaximum dat één aankoop met deze campagne kan opleveren. Ontbreekt hij of is hij null, dan is er geen limiet.
productsBij products-bereik: een vaste momentopname van { "sku", "name" }-paren
stackabletrue wanneer deze campagne combineerbaar is en met andere mag stapelen. Anders betaalt alleen de sterkste uit

Voor family: "customer_moment": de eigen instellingen van het moment (bijvoorbeeld every_n en min_amount bij nth_purchase, years bij anniversary, months bij win_back) plus een gift-object dat beschrijft wat het moment uitdeelt. min_amount staat in eurocenten, anders dan min_transaction_amount ernaast.

config, recurrence_config en elk gift dragen een type dat hun vorm noemt: kijk ernaar om te weten hoe je het object leest. Het type van een geschenk is discount, free_product of points. Het type staat er alleen als de vorm bekend is. Een config heeft er geen bij een momentcampagne zonder moment. Een gift.discount heeft er geen als kind niet percentage of fixed is.

Een kortingsgeschenk bewaart zijn bedrag in gift.discount: type (percentage of fixed), dan percentage of amount_cents. kind bevat dezelfde waarde maar is verouderd: lees type. Geldt de korting op één product, dan is gift.discount.product_id de id van dat product zoals GET /products hem teruggeeft. Ontbreekt het veld of is het null, dan geldt de korting op de hele aankoop. Je kassa hoeft het hier niet te lezen: wie de bon verifieert, krijgt de external_id van het product terug als discount.product_reference.

Campagnes van vóór dit contract

Een campagne uit een eerdere release kan een null of een onvolledige config hebben. Zo'n rij gedraagt zich als een campagne met een vermenigvuldiger over de hele aankoop die niet mag stapelen. Lees de standaardwaarden en ga er niet van uit dat de sleutels er staan.

Fouten

CodeStatusBeschrijving
BRANCH_NOT_FOUND422branch is leeg of hoort bij geen enkel filiaal van jou, ook niet bij een gedeactiveerd filiaal. Puntjes weigert de filter in plaats van hem te negeren, zodat een verkeerd gespelde sleutel niet kan doorgaan voor een rapport over al je filialen