API-referentie
Transactie-endpoints
Endpoints om aankopen vast te leggen en de transactiegeschiedenis te bekijken. Zodra je een transactie indient, kent Puntjes automatisch punten toe volgens je actieve verdienregels.
Een transactie indienen
POST /api/v1/transactions
Leg een aankoop vast en laat de klant er punten mee sparen. Je wijst de klant aan met een van diens actieve identifiers.
Request body
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
identifier | string | Ja | Identifier van de klant (kaartnummer, e-mailadres, enz.) |
idempotency_key | string | Ja | Unieke sleutel die dubbele verwerking tegenhoudt |
total_amount | integer | Ja | Transactiebedrag in eurocenten. Een geheel getal, minimaal 1, dat in een signed 32-bit integer past. |
description | string | Nee | Beschrijving van de aankoop |
external_reference | string | Nee | Verwijzing naar de transactie in je eigen systeem |
branch | string | Nee | Je eigen sleutel voor de vestiging waar deze aankoop gebeurde: de external_id die je eraan gaf, maximaal 64 tekens. Die sleutel gaat voor op de vestiging die de API-credential standaard gebruikt, zodat één gedeelde credential per request een andere winkel kan noemen. Laat het veld weg en de standaard van de credential geldt; laat beide weg en de aankoop krijgt geen vestiging. |
items | array | Nee | Orderregels (zie hieronder). Optioneel. Laat het veld weg voor een transactie zonder regels. |
Orderregels
Stuur items mee om vast te leggen wat er precies gekocht is. Die regels voeden je
statistieken: topproducten, omzet per categorie en ordervolume. Elke regel:
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
name | string | Ja | Productnaam |
quantity | integer | Ja | Aantal, minimaal 1, signed 32-bit integer |
unit_price | integer | Ja | Prijs per stuk in eurocenten, signed 32-bit integer |
sku | string | Nee | Product-SKU / code |
category | string | Nee | Productcategorie (gebruikt voor omzet-per-categorie) |
line_total wordt server-side berekend
Puntjes berekent de line_total per regel zelf als quantity × unit_price; stuur die dus niet mee. De regels
hoeven niet samen op total_amount uit te komen: dat totaal mag btw, korting of afronding bevatten. Regels
worden alleen bij de eerste aanmaak opgeslagen, en een idempotente herhaling dupliceert ze nooit.
Example request
{
"identifier": "CARD-001",
"idempotency_key": "order-2024-001",
"total_amount": 2500,
"description": "Lunch order",
"external_reference": "POS-42-001",
"branch": "string",
"items": [
{
"name": "Latte",
"quantity": 2,
"unit_price": 350,
"sku": "COF-01",
"category": "drinks"
}
]
}
curl -X POST https://puntjes.app/api/v1/transactions \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"identifier":"CARD-001","idempotency_key":"order-2024-001","total_amount":2500,"description":"Lunch order","external_reference":"POS-42-001","branch":"string","items":[{"name":"Latte","quantity":2,"unit_price":350,"sku":"COF-01","category":"drinks"}]}'
Response (201 Created)
{
"data": {
"id": 42,
"customer_id": 1,
"idempotency_key": "order-2024-001",
"total_amount": 2500,
"description": "Lunch order",
"external_reference": "POS-42-001",
"branch": "string",
"created_at": "2024-03-15T12:30:00Z",
"points_earned": 500,
"rules_applied": [
{
"campaign_version": null,
"family": null,
"line_breakdown": null,
"moment": null,
"points_earned": 250,
"reason": null,
"rule_id": 3,
"rule_name": "Standaardtarief",
"rule_type": "base_rate",
"suppressed_by": null
}
],
"items": [
{
"id": 101,
"name": "Latte",
"sku": "COF-01",
"quantity": 2,
"unit_price": 350,
"line_total": 700,
"category": "drinks"
}
]
}
}
De rules_applied-uitsplitsing
rules_applied is het volledige overzicht van alles wat voor deze aankoop is beoordeeld: zowel je vaste
verdienregels als je campagnes. Elk item draagt dezelfde tien sleutels. De laatste zes staan er
altijd in en zijn null voor alles wat geen campagne is.
| Veld | Type | Beschrijving |
|---|---|---|
rule_id | integer | Het ID van de verdienregel, of van de campagne wanneer rule_type campaign is |
rule_name | string | Je eigen naam voor de regel of campagne. De server vertaalt die nooit. |
rule_type | string | base_rate of campaign |
points_earned | integer | Punten die dit item heeft bijgedragen. 0 is een normale waarde. Zie hieronder. |
family | string|null | transaction of customer_moment. null voor een verdienregel. |
moment | string|null | Het klantmoment dat zich voordeed, bij een customer_moment-campagne. Anders null. |
campaign_version | integer|null | De campagneversie die gold toen de aankoop werd beoordeeld. null voor een verdienregel. |
suppressed_by | integer|null | Het ID van de campagne die deze verdrong. null als er niets verdrong. |
reason | string|null | Stabiel token dat een uitkomst van nul punten verklaart. Momenteel alleen suppressed_by_stronger_campaign. |
line_breakdown | array|null | Detail per orderregel bij een productgerichte toekenning met vaste punten. Anders null. |
rule_type `campaign` verving het oude `multiplier`-item
Bonusvermenigvuldigers horen bij campagnes, niet bij verdienregels. Vertakt je client op rule_type, dan moet hij
campaign aankunnen: de waarde multiplier komt niet meer voor in rules_applied. Ook de sleutelnamen zijn
veranderd. Elk item gebruikt rule_id / rule_name / rule_type / points_earned, niet name / type /
points.
Items met nul punten worden gerapporteerd, niet weggelaten
Een item met points_earned: 0 is informatief en verschijnt in twee situaties:
- Een verdienregel die nergens op aansloeg, bijvoorbeeld een regel waarvan de aankoop de
min_transaction_amountniet haalde. Elke actieve verdienregel staat in de lijst, of ze nu punten opleverde of niet. - Een verdrongen campagne. Slaan twee campagnes aan die een aankoop niet willen delen, dan betaalt
alleen de sterkste. De andere komen terug met
points_earned: 0,reason: "suppressed_by_stronger_campaign"ensuppressed_byop het ID van de winnaar. Bij gelijkspel wint het laagste campagne-ID. Campagnes die als combineerbaar gemarkeerd staan, zijn uitgezonderd en betalen allemaal uit.
Campagnepunten worden berekend op wat het basistarief echt heeft toegekend. Blijft een aankoop onder
de min_transaction_amount van de basisregel, dan levert ze geen basispunten op, en dus ook geen
vermenigvuldigingsbonus daarbovenop.
Productgerichte campagnes en sku
Een campagne die tot bepaalde producten beperkt is, geeft alleen bonus op orderregels waarvan de sku
exact overeenkomt met een van de producten die op de campagne staan. Orderregels zonder sku, of met
een sku die de campagne niet vermeldt, krijgen geen bonus en veroorzaken nooit een fout. Stuur je
alleen total_amount mee en geen items, dan speelt dat productbereik geen rol.
Betaalt zo'n campagne uit, dan laat line_breakdown zien hoe het totaal tot stand kwam:
{
"rule_id": 9,
"rule_name": "Koffiebonus",
"rule_type": "campaign",
"points_earned": 100,
"family": "transaction",
"moment": null,
"campaign_version": 3,
"suppressed_by": null,
"reason": null,
"line_breakdown": [{ "sku": "COF-01", "quantity": 2, "points": 100 }]
}
line_breakdown kan hoger optellen dan points_earned
De uitsplitsing noemt elke orderregel die overeenkomt. Pas daarna legt de campagne haar plafond per aankoop op het
eindtotaal. Kapt dat plafond de toekenning af, dan blijven de regels tonen wat er overeenkwam, en hun som kan dus
hoger liggen dan points_earned.
Idempotentie
De idempotency_key moet uniek zijn binnen je eigen zaak. Dien je een transactie in met een idempotency_key die al bestaat, dan:
- krijg je de oorspronkelijke transactie terug
- worden er geen punten opnieuw toegekend
- komt er geen nieuwe grootboekregel bij
- melden
points_earnedenrules_appliedwat de eerste aanroep toekende, niet nul
Dat is essentieel voor kassasystemen, waar een nieuwe poging na een netwerkfout dezelfde aanroep twee keer kan versturen.
points_earned is betrouwbaar bij een herhaling
Een herhaling geeft dezelfde points_earned en dezelfde rules_applied-uitsplitsing
terug als de eerste aanroep, inclusief elke regel die 0 punten opleverde. Je
verwerkt de response van een herhaalde aanroep dus precies zoals die van de eerste.
Transacties van vóór dit gedrag hebben geen opgeslagen uitsplitsing: een herhaling
daarvan meldt nog altijd points_earned: 0. Hun grootboekregels veranderen niet en
blijven de vastlegging van wat er werkelijk is toegekend.
Kies idempotency keys zorgvuldig
Gebruik een waarde die de aankoop uniek maakt in je eigen systeem, zoals een bestelnummer of een bonnummer. Neem geen willekeurige UUID die bij elke nieuwe poging verandert: dan verlies je net de bescherming waarvoor de sleutel bestaat.
Fouten
| Code | Status | Beschrijving |
|---|---|---|
BRANCH_INACTIVE | 422 | De vestiging bestaat, maar je hebt ze gedeactiveerd. Puntjes weigert alleen een NIEUWE transactie. Stuur je een transactie opnieuw die al vastlag voor je de vestiging sloot, dan krijg je gewoon de originele terug: een kassa die een oude verkoop nog eens doorstuurt, strandt dus nooit op een winkel die intussen dicht is. |
BRANCH_NOT_FOUND | 422 | Geen enkele van je vestigingen heeft deze sleutel. Een gedeactiveerde vestiging telt nog steeds als gevonden, dus deze fout zegt dat de sleutel fout is en niet dat de winkel dicht is. |
CUSTOMER_DEACTIVATED | 422 | De klant bestaat, maar is gedeactiveerd en kan dus geen punten sparen |
CUSTOMER_NOT_FOUND | 404 | Geen klant gevonden met de opgegeven identifier |
PLAN_LIMIT_EXCEEDED | 429 | Maandelijkse transactielimiet bereikt |
Klanttransacties weergeven
GET /api/v1/customers/{customer}/transactions
Haal de transacties van één klant op, gepagineerd.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
customer | string | Het klant-ID |
Queryparameters
| Parameter | Type | Verplicht | Beschrijving |
|---|---|---|---|
date_from | string | Nee | Alleen transacties die op of na dit moment zijn aangemaakt. Puntjes vergelijkt met created_at, dus een datum zonder tijdstip begint om middernacht UTC |
date_to | string | Nee | Alleen transacties die op of vóór dit moment zijn aangemaakt. Een datum zonder tijdstip stopt dus om middernacht, waardoor die dag zelf erbuiten valt |
Pagineren doe je met ?page=. De paginator leest die parameter zelf uit de querystring,
en daarom staat hij nooit in de parametertabel hierboven.
Response
{
"data": {
"data": [
{
"id": 42,
"customer_id": 1,
"idempotency_key": "order-2024-001",
"total_amount": 2500,
"description": "Lunch order",
"external_reference": "POS-42-001",
"branch": "string",
"created_at": "2024-03-15T12:30:00Z",
"points_earned": 500,
"rules_applied": [
{
"campaign_version": null,
"family": null,
"line_breakdown": null,
"moment": null,
"points_earned": 250,
"reason": null,
"rule_id": 3,
"rule_name": "Standaardtarief",
"rule_type": "base_rate",
"suppressed_by": null
}
],
"items": [
{
"id": 101,
"name": "Latte",
"sku": "COF-01",
"quantity": 2,
"unit_price": 350,
"line_total": 700,
"category": "drinks"
}
]
}
],
"links": {
"first": "string",
"last": "string",
"prev": "string",
"next": "string"
},
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 15,
"total": 42
}
}
}