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

VeldTypeVerplichtBeschrijving
identifierstringJaIdentifier van de klant (kaartnummer, e-mailadres, enz.)
idempotency_keystringJaUnieke sleutel die dubbele verwerking tegenhoudt
total_amountintegerJaTransactiebedrag in eurocenten. Een geheel getal, minimaal 1, dat in een signed 32-bit integer past.
descriptionstringNeeBeschrijving van de aankoop
external_referencestringNeeVerwijzing naar de transactie in je eigen systeem
branchstringNeeJe 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.
itemsarrayNeeOrderregels (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:

VeldTypeVerplichtBeschrijving
namestringJaProductnaam
quantityintegerJaAantal, minimaal 1, signed 32-bit integer
unit_priceintegerJaPrijs per stuk in eurocenten, signed 32-bit integer
skustringNeeProduct-SKU / code
categorystringNeeProductcategorie (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.

VeldTypeBeschrijving
rule_idintegerHet ID van de verdienregel, of van de campagne wanneer rule_type campaign is
rule_namestringJe eigen naam voor de regel of campagne. De server vertaalt die nooit.
rule_typestringbase_rate of campaign
points_earnedintegerPunten die dit item heeft bijgedragen. 0 is een normale waarde. Zie hieronder.
familystring|nulltransaction of customer_moment. null voor een verdienregel.
momentstring|nullHet klantmoment dat zich voordeed, bij een customer_moment-campagne. Anders null.
campaign_versioninteger|nullDe campagneversie die gold toen de aankoop werd beoordeeld. null voor een verdienregel.
suppressed_byinteger|nullHet ID van de campagne die deze verdrong. null als er niets verdrong.
reasonstring|nullStabiel token dat een uitkomst van nul punten verklaart. Momenteel alleen suppressed_by_stronger_campaign.
line_breakdownarray|nullDetail 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_amount niet 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" en suppressed_by op 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_earned en rules_applied wat 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

CodeStatusBeschrijving
BRANCH_INACTIVE422De 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_FOUND422Geen 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_DEACTIVATED422De klant bestaat, maar is gedeactiveerd en kan dus geen punten sparen
CUSTOMER_NOT_FOUND404Geen klant gevonden met de opgegeven identifier
PLAN_LIMIT_EXCEEDED429Maandelijkse transactielimiet bereikt

Klanttransacties weergeven

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

Haal de transacties van één klant op, gepagineerd.

Padparameters

ParameterTypeBeschrijving
customerstringHet klant-ID

Queryparameters

ParameterTypeVerplichtBeschrijving
date_fromstringNeeAlleen transacties die op of na dit moment zijn aangemaakt. Puntjes vergelijkt met created_at, dus een datum zonder tijdstip begint om middernacht UTC
date_tostringNeeAlleen 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
        }
    }
}