Kernconcepten
Beloningen & Inwisselingen
Beloningen zijn wat je klanten met hun gespaarde loyaliteitspunten kunnen kopen. Besteedt een klant punten aan een beloning, dan legt Puntjes dat vast als een inwisseling, met een eigen bevestigingscode.
Beloningstypen
GET /rewards houdt de twee typen uit elkaar met type, en geeft van elke beloning ook payment_amount, product_reference, discount_type en discount_value. Zo kan de kassa vóór het inwisselen nagaan of het product van een korting op de rekening staat, en of er op de verkoop een percentage af mag of alleen een vast bedrag. De kassa krijgt alle velden hieronder in type_specific_data zodra ze de beloning inwisselt met POST /redemptions, en rekent de beloning daarmee af, niet met de lijst: de inwisseling bewaart de waarden van het moment zelf (zie de Verzilvering-endpoints).
Korting
Korting op een volgende aankoop, op de hele aankoop of op één product:
| Veld | Beschrijving |
|---|---|
discount_value | Hoeveel korting de klant krijgt. Bij fixed_amount een bedrag in eurocenten (500 is EUR 5,00; het minimum is 1, EUR 0,01). Bij percentage een geheel getal van 1 tot 100 |
discount_type | fixed_amount (bv. EUR 5,00 korting) of percentage (bv. 10% korting) |
product_reference | Het artikelnummer (external_id) van het ene product waarop de korting geldt. null als ze op de hele aankoop geldt |
Een vast bedrag kan hoger uitvallen dan de prijs van het product. Hoe je dat begrenst, bepaal je in je kassa.
Gratis product
Een gratis product of dienst:
| Veld | Beschrijving |
|---|---|
product_reference | Verwijzing naar het gratis product (die bepaal je zelf) |
payment_amount | Wat de kassa bovenop de punten aanrekent, in eurocenten (200 is EUR 2,00). 0 betekent alleen punten |
Dat bedrag betaalt de klant aan je kassa; Puntjes houdt die betaling niet bij. Lees het bedrag uit de inwisseling, niet uit GET /rewards: de inwisseling bewaart het bedrag zoals het was bij het inwisselen, ook als de beloning daarna verandert.
Beloningseigenschappen
Wat GET /rewards per beloning teruggeeft:
| Veld | Type | Beschrijving |
|---|---|---|
id | integer | Uniek belonings-ID |
name | string | Naam van de beloning |
description | string|null | Omschrijving van de beloning, of null als die er niet is |
type | string | discount of free_product |
point_cost | integer | Punten die het inwisselen kost |
payment_amount | integer | Wat de kassa bovenop de punten aanrekent, in eurocenten. 0 betekent alleen punten, en bij een korting is het altijd 0 |
image_url | string|null | Absolute URL naar de afbeelding van de beloning, of null als die er geen heeft |
remaining_stock | integer | Inwisselingen die nog over zijn. 0 bij een beloning zonder limiet: kijk eerst naar is_unlimited |
total_stock | integer|null | Aantal inwisselingen waarmee de beloning begon. null betekent onbeperkt |
available_from | string|null | Vanaf wanneer de beloning te krijgen is, een datum als YYYY-MM-DD. null betekent zonder begindatum |
available_until | string|null | Tot wanneer de beloning te krijgen is, een datum als YYYY-MM-DD. null betekent dat de beloning niet verloopt |
branches | array|null | De vestigingen waar klanten deze beloning kunnen inwisselen, elk { "external_id", "name", "type" }. null betekent elke vestiging |
product_reference | string|null | 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 | boolean | true als de beloning geen voorraadlimiet heeft. total_stock is dan null en remaining_stock is 0 |
discount_type | string|null | percentage of fixed_amount bij een discount, dezelfde waarden als type_specific_data.discount_type. null bij een free_product |
discount_value | integer|null | Hoe groot de korting is: 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 | integer|null | Hoe vaak één klant deze beloning kan inwisselen. null betekent geen grens. Een inwisseling die de winkel annuleerde, telt niet mee |
customer_redemptions | integer|null | Hoe vaak de klant van identifier deze beloning inwisselde, zonder de geannuleerde. null als je geen identifier meestuurde of niemand erbij hoort |
Beschikbaarheid
Met available_from en available_until beperk je een beloning tot een periode. GET /rewards filtert alleen op status: ook beloningen buiten hun periode en uitverkochte beloningen staan erin, en POST /redemptions weigert die met REWARD_UNAVAILABLE of OUT_OF_STOCK. Vergelijk de datums en kijk naar remaining_stock en is_unlimited voor je een beloning aanbiedt.
Een beloning kan ook vastleggen hoe vaak één klant ze krijgt, met max_redemptions_per_customer. Die grens telt voor altijd. Een geannuleerde inwisseling geeft de klant die kans terug; een code die ongebruikt verliep, telt wel mee, want de klant betaalde er de punten al voor. Vraag GET /rewards?identifier=CARD-001 op en elke beloning krijgt customer_redemptions mee: hoe vaak die klant ze al inwisselde. Bied de beloning alleen aan zolang dat getal onder de grens ligt. Staat het op de grens, dan weigert POST /redemptions met REDEMPTION_LIMIT_REACHED (422).
Inwisselproces
1. De klant kiest een beloning
Het kassasysteem toont de beloningen die te krijgen zijn en de klant kiest er een. Haal de actieve beloningen op met het beloningen-endpoint.
2. Dien een inwisseling in
POST /api/v1/redemptions
{
"identifier": "CARD-001",
"reward_id": 5,
"idempotency_key": "redeem-2024-0042"
}
De API controleert:
- of het walletsaldo van de klant volstaat
- of de beloning actief en beschikbaar is
- of er nog voorraad is
- of de beloning de vestiging toestaat waar je de inwisseling boekt
- of de klant de beloning nog niet zo vaak inwisselde als de winkel toestaat
Dankzij de idempotency_key kun je de aanroep gerust opnieuw sturen: dezelfde
sleutel levert de oorspronkelijke inwisseling op, en schrijft geen punten een
tweede keer af.
Waar de inwisseling plaatsvond
branch is je eigen sleutel voor de winkel: de external_id die je die vestiging gaf. De volgorde is
dezelfde als bij een transactie: de sleutel in de payload wint, anders de standaardvestiging van de
API-credential, anders geen vestiging. Een sleutel die bij geen van je vestigingen hoort, wordt geweigerd
met BRANCH_NOT_FOUND (422). Zo gaat een typfout niet door voor een inwisseling zonder vestiging.
Een beloning die vestigingen noemt, kunnen klanten alleen daar inwisselen. Stuur je geen vestiging, of een
vestiging die er niet bij staat, dan wordt de aanroep geweigerd met BRANCH_REQUIRED (422). Eén code voor
allebei, want allebei hebben dezelfde oplossing: noem een vestiging die de beloning toestaat.
De vestiging wordt één keer vastgelegd. Stuur je dezelfde idempotency_key opnieuw, dan krijg je de
oorspronkelijke inwisseling terug, met de vestiging van de eerste aanroep, ook als de herhaling een andere
noemt.
3. De bevestigingscode komt terug
Lukt de aanroep, dan gaan de punten van de wallet van de klant af en krijg je een inwisseling terug met een bevestigingscode:
{
"data": {
"redemption_id": 12,
"confirmation_code": "PNTJ-K4MP7QRS",
"reward": {
"name": "Gratis koffie",
"type": "free_product"
},
"points_deducted": 500,
"remaining_balance": 1000,
"redeemed_at": "2024-03-15T12:30:00+00:00",
"expires_at": "2024-03-15T14:30:00+00:00"
}
}
4. Verifieer de inwisseling
Toont de klant de bevestigingscode, dan verifieer je die:
POST /api/v1/redemptions/{code}/verify
Daarmee gaat de inwisseling van valid naar used. Een code verifieer je één keer: een tweede verificatie antwoordt CODE_ALREADY_USED.
Inwisselstatus
| Status | Beschrijving |
|---|---|
| valid | Code uitgegeven, de klant heeft de beloning nog niet opgehaald |
| used | De klant kreeg de beloning: de code is geverifieerd, of de zaak gaf ze meteen |
| expired | De code liep af voordat iemand ze verifieerde |
| cancelled | De zaak annuleerde de inwisseling: de punten en de voorraad kwamen terug |
Een beloning die de zaak in het beheerportaal meteen aan de klant geeft, is meteen used. De klant
heeft ze al in handen, dus de code is gesloten en geen kassa kan ze een tweede keer verifiëren.
Een beheerder van de zaak kan een inwisseling met status valid of used annuleren op de klantpagina.
De klant krijgt de punten terug die de inwisseling kostte, en de beloning gaat terug in de voorraad. Een
kassa die een geannuleerde code verifieert, krijgt CODE_CANCELLED. Een verlopen code kun je niet annuleren.
Hoe lang een bevestigingscode geldig is
Stelt de handelaar in het portaal in hoeveel uur de bevestigingscode van een beloning geldig blijft, dan verloopt de code na die tijd. Daarna kun je hem niet meer verifiëren. Het veld expires_at in de response van de inwisseling zegt wanneer dat moment valt.
Inwisseling opzoeken
Je zoekt een inwisseling op met de bevestigingscode:
GET /api/v1/redemptions/{code}
Handig voor een kassasysteem dat de inwisseling eerst wil tonen en pas daarna verifieert.
Zie de Verzilvering-endpoints voor de volledige API-referentie.