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
| Parameter | Type | Verplicht | Standaard | Beschrijving |
|---|---|---|---|---|
affordable | boolean | Nee | false | Geeft 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 |
identifier | string | Nee | — | 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
| Veld | Beschrijving |
|---|---|
payment_amount | Wat de kassa bovenop de punten aanrekent, in eurocenten. 0 betekent alleen punten, en bij een korting is het altijd 0. |
remaining_stock | Inwisselingen die nog over zijn. 0 bij een beloning zonder limiet: kijk eerst naar is_unlimited. |
total_stock | Aantal inwisselingen waarmee de beloning begon. null betekent onbeperkt. |
available_from | YYYY-MM-DD, geen timestamp. Deze twee datums zijn de uitzondering op deze API |
available_until | YYYY-MM-DD. null betekent dat de beloning niet verloopt. |
branches | De filialen waar de beloning kan worden ingewisseld, elk { "external_id", "name", "type" }, in de volgorde die jij aanhoudt. null betekent elk filiaal. |
product_reference | Het 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_unlimited | true als de beloning geen voorraadlimiet heeft. total_stock is dan null en remaining_stock is 0. |
discount_type | percentage of fixed_amount bij een discount, dezelfde waarden als type_specific_data.discount_type van een inwisseling. null bij een free_product. |
discount_value | Hoe 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_customer | Hoe vaak één klant deze beloning kan inwisselen. null betekent geen grens. Een inwisseling die de winkel annuleerde, telt niet mee. |
customer_redemptions | Hoe 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
| Parameter | Type | Verplicht | Standaard | Beschrijving |
|---|---|---|---|---|
branch | string | Nee | — | 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 |
page | integer | Nee | 1 | Welke 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
| Veld | Type | Beschrijving |
|---|---|---|
family | string | transaction (getriggerd door een aankoop) of customer_moment (getriggerd door de klant) |
moment | string|null | Voor customer_moment: birthday, anniversary, first_purchase, nth_purchase of win_back |
config | object|null | Instellingen per familie. Het veld type geeft aan welke vorm het object heeft: transaction, moment, anniversary, nth_purchase of win_back. Zie hieronder |
version | integer | Gaat omhoog bij elke inhoudelijke wijziging. Wordt meegegeven aan de punten die een aankoop oplevert. |
multiplier | integer|null | Heel getal, minimaal 2: 2 = 2x, 3 = 3x. null tenzij deze campagne met een vermenigvuldiger werkt. |
recurrence_type | string|null | date_range, days_of_week of specific_dates. null wanneer de campagne geen schema heeft. |
recurrence_config | object|null | Instellingen bij het herhalingstype. Het veld type herhaalt recurrence_type. null wanneer de campagne zich niet herhaalt. |
schedule_summary | string | Het schema in gewone taal. "Always active" wanneer de campagne zich niet herhaalt. De server vertaalt het niet. |
starts_at | string | Kalenderdatum, YYYY-MM-DD, volgens de Brusselse kalender. De eerste dag waarop de campagne geldt |
ends_at | string|null | Kalenderdatum, 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. |
status | object | { "value": "active", "label": "Active" }. label is altijd Engels. Zie hieronder |
min_transaction_amount | number|null | Minimale aankoop waarbij de campagne geldt, in euro's. Zie hieronder |
branches | array|null | De filialen waar deze campagne loopt, elk { "external_id", "name", "type" }. null betekent alle filialen. Zie hieronder |
created_at | string|null | ISO 8601-timestamp |
updated_at | string|null | ISO 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":
| Sleutel | Beschrijving |
|---|---|
scope | whole, whole_purchase of products. whole en whole_purchase betekenen hetzelfde: de hele aankoop. Een campagne uit een oudere release kan whole_purchase bevatten |
reward_type | multiplier (gebruikt het veld multiplier) of fixed_points (gebruikt points) |
points | Punten per eenheid die meetelt, bij fixed_points |
max_points_per_purchase | Maximum dat één aankoop met deze campagne kan opleveren. Ontbreekt hij of is hij null, dan is er geen limiet. |
products | Bij products-bereik: een vaste momentopname van { "sku", "name" }-paren |
stackable | true 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
| Code | Status | Beschrijving |
|---|---|---|
BRANCH_NOT_FOUND | 422 | branch 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 |