API-referentie
Wallet-endpoints
Endpoints om wallet-saldo's op te vragen, het puntengrootboek te doorlopen en het saldo handmatig aan te passen.
Wallet-saldo ophalen
GET /api/v1/customers/{customer}/wallet
Geeft het huidige wallet-saldo van een klant terug, met daarbij de punten die binnenkort verlopen.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
customer | string | Het klant-ID |
Response
{
"data": {
"id": 1,
"customer_id": 1,
"balance": 1500,
"expiring_soon": 200,
"created_at": "2024-03-15T12:30:00Z",
"updated_at": "string"
}
}
| Veld | Beschrijving |
|---|---|
balance | Het huidige totale puntensaldo |
expiring_soon | Punten die binnenkort verlopen (als het verlopen van punten aanstaat) |
Grootboekregels opvragen
GET /api/v1/customers/{customer}/ledger
Geeft alle puntbewegingen in de wallet van een klant terug, gepagineerd en met de nieuwste eerst.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
customer | string | Het klant-ID |
Queryparameters
| Parameter | Type | Verplicht | Beschrijving |
|---|---|---|---|
date_from | string | Nee | Alleen regels die op of na dit moment zijn aangemaakt. De vergelijking loopt over created_at, dus een datum zonder tijd begint om middernacht UTC |
date_to | string | Nee | Alleen regels die op of vóór dit moment zijn aangemaakt. Een datum zonder tijd stopt dus om middernacht en laat die dag zelf buiten beschouwing |
type | string | Nee | Eén van earn, adjust, redeem of expire |
Pagineren doe je met ?page=. De paginator leest die zelf uit de querystring, dus hij
staat niet in de parametertabel hierboven.
Response
{
"data": {
"data": [
{
"id": 1,
"wallet_id": 1,
"type": "earn",
"amount": 250,
"running_balance": 1500,
"reason": "Points earned from transaction #42",
"causer_type": "user",
"causer_id": "0",
"created_at": "2024-03-15T12:30:00Z"
}
],
"links": {
"first": "string",
"last": "string",
"prev": "string",
"next": "string"
},
"meta": {
"current_page": 1,
"last_page": 2,
"per_page": 15,
"total": 20
}
}
}
Typen grootboekregels
| Type | Teken van het bedrag | Beschrijving |
|---|---|---|
earn | Positief | Punten verdiend met een transactie |
adjust | Positief of negatief | Handmatige saldo-aanpassing |
redeem | Negatief | Punten besteed aan een beloning |
expire | Negatief | Punten die verlopen zijn |
Wallet-saldo aanpassen
POST /api/v1/customers/{customer}/wallet/adjust
Boekt handmatig punten bij of af op de wallet van een klant, en legt dat vast als een nieuwe grootboekregel van het type adjust.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
customer | string | Het klant-ID |
Request body
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
amount | integer | Ja | Punten die je bijboekt (positief) of afboekt (negatief). Een geheel getal, niet 0, dat in een signed 32-bit integer past. |
reason | string | Ja | Toelichting bij de aanpassing (max. 500 tekens) |
idempotency_key | string | Ja | Unieke sleutel die dubbele aanpassingen voorkomt (max. 255 tekens) |
Example request
{
"amount": 250,
"reason": "Points earned from transaction #42",
"idempotency_key": "goodwill-2024-0042"
}
curl -X POST https://puntjes.app/api/v1/customers/1/wallet/adjust \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"amount":250,"reason":"Points earned from transaction #42","idempotency_key":"goodwill-2024-0042"}'
De aangemaakte grootboekregel, in dezelfde vorm als de regels die
GET /customers/{customer}/ledger teruggeeft. running_balance is het wallet-saldo
ná de aanpassing.
Response
{
"data": {
"id": 1,
"wallet_id": 1,
"type": "earn",
"amount": 250,
"running_balance": 1500,
"reason": "Points earned from transaction #42",
"causer_type": "user",
"causer_id": "0",
"created_at": "2024-03-15T12:30:00Z"
}
}
Idempotentie
De idempotency_key moet uniek zijn per wallet. Pas je aan met een
idempotency_key die al bestaat voor deze klant en is het amount
hetzelfde, dan:
- wordt de oorspronkelijke grootboekregel teruggegeven
- wordt het saldo niet een tweede keer verplaatst
- wordt er geen nieuwe grootboekregel aangemaakt
Daardoor kun je deze aanroep veilig opnieuw sturen na een netwerkfout. Een
herhaling krijgt ook antwoord wanneer het saldo de aanpassing niet meer kan
dragen: de oorspronkelijke aanpassing is al verwerkt, dus een retry faalt nooit
met INSUFFICIENT_BALANCE.
Komt dezelfde sleutel terug met een ander amount, dan is dat een andere
aanpassing en geen retry. Die wordt geweigerd met IDEMPOTENCY_KEY_CONFLICT en
er wordt niets weggeschreven. Geef elke aanpassing dus een eigen sleutel. Het
reason wordt niet vergeleken: een kassa mag dat tussen twee pogingen anders
formuleren zonder er iets anders mee te bedoelen.
Negatieve aanpassingen
Controleer eerst of de klant genoeg saldo heeft voor je punten afboekt. Een negatieve aanpassing die het saldo onder nul zou brengen, wordt geweigerd.
Fouten
| Code | Status | Beschrijving |
|---|---|---|
CUSTOMER_NOT_FOUND | 404 | Geen klant gevonden met het opgegeven ID |
IDEMPOTENCY_KEY_CONFLICT | 422 | Deze idempotency_key is al gebruikt, voor een ander bedrag |
INSUFFICIENT_BALANCE | 422 | De aanpassing zou het saldo onder nul brengen |
WALLET_NOT_FOUND | 404 | De klant heeft geen wallet |
Een wallet-pas downloaden
GET /api/v1/customers/{customer}/wallet-pass
Bouwt de loyaltypas van de klant voor Apple Wallet of Google Wallet. Puntjes leest het saldo op het moment dat je dit endpoint aanroept, dus een pas draagt nooit een verouderd cijfer.
De twee platformen antwoorden verschillend. apple geeft het ondertekende .pkpass-bestand
zelf terug, als bijlage. Geef die bytes rechtstreeks door aan de browser of app van de klant.
google geeft JSON terug met een link waarmee de klant de pas bewaart. Bewust geen redirect:
de aanroeper is meestal een kassa of een webshop-backend die de URL nodig heeft om er een
knop van te maken.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
customer | string | Het klant-ID |
Queryparameters
| Parameter | Type | Verplicht | Beschrijving |
|---|---|---|---|
platform | string | Ja | Voor welke wallet je bouwt: apple of google |
Example request
curl https://puntjes.app/api/v1/customers/1/wallet-pass?platform=google \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response
{
"data": {
"save_url": "https://pay.google.com/gp/v/save/eyJhbGciOiJSUzI1NiJ9"
}
}
Het voorbeeld hierboven is de google-variant. Met platform=apple krijg je
application/vnd.apple.pkpass terug met een Content-Disposition: attachment-header, geen JSON.
Fouten
| Code | Status | Beschrijving |
|---|---|---|
CUSTOMER_NOT_FOUND | 404 | Geen klant gevonden met het opgegeven ID |
De klantenkaart naar de klant sturen
POST /api/v1/customers/{customer}/send-card
Mail de klant zijn klantenkaart terwijl hij nog aan de kassa staat. Het is dezelfde
mail als die van de verstuurknop in het beheerportaal: de ondertekende .pkpass als
bijlage, een "Toevoegen aan Google Wallet"-knop en de QR-code van de kaart om in de
winkel te scannen.
Het versturen loopt via een wachtrij, dus 202 betekent dat de mail is aangenomen
om te versturen. Niets in de response zegt dat hij is aangekomen.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
customer | string | Het klant-ID |
Request body
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
channel | string | Nee | Waar de kaart naartoe gaat. email is vandaag het enige kanaal waarop Puntjes kan bezorgen, en het veld weglaten betekent email. |
Example request
{
"channel": "email"
}
curl -X POST https://puntjes.app/api/v1/customers/1/send-card \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel":"email"}'
Response (202 Accepted)
{
"data": {
"customer_id": 1,
"channel": "email",
"queued": true
}
}
Twee weigeringen vang je best apart op. Een klant van wie je geen e-mailadres hebt
krijgt CUSTOMER_HAS_NO_EMAIL en geen algemene validatiefout, zodat de kassier
ernaar kan vragen. Een tweede verzending voor dezelfde klant binnen de wachttijd
krijgt CARD_SEND_THROTTLED, met details.retry_after in seconden. Dat is een
kassier die twee keer drukt, niet je integratie die over haar plan heen gaat, en na
60 seconden lost het zichzelf op. Een andere channel dan email geeft
VALIDATION_ERROR, en een gedeactiveerde klant geeft CUSTOMER_DEACTIVATED.
Er is hier geen idempotency_key, anders dan bij de endpoints die punten
verplaatsen. De wachttijd maakt opnieuw proberen veilig: een tweede poging binnen
die tijd wordt geweigerd in plaats van verstuurd. Een sleutel had trouwens niet
geholpen, want tien keer drukken levert tien sleutels op.
Marketingtoestemming speelt hier geen rol. De klant vraagt aan de kassa om zijn eigen kaart, dus een afmelding voor campagnemails blokkeert dit niet.
Fouten
| Code | Status | Beschrijving |
|---|---|---|
CARD_SEND_THROTTLED | 429 | Deze klant kreeg zijn kaart net al toegestuurd; wacht details.retry_after seconden |
CUSTOMER_DEACTIVATED | 422 | De klant is gestopt met het programma, dus er wordt geen kaart verstuurd |
CUSTOMER_HAS_NO_EMAIL | 422 | De klant heeft geen e-mailadres, dus er is geen adres om de kaart naartoe te sturen |
CUSTOMER_NOT_FOUND | 404 | Geen klant van jou heeft dit ID. Een gedeactiveerde klant wordt wél gevonden en geweigerd met CUSTOMER_DEACTIVATED |
De klantenkaart versturen via je eigen id
POST /api/v1/customers/by-external-id/{externalId}/send-card
Dezelfde verzending als hierboven, maar je bereikt ze via de external_id die jij de
klant gaf. Een kassasysteem hoeft ons numerieke id dus niet bij te houden om op deze
knop te drukken. Heb je beide, kies dan de id-variant: dat scheelt één opzoeking in
de index.
De wachttijd hangt aan de klant en niet aan de route, dus afwisselen tussen de twee varianten kan de mails niet verdubbelen.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
externalId | string | De external_id die je deze klant in je eigen systeem hebt gegeven |
Request body
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
channel | string | Nee | Waar de kaart naartoe gaat. email is vandaag het enige kanaal waarop Puntjes kan bezorgen, en het veld weglaten betekent email. |
Example request
{
"channel": "email"
}
curl -X POST https://puntjes.app/api/v1/customers/by-external-id/POS-4471/send-card \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel":"email"}'
Response (202 Accepted)
{
"data": {
"customer_id": 1,
"channel": "email",
"queued": true
}
}
Fouten
| Code | Status | Beschrijving |
|---|---|---|
CARD_SEND_THROTTLED | 429 | Deze klant kreeg zijn kaart net al toegestuurd; wacht details.retry_after seconden |
CUSTOMER_DEACTIVATED | 422 | De klant is gestopt met het programma, dus er wordt geen kaart verstuurd |
CUSTOMER_HAS_NO_EMAIL | 422 | De klant heeft geen e-mailadres, dus er is geen adres om de kaart naartoe te sturen |
EXTERNAL_ID_NOT_FOUND | 404 | Geen klant van jou heeft deze external_id. Een gedeactiveerde klant wordt wél gevonden en geweigerd met CUSTOMER_DEACTIVATED |