API-referentie

Verzilvering-endpoints

Endpoints om beloningen te verzilveren, de verzilveringen van een klant op te vragen, de gegevens van een verzilvering op te zoeken, en aan de kassa bevestigingscodes en campagnebonnen te verifiëren.


Een beloning verzilveren

POST /api/v1/redemptions

Schrijf punten af van de wallet van een klant en geef een bevestigingscode uit voor de beloning.

Request body

VeldTypeVerplichtBeschrijving
identifierstringJaKlant-identifier (kaartnummer, e-mail, enz.), maximaal 255 tekens
reward_idintegerJaDe beloning die je verzilvert
idempotency_keystringJaUnieke sleutel die dubbele verzilveringen voorkomt (max. 255 tekens)
branchstringNeeJe eigen sleutel voor de vestiging waar deze beloning is afgegeven: de external_id die je die vestiging gaf, maximaal 64 tekens. Die gaat voor op de standaardvestiging van de API-credential, zodat één gedeelde credential per aanroep een andere winkel kan noemen. Laat het veld weg om de standaardvestiging van de credential te gebruiken, en laat beide weg om de verzilvering zonder vestiging vast te leggen. Een beloning die maar bij bepaalde vestigingen te verzilveren is, moet er een van die vestigingen krijgen.

Example request

{
    "identifier": "CARD-001",
    "reward_id": 5,
    "idempotency_key": "redeem-2024-0042",
    "branch": "shop-1"
}
curl -X POST https://puntjes.app/api/v1/redemptions \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"identifier":"CARD-001","reward_id":5,"idempotency_key":"redeem-2024-0042","branch":"shop-1"}'

Response (201 Created)

{
    "data": {
        "redemption_id": 12,
        "confirmation_code": "PNTJ-K4MP7QRS",
        "reward": {
            "name": "Gratis koffie",
            "type": "free_product",
            "id": 5
        },
        "points_deducted": 500,
        "remaining_balance": 1000,
        "redeemed_at": "2024-03-15T12:30:00+00:00",
        "expires_at": "2024-03-15T14:30:00+00:00",
        "type_specific_data": {
            "product_reference": "COFFEE-REG",
            "payment_amount": 200
        },
        "branches": [
            {
                "external_id": "shop-1",
                "name": "Gent",
                "type": "physical"
            }
        ]
    }
}

Responsevelden

VeldBeschrijving
redemption_idHet ID van de verzilvering. Gebruik het om die aan je eigen administratie te koppelen
confirmation_codeUnieke code die de klant aan de kassa toont, in het formaat PNTJ-XXXXXXXX
rewardDe beloning die is ingewisseld: {"name", "type", "id"}. id is de id die GET /rewards voor deze beloning geeft
points_deductedAantal punten dat van de wallet is afgeschreven
remaining_balanceHet saldo van de wallet na de verzilvering
redeemed_atWanneer de verzilvering is aangemaakt
expires_atWanneer de bevestigingscode verloopt (als de beloning een code_valid_for_hours heeft), anders null
type_specific_data{"type", "discount_value", "discount_type", "product_reference"} bij kortingsbeloningen, {"type", "product_reference", "payment_amount"} bij gratis-productbeloningen. type is discount of free_product: lees het om de vorm te kiezen
branchesDe vestigingen waar de beloning ingewisseld mag worden, elk {"external_id", "name", "type"}. null betekent elke vestiging

Bij een korting is product_reference het artikelnummer van het ene product waarop de korting geldt: de external_id van dat product in je catalogus. Geldt de korting op de hele aankoop, dan is het null, net als bij elke korting die is ingewisseld voordat dit veld bestond. Een vast bedrag kan hoger uitvallen dan de prijs van dat product. Hoe je dat begrenst, bepaal je in je kassa. In het antwoord, zonder de andere velden:

{
    "data": {
        "type_specific_data": {
            "type": "discount",
            "discount_value": 20,
            "discount_type": "percentage",
            "product_reference": "THERMO-01"
        }
    }
}

payment_amount is wat je kassa bovenop de punten aanrekent, in eurocenten (0 = alleen punten). Die betaling loopt niet via Puntjes. Puntjes kopieert type_specific_data op het moment van de verzilvering. Pas je de beloning daarna aan, dan geven het opzoeken, de lijst van de klant en de verificatie nog altijd de oude waarden terug.

De vestigingsregel van de beloning geldt wanneer de beloning wordt ingewisseld, met branch op POST /redemptions. Ze geldt opnieuw wanneer de code wordt geverifieerd: zie POST /redemptions/{code}/verify. branches toont de vestigingen van de beloning zoals ze nu zijn, niet zoals ze bij de inwisseling waren. null betekent elke vestiging. GET /redemptions/{code}, de lijst met inwisselingen van een klant en het antwoord van de verificatie hebben ook reward.id en branches.

Idempotentie

De idempotency_key moet uniek zijn per handelaar. Verzilver je met een idempotency_key die al bestaat, dan:

  • krijg je de oorspronkelijke verzilvering terug, met dezelfde confirmation_code
  • gaan er geen punten een tweede keer af
  • komt er geen nieuwe grootboekregel bij en gaat de voorraad niet opnieuw omlaag

Zo kun je de aanroep na een netwerkfout veilig opnieuw versturen. Je krijgt ook antwoord wanneer de wallet de beloning intussen niet meer kan betalen: de oorspronkelijke verzilvering heeft er al voor betaald, dus een retry faalt nooit met INSUFFICIENT_BALANCE. remaining_balance toont het saldo van nu, niet dat op het moment van de oorspronkelijke verzilvering. Je krijgt de oorspronkelijke verzilvering ook terug als ze later geannuleerd is. Zoek de code op met GET /redemptions/{code} om de huidige status te zien.

Kies je idempotency keys zorgvuldig

Gebruik een waarde die de verzilvering uniek identificeert in jouw systeem, zoals een order-ID of een UUID die je één keer per afrekening genereert. Vermijd willekeurige waarden die bij een retry veranderen. Dat maakt idempotentie zinloos. Hergebruik je dezelfde sleutel voor een andere klant of een andere beloning, dan krijg je IDEMPOTENCY_KEY_CONFLICT terug.

Fouten

CodeStatusBeschrijving
BRANCH_NOT_FOUND422Geen enkele vestiging van jou heeft deze sleutel. Inactieve vestigingen tellen mee als gevonden: bij het verzilveren telt alleen welke winkel het was, niet of ze nog open is.
BRANCH_REQUIRED422De beloning is maar bij een deel van je vestigingen te verzilveren, en deze aanroep noemt er geen: er staat helemaal geen branch in, of een die de beloning niet toelaat. Stuur een vestiging die de beloning wel toelaat.
CUSTOMER_DEACTIVATED422Het klantaccount is gedeactiveerd
CUSTOMER_NOT_FOUND404Geen klant gevonden met de opgegeven identifier
IDEMPOTENCY_KEY_CONFLICT422Deze idempotency_key is al gebruikt voor een andere klant of beloning
INSUFFICIENT_BALANCE422De klant heeft niet genoeg punten
NO_WALLET422De klant heeft geen wallet
OUT_OF_STOCK422De beloning is niet meer op voorraad
REDEMPTION_LIMIT_REACHED422De klant wisselde deze beloning al zo vaak in als de winkel toestaat
REWARD_NOT_FOUND404Geen beloning gevonden met het opgegeven reward_id, ook een die de handelaar heeft ingetrokken
REWARD_UNAVAILABLE422De beloning is inactief of buiten de beschikbaarheidsperiode

Een verzilvering opzoeken

GET /api/v1/redemptions/{code}

Zoek een verzilvering op met de bevestigingscode. Zo toon je de gegevens van de verzilvering voordat je ze verifieert.

Path parameters

ParameterTypeBeschrijving
codestringDe bevestigingscode. Hoofdletters of kleine letters werken allebei, en spaties ervoor of erna tellen niet mee

Response

{
    "data": {
        "redemption_id": 12,
        "confirmation_code": "PNTJ-K4MP7QRS",
        "status": "valid",
        "reward": {
            "name": "Gratis koffie",
            "type": "free_product",
            "id": 5
        },
        "customer": {
            "name": "Jan De Vries"
        },
        "points_deducted": 500,
        "redeemed_at": "2024-03-15T12:30:00+00:00",
        "verified_at": null,
        "expires_at": "2024-03-15T14:30:00+00:00",
        "type_specific_data": {
            "product_reference": "COFFEE-REG",
            "payment_amount": 200
        },
        "branches": [
            {
                "external_id": "shop-1",
                "name": "Gent",
                "type": "physical"
            }
        ]
    }
}

status is valid tot de code geverifieerd is, daarna used. Een code die voorbij zijn expires_at is, springt bij het opzoeken op expired. Een verzilvering die de zaak in het beheerportaal annuleerde, is cancelled en blijft dat ook na haar expires_at. Een beloning die de zaak in het beheerportaal meteen aan de klant gaf, is van bij het begin used.


De verzilveringen van een klant opvragen

GET /api/v1/customers/{customer}/redemptions

Alle beloningen die één klant bij je heeft verzilverd, de nieuwste eerst, 15 per pagina. Gebruik dit als de klant aan de kassa staat zonder zijn PNTJ--code: zoek de klant op met zijn klantenkaart via GET /customers/lookup, vraag deze lijst op met status=valid, en verifieer de beloning die de klant kiest met POST /redemptions/{code}/verify.

Path parameters

ParameterTypeBeschrijving
customerstringHet klant-ID, zoals GET /customers/lookup het teruggeeft

Query parameters

ParameterTypeVerplichtStandaardBeschrijving
statusstringNee—Eén van valid, used, expired of cancelled. Alleen een valid code voorbij zijn expires_at telt als expired; een used of cancelled code houdt haar status
pageintegerNee1Welke pagina je terugkrijgt. Een waarde onder 1, of een waarde die geen geheel getal is, geeft pagina 1

Response

{
    "data": {
        "data": [
            {
                "redemption_id": 12,
                "confirmation_code": "PNTJ-K4MP7QRS",
                "status": "valid",
                "reward": {
                    "name": "Gratis koffie",
                    "type": "free_product",
                    "id": 5
                },
                "customer": {
                    "name": "Jan De Vries"
                },
                "points_deducted": 500,
                "redeemed_at": "2024-03-15T12:30:00+00:00",
                "verified_at": null,
                "expires_at": "2024-03-15T14:30:00+00:00",
                "type_specific_data": {
                    "product_reference": "COFFEE-REG",
                    "payment_amount": 200
                },
                "branches": [
                    {
                        "external_id": "shop-1",
                        "name": "Gent",
                        "type": "physical"
                    }
                ]
            }
        ],
        "links": {
            "first": "string",
            "last": "string",
            "prev": "string",
            "next": "string"
        },
        "meta": {
            "current_page": 1,
            "last_page": 2,
            "per_page": 15,
            "total": 20
        }
    }
}

Elk item heeft dezelfde velden als GET /redemptions/{code}, zodat één parser ze allebei leest.

status is de status op het moment dat je de lijst opvraagt. Een code die voorbij zijn expires_at is, staat op expired en valt buiten status=valid, ook als nog niemand hem heeft opgezocht. Een beloning die de zaak in het beheerportaal meteen gaf, is used, en een geannuleerde verzilvering is cancelled. Geen van beide staat dus in status=valid.

Fouten

CodeStatusBeschrijving
CUSTOMER_NOT_FOUND404Je hebt geen klant met dit ID

Een verzilvering verifiëren

POST /api/v1/redemptions/{code}/verify

Markeer een verzilvering als geverifieerd. Daarmee bevestig je dat de klant de beloning gekregen heeft.

Een code verifieer je één keer. Een beloning die de zaak in het beheerportaal meteen gaf, is al used, dus die code antwoordt CODE_ALREADY_USED. Een geannuleerde verzilvering antwoordt CODE_CANCELLED: de klant heeft zijn punten terug, dus geef de beloning niet mee.

Path parameters

ParameterTypeBeschrijving
codestringDe bevestigingscode. Hoofdletters of kleine letters werken allebei, en spaties ervoor of erna tellen niet mee

Request body

VeldTypeVerplichtBeschrijving
branchstringNeeJe eigen sleutel voor de vestiging waar de beloning wordt meegegeven: de external_id die je die vestiging gaf, maximaal 64 tekens. Die gaat voor op de standaardvestiging van je API-credential. Een beloning met branches moet een van die vestigingen krijgen, hoe ze ook binnenkomt.
idempotency_keystringNeeJe eigen sleutel voor deze verificatie, maximaal 255 tekens. Stuur dezelfde sleutel opnieuw als een antwoord zoekraakt: de herhaling geeft het eerste geslaagde antwoord. Een sleutel verifieert één code: bij een andere code geeft hij IDEMPOTENCY_KEY_CONFLICT

De body is optioneel. Een beloning zonder vestigingen verifieer je zonder body. Een body die niet te lezen is, geeft 400 INVALID_JSON, en een verkeerde waarde geeft 422 VALIDATION_ERROR.

Idempotentie

Stuur bij elke verificatie een idempotency_key mee. Raakt het antwoord zoek, stuur dan dezelfde aanroep opnieuw met dezelfde sleutel. De herhaling geeft het eerste geslaagde antwoord, met dezelfde verified_at. Zonder sleutel geeft een tweede aanroep CODE_ALREADY_USED. Een sleutel verifieert één code. Stuur je hem mee met een andere code, dan geeft dat IDEMPOTENCY_KEY_CONFLICT, en die code blijft valid. De sleutel van een verificatie staat los van de idempotency_key van de inwisseling, dus je mag één waarde voor beide gebruiken.

Vestiging

Een beloning met branches verifieer je alleen bij een van die vestigingen. De vestiging wordt bepaald zoals bij POST /redemptions: de branch die je meestuurt, anders de standaardvestiging van je API-credential, anders geen. Is er geen vestiging, of een die de beloning niet toelaat, dan geeft de aanroep BRANCH_REQUIRED, en de code blijft valid. Een beloning waarvan branches null is, verifieer je bij elke vestiging. Een verificatie van een beperkte beloning zonder bekende vestiging slaagde vóór deze regel. Ze geeft nu BRANCH_REQUIRED. Nog drie aanroepen die eerder slaagden, geven nu 422. Een branch die geen vestiging noemt, bij een beloning zonder branches, geeft BRANCH_NOT_FOUND. Een idempotency_key van meer dan 255 tekens geeft VALIDATION_ERROR. Eén idempotency_key die je met twee codes meestuurt, geeft bij de tweede code IDEMPOTENCY_KEY_CONFLICT. branches is de lijst die de beloning nu heeft. Een herhaling met de sleutel die de code verifieerde, wordt beantwoord voordat de vestigingsregel wordt toegepast, ook als de vestigingen van de beloning intussen zijn veranderd. Een branch-sleutel die geen vestiging van jou noemt, geeft nog steeds BRANCH_NOT_FOUND, ook bij een herhaling.

Example request

{
    "branch": "shop-1",
    "idempotency_key": "verify-2024-0042"
}
curl -X POST https://puntjes.app/api/v1/redemptions/1/verify \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"branch":"shop-1","idempotency_key":"verify-2024-0042"}'

Response

{
    "data": {
        "redemption_id": 12,
        "confirmation_code": "PNTJ-K4MP7QRS",
        "status": "used",
        "reward": {
            "name": "Gratis koffie",
            "type": "free_product",
            "id": 5
        },
        "points_deducted": 500,
        "verified_at": "2024-03-15T12:45:00+00:00",
        "type_specific_data": {
            "product_reference": "COFFEE-REG",
            "payment_amount": 200
        },
        "branches": [
            {
                "external_id": "shop-1",
                "name": "Gent",
                "type": "physical"
            }
        ]
    }
}

Fouten

CodeStatusBeschrijving
BRANCH_NOT_FOUND422Geen enkele vestiging van jou heeft deze sleutel. Inactieve vestigingen tellen mee als gevonden: bij het verifiëren telt alleen welke winkel het was, niet of ze nog open is.
BRANCH_REQUIRED422De beloning mag maar bij een deel van je vestigingen worden geverifieerd, en deze aanroep noemt er geen: er staat helemaal geen branch in, of een die de beloning niet toelaat. De code blijft valid; stuur een vestiging die de beloning wel toelaat en roep opnieuw aan.
CODE_ALREADY_USED422Deze verzilvering is al geverifieerd
CODE_CANCELLED422De zaak heeft deze verzilvering in het beheerportaal geannuleerd en de punten teruggegeven, dus de code werkt niet meer
CODE_EXPIRED422De bevestigingscode is verlopen
IDEMPOTENCY_KEY_CONFLICT422Deze idempotency_key heeft al een andere code geverifieerd. De code in deze aanroep is niet geverifieerd; stuur een nieuwe sleutel
REDEMPTION_NOT_FOUND404Geen verzilvering gevonden met deze code
VERIFICATION_FAILED422De verzilvering kon niet geverifieerd worden

Een bon opzoeken

GET /api/v1/vouchers/{code}

Lees wat een campagnebon geeft zonder hem te verbruiken. Een kassa die een BON--code scant, kan de kassier de korting of de producten tonen, nakijken of ze op de open verkoop staan, en pas daarna POST /vouchers/{code}/verify aanroepen. Deze aanroep verandert niets: de bon blijft zoals hij is.

Het antwoord heeft dezelfde velden als het antwoord van verify, plus status. Een bon die al verbruikt is en intussen verlopen is, leest used: hetzelfde feit dat verify meldt.

Path parameters

ParameterTypeBeschrijving
codestringDe boncode die de klant toont, BON-XXXXXXXX. Hoofdletters of kleine letters werken allebei, en spaties ervoor of erna tellen niet mee

Response

{
    "data": {
        "voucher_code": "BON-7K2MQX4P",
        "discount": null,
        "valid_until": "2026-08-31",
        "consumed_at": "2026-08-12T10:14:02+00:00",
        "campaign_id": 7,
        "kind": "free_product",
        "products": [
            {
                "id": 41,
                "name": "Croissant",
                "quantity": 2,
                "product_reference": "CROISSANT-01"
            }
        ],
        "status": "valid"
    }
}

Fouten

CodeStatusBeschrijving
VOUCHER_NOT_FOUND404Geen enkele bon van jou draagt deze code

Een code van een andere handelaar geeft dezelfde 404 als een code die nooit is uitgegeven, byte voor byte, om de reden die het stuk over verify hieronder geeft.


Een bon verifiëren

POST /api/v1/vouchers/{code}/verify

Bonnen

Een bon is wat een campagnegeschenk aan één klant geeft: de verjaardagsmail die met 15% korting binnenkomt, of die met twee croissants en een koffie. Het is geen beloning en kost geen punten. De klant heeft hem niet gekozen en niet betaald, dus er valt niets te verzilveren en er is geen wallet om van af te schrijven. Wat er wél is, is een code om in te leveren.

kind zegt welke van de twee je in handen hebt. Een discount-bon draagt een discount-object dat je kassa toepast; een free_product-bon draagt products, de artikelen die je meegeeft. Eén code dekt het hele geschenk, hoeveel producten het ook noemt, dus een cadeau van twee artikelen is één scan en één aanroep.

Boncodes bestaan uit BON- gevolgd door acht tekens, zodat ze aan de kassa en in een supportgesprek niet te verwarren zijn met de PNTJ--bevestigingscodes hierboven.

Splits op `kind`, niet op welk veld gevuld is

Elke bon die is uitgegeven voordat productbonnen bestonden is een discount-bon, en discount houdt elke sleutel die het altijd had. Een client die discount leest zonder kind te controleren krijgt null zodra een handelaar voor het eerst een product weggeeft. Lees eerst kind en behandel alles wat je niet kent als een korting.

Deze aanroep verbruikt de bon

Verifiëren is niet vooraf bekijken. Een geslaagde aanroep markeert de bon als gebruikt. Wil je eerst zien wat een bon geeft, roep dan GET /vouchers/{code} aan. Stuur bij elke verify een idempotency_key mee: raakt het antwoord zoek, dan geeft dezelfde aanroep met dezelfde sleutel opnieuw het eerste geslaagde antwoord. Zonder die sleutel geeft een tweede aanroep VOUCHER_ALREADY_USED.

Path parameters

ParameterTypeBeschrijving
codestringDe boncode die de klant toont, BON-XXXXXXXX. Hoofdletters of kleine letters werken allebei, en spaties ervoor of erna tellen niet mee

Request body

VeldTypeVerplichtBeschrijving
branchstringNeeJe eigen sleutel voor de vestiging waar de bon wordt verzilverd: de external_id die je die vestiging gaf, maximaal 64 tekens. Die gaat voor op de standaardvestiging van je API-credential. Laat beide weg voor een bon die geen vestigingen noemt; een bon die er wel noemt moet er één krijgen, langs welke weg dan ook.
idempotency_keystringNeeJe eigen sleutel voor deze verzilvering, maximaal 255 tekens. Stuur dezelfde sleutel opnieuw als een antwoord zoekraakt: de herhaling geeft het eerste geslaagde antwoord. Een sleutel dient maar één keer: bij een andere bon geeft hij IDEMPOTENCY_KEY_CONFLICT

De body is optioneel. Een bon van een campagne die overal loopt verzilver je zonder body:

curl -X POST https://puntjes.app/api/v1/vouchers/BON-7K2MQX4P/verify \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Bij een bon van een campagne die maar bij een deel van je vestigingen loopt, moet je er één noemen. De vestiging wordt op dezelfde manier bepaald als bij POST /transactions en POST /redemptions: eerst de branch die je meestuurt, dan de standaardvestiging van je API-credential, dan niets. Een kassa waarvan de credential haar eigen winkel al noemt, heeft hier dus ook geen body nodig.

curl -X POST https://puntjes.app/api/v1/vouchers/BON-7K2MQX4P/verify \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"branch": "shop-1"}'

Welke vestigingen een bon toelaat ligt vast op het moment dat de campagne hem uitgeeft; de kassa leest dat niet opnieuw uit de campagne. De campagne daarna aanpassen verandert wat de volgende bon toelaat en nooit wat een openstaande bon toelaat. Dat is dezelfde regel die products en valid_until volgen, en daarom is de vestiging in de mail van de klant ook de vestiging die de kassa aanvaardt.

Response

{
    "data": {
        "voucher_code": "BON-7K2MQX4P",
        "discount": null,
        "valid_until": "2026-08-31",
        "consumed_at": "2026-08-12T10:14:02+00:00",
        "campaign_id": 7,
        "kind": "free_product",
        "products": [
            {
                "id": 41,
                "name": "Croissant",
                "quantity": 2,
                "product_reference": "CROISSANT-01"
            }
        ]
    }
}

Dat is een productbon. Een kortingsbon is de andere vorm: discount is ingevuld en products is null.

{
    "data": {
        "voucher_code": "BON-7K2MQX4P",
        "discount": { "type": "percentage", "kind": "percentage", "percentage": 15, "product_reference": null },
        "valid_until": "2026-08-31",
        "consumed_at": "2026-08-12T10:14:02+00:00",
        "campaign_id": 7,
        "kind": "discount",
        "products": null
    }
}

Een bon met een vast bedrag draagt een andere sleutel binnen data.discount. Deze geldt op één product:

{ "type": "fixed", "kind": "fixed", "amount_cents": 750, "product_reference": "THERMO-01" }

Responsevelden

VeldTypeBeschrijving
voucher_codestringDe code die je zonet verbruikt hebt
discountobject|nullAanwezig wanneer kind discount is, null bij een productbon
discount.typestringpercentage of fixed
discount.kindstringVerouderd: dezelfde waarde als discount.type. Lees type.
discount.percentageintegerAanwezig wanneer discount.type percentage is. Een geheel getal van 1 tot en met 100.
discount.amount_centsintegerAanwezig wanneer discount.type fixed is. Hele eurocenten. 750 is € 7,50.
discount.product_referencestring|nullHet artikelnummer van het ene product waarop de korting geldt; null als ze op de hele aankoop geldt
valid_untilstring|nullDe laatste dag waarop de bon geldig is, of null als hij nooit vervalt
consumed_atstringISO 8601-tijdstip van deze aanroep
campaign_idintegerDe campagne met het geschenk dat deze bon uitgaf
kindstringAltijd aanwezig: discount of free_product
productsarray|nullAanwezig wanneer kind free_product is, anders null
products[].idinteger|nullHet product in je catalogus, of null bij een regel die er nooit een had
products[].namestringDe naam van het product zoals het geschenk die beloofde
products[].quantityintegerHoeveel stuks je ervan meegeeft. Weergeven doe je zoals je wil: het portaal toont "2× Croissant".
products[].product_referencestring|nullHet artikelnummer van dat product, zodat je kassa het op de verkoop kan zetten; null voor een regel zonder catalogusproduct

Het bedrag staat onder een sleutel die naar zijn soort genoemd is en niet onder één gedeelde value. Met één gedeelde sleutel kan een client een fixed-bon als percentage lezen, en dan wordt € 7,50 korting plots 750% korting. Met twee sleutels krijg je in plaats daarvan een fout over een ontbrekende sleutel in je eigen code.

discount.product_reference is de external_id van dat product in je catalogus, vastgelegd op het moment dat de bon werd uitgegeven. Een vast bedrag kan hoger uitvallen dan de prijs van het product. Hoe je dat begrenst, bepaal je in je kassa. Een bon die is uitgegeven voordat dit veld bestond, geeft null.

products[].product_reference is de external_id van elk product, vastgelegd toen de bon werd uitgegeven, net als discount.product_reference. Elke bon van voor dit veld bestond, geeft null.

products is een kopie van het moment waarop de bon werd uitgegeven, geen live uitlezing van de campagne. Het geschenk daarna aanpassen verandert wat de volgende bon belooft en nooit wat deze bon oplevert. Dat is dezelfde regel die valid_until volgt. Productbonnen dragen vandaag valid_until: null: het geldigheidsvenster is een instelling van de kortingsbon, en een geschenk zonder venster vervalt nooit.

Fouten

CodeStatusBeschrijving
BRANCH_NOT_FOUND422Geen enkele vestiging van jou heeft deze sleutel. Inactieve vestigingen tellen mee als gevonden: bij het verzilveren van een bon telt alleen welke winkel het was, niet of ze nog open is.
BRANCH_REQUIRED422De bon is maar bij een deel van je vestigingen te verzilveren, en deze aanroep noemt er geen: er staat helemaal geen branch in, of een die de bon niet toelaat. De bon is niet verbruikt; stuur een vestiging die hij wel toelaat en roep opnieuw aan.
IDEMPOTENCY_KEY_CONFLICT422Deze idempotency_key heeft al een andere bon verbruikt. De bon in deze aanroep is niet verbruikt; stuur een nieuwe sleutel
VOUCHER_ALREADY_USED422De bon is al verbruikt
VOUCHER_EXPIRED422De bon is niet meer geldig
VOUCHER_NOT_FOUND404Geen enkele bon van jou draagt deze code

Een code van een andere handelaar geeft dezelfde 404 als een code die nooit is uitgegeven: dezelfde status en dezelfde body, byte voor byte. Zou er ook maar iets verschillen, dan kon een client de hele coderuimte aflopen en ontdekken welke codes echt bestaan, zonder er ooit een te kunnen verzilveren.

Een bon die vorige maand is verbruikt en sindsdien is verlopen meldt VOUCHER_ALREADY_USED en niet VOUCHER_EXPIRED. Dat is wat de kassamedewerker moet weten: "verlopen" leest als nooit ingeleverd, en nodigt uit tot een tweede korting bovenop de korting die de klant al kreeg.

Geldigheid loopt tot en met de genoemde dag

valid_until is een datum en geen tijdstip, en de bon is op die dag nog geldig. De mail aan de klant belooft "geldig tot 30 september", en op de 30e langskomen is dus zijn goed recht. De datum ligt vast zodra de bon wordt uitgegeven, dus het geldigheidsvenster van de campagne later inkorten verkort geen bon die al in iemands inbox ligt. Een geschenk zonder venster geeft een bon met valid_until: null, die nooit vervalt.

Puntjes verplaatst het geld niet

Verifiëren schrijft één tijdstip weg en verder niets: geen transactie, geen grootboekregel, geen punten in welke richting dan ook. De korting toepassen is het werk van je kassa, en dit endpoint legt vast wat er beloofd is en of het is ingeleverd.


De open bonnen van een klant opvragen

GET /api/v1/customers/{customer}/vouchers

Alle campagnebonnen die één klant nog bij je kan verzilveren, in één lijst zonder pagina's. Gebruik dit als de klant aan de kassa staat zonder zijn BON--code: zoek de klant op met zijn klantenkaart via GET /customers/lookup, vraag deze lijst op, en verzilver de bon die de klant kiest met POST /vouchers/{code}/verify.

Path parameters

ParameterTypeBeschrijving
customerstringHet klant-ID, zoals GET /customers/lookup het teruggeeft

Response

{
    "data": [
        {
            "voucher_code": "BON-7K2MQX4P",
            "campaign_id": 7,
            "kind": "free_product",
            "discount": null,
            "products": [
                {
                    "id": 41,
                    "name": "Croissant",
                    "quantity": 2,
                    "product_reference": "CROISSANT-01"
                }
            ],
            "valid_until": "2026-08-31",
            "branches": [
                {
                    "external_id": "shop-1",
                    "name": "Gent",
                    "type": "physical"
                }
            ]
        }
    ]
}

De bon die het eerst afloopt, staat bovenaan. Een bon zonder laatste dag staat onderaan. Een bon die al verbruikt is, of waarvan valid_until voorbij is, staat niet in de lijst. Op zijn laatste dag blijft een bon de hele dag (Belgische tijd) in de lijst staan, en zolang aanvaardt verify hem ook.

kind, discount en products zien er hetzelfde uit als in GET /vouchers/{code}, zodat één parser ze allebei leest. branches noemt de vestigingen die de bon aanvaarden, en is null voor een bon die elke vestiging aanvaardt. Een kassa in een andere vestiging kan de bon toch tonen en de klant zeggen waar hij hem kan gebruiken, want verify geeft daar BRANCH_REQUIRED.

Een gedeactiveerde klant krijgt zijn lijst nog, want zijn bonnen zijn nog te verzilveren.

Fouten

CodeStatusBeschrijving
CUSTOMER_NOT_FOUND404Je hebt geen klant met dit ID

Typische kassaflow

  1. De klant toont zijn klantenkaart bij het afrekenen
  2. De kassa roept GET /customers/lookup aan om het saldo te tonen
  3. De klant kiest een beloning uit GET /rewards
  4. De kassa roept POST /redemptions aan om de beloning te verzilveren
  5. De klant krijgt een bevestigingscode (op het kasticket, via sms of op het scherm)
  6. Later, wanneer de klant de code toont, roept de kassa POST /redemptions/{code}/verify aan

Een klant die zonder code terugkomt, kan zijn beloning toch ophalen: na stap 2 roept de kassa GET /customers/{customer}/redemptions?status=valid aan, de kassier kiest de beloning uit die lijst, en de kassa verifieert de confirmation_code ervan.

Een klant die met een BON--code uit een campagnemail binnenkomt volgt een aparte, kortere weg: roep GET /vouchers/{code} aan om te zien wat de bon geeft, lees kind, kijk de korting of de producten na tegen de open verkoop, roep dan POST /vouchers/{code}/verify aan met een idempotency_key, en pas de korting toe of geef de producten mee.

Een klant met een campagnecadeau maar zonder de BON--code volgt dezelfde weg, met één aanroep meer: na stap 2 roept de kassa GET /customers/{customer}/vouchers aan en de kassier kiest de bon uit die lijst.