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 vervallen.
Path parameters
| 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 in de komende 30 dagen vervallen. Elk punt dat al een einddatum heeft, telt mee, ook als de zaak het vervallen later uitzette of naar een plan zonder vervallen ging |
Fouten
| Code | Status | Beschrijving |
|---|---|---|
CUSTOMER_NOT_FOUND | 404 | Geen klant gevonden met het opgegeven ID |
WALLET_NOT_FOUND | 404 | De klant heeft geen wallet |
Grootboekregels opvragen
GET /api/v1/customers/{customer}/ledger
Geeft alle puntbewegingen in de wallet van een klant terug, gepagineerd en met de nieuwste eerst.
Path parameters
| Parameter | Type | Beschrijving |
|---|---|---|
customer | string | Het klant-ID |
Query parameters
| Parameter | Type | Verplicht | Standaard | 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 |
page | integer | Nee | 1 | Welke pagina je terugkrijgt. Een waarde onder 1, of een waarde die geen geheel getal is, geeft pagina 1 |
Response
{
"data": {
"data": [
{
"id": 1,
"wallet_id": 1,
"type": "adjust",
"amount": 250,
"running_balance": 1500,
"reason": "Goodwill after a late delivery",
"causer_type": "api_client",
"causer_id": "1",
"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 vervallen zijn |
Fouten
| Code | Status | Beschrijving |
|---|---|---|
CUSTOMER_NOT_FOUND | 404 | Geen klant gevonden met het opgegeven ID |
WALLET_NOT_FOUND | 404 | De klant heeft geen wallet |
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.
Boek je punten bij, dan vervallen die net als gespaarde punten. Boek je punten af, dan gaan eerst de punten weg die het eerst vervallen.
Path parameters
| 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": "Goodwill after a late delivery",
"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":"Goodwill after a late delivery","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": "adjust",
"amount": 250,
"running_balance": 1500,
"reason": "Goodwill after a late delivery",
"causer_type": "api_client",
"causer_id": "1",
"created_at": "2024-03-15T12:30:00Z"
}
}
Idempotentie
Een idempotency_key is uniek per klant, en daarbij tellen ook de aanpassingen
mee van elk account dat de zaak met deze klant samenvoegde. Pas je aan met een
sleutel die al bestaat voor deze klant, of voor een account dat met deze klant
is samengevoegd, 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.
Een retry werkt ook na een samenvoeging. Stuur je hem naar de klant die bleef,
dan vindt de sleutel ook de aanpassingen op de accounts die met die klant zijn
samengevoegd. Stuur je hem naar het gesloten account, dan krijg je de aanpassing
terug die vóór de samenvoeging met die sleutel gebeurde. Een nieuwe sleutel wordt
daar geweigerd met CUSTOMER_DEACTIVATED.
Een herhaling naar de klant die bleef, kan een aanpassing teruggeven die op een
samengevoegd account gebeurde. Die regel houdt de wallet_id van het gesloten
account, en zijn running_balance is het saldo van dat account net na de
aanpassing, niet het saldo van de klant die bleef.
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_DEACTIVATED | 422 | De zaak heeft deze klant samengevoegd met een ander account, dus deze wallet is gesloten. Een gedeactiveerde klant die de zaak niet samenvoegde, aanvaardt nog steeds aanpassingen |
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.
Path parameters
| Parameter | Type | Beschrijving |
|---|---|---|
customer | string | Het klant-ID |
Query parameters
| 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.
Mislukt het maken van de pas aan onze kant, bijvoorbeeld bij het ondertekenen voor Apple of bij de
dienst van Google, dan krijg je 500 INTERNAL_ERROR. Probeer het later opnieuw, en geef de
request_id mee als het blijft mislukken.
Elke zaak ondertekent haar Apple-passen met een eigen Apple-identiteit, die Puntjes bij Apple
aanvraagt zodra de zaak bestaat. Zolang Apple die niet klaar heeft, antwoordt platform=apple
met 503 APPLE_WALLET_UNAVAILABLE en een Retry-After-header in seconden. Is Apple nog bezig of
antwoordt Apple niet, dan vraagt Puntjes het hoogstens één keer per minuut per zaak opnieuw, dus
wie eerder opnieuw probeert, krijgt meteen hetzelfde antwoord. Heeft Apple de identiteit
geweigerd, dan vraagt Puntjes het pas opnieuw als ons team de oorzaak heeft opgelost. Tot dan
blijft het antwoord 503. platform=google merkt hier niets van.
Fouten
| Code | Status | Beschrijving |
|---|---|---|
APPLE_WALLET_UNAVAILABLE | 503 | Apple is nog niet klaar met de Apple-identiteit van de zaak, dus er kan geen Apple-pas ondertekend worden. Probeer opnieuw na het aantal seconden in Retry-After |
CUSTOMER_DEACTIVATED | 422 | De zaak heeft deze klant samengevoegd met een ander account, dus er komt geen nieuwe pas. Een gedeactiveerde klant die de zaak niet samenvoegde, krijgt er nog wel een |
CUSTOMER_NOT_FOUND | 404 | Geen klant van jou heeft dit ID, of de klant is geanonimiseerd en krijgt geen pas meer |
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.
Path parameters
| 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
}
}
Drie 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 adres waar we niet meer naar mailen, krijgt
CUSTOMER_EMAIL_SUPPRESSED: eerdere mail kwam terug, of jouw mail werd als spam
gemarkeerd. Komt mail terug, dan geldt dat voor elke zaak op Puntjes: een adres dat bij
een andere zaak niet aankwam, weigeren we ook bij jou. Vraag de klant dan om een adres
dat werkt, of zet het versturen weer aan op zijn pagina in het beheerportaal. 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_EMAIL_SUPPRESSED | 422 | Mail naar dit adres kwam terug, bij jouw zaak of bij een andere, of jouw mail werd als spam gemarkeerd. Daarom mailen we het niet meer. Verbeter het adres, of zet het versturen weer aan in het beheerportaal |
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.
Path parameters
| Parameter | Type | Beschrijving |
|---|---|---|
externalId | string | De external_id die je deze klant in je eigen systeem hebt gegeven. Een id met / kun je hier niet gebruiken |
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_EMAIL_SUPPRESSED | 422 | Mail naar dit adres kwam terug, bij jouw zaak of bij een andere, of jouw mail werd als spam gemarkeerd. Daarom mailen we het niet meer. Verbeter het adres, of zet het versturen weer aan in het beheerportaal |
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 |