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

ParameterTypeBeschrijving
customerstringHet klant-ID

Response

{
    "data": {
        "id": 1,
        "customer_id": 1,
        "balance": 1500,
        "expiring_soon": 200,
        "created_at": "2024-03-15T12:30:00Z",
        "updated_at": "string"
    }
}
VeldBeschrijving
balanceHet huidige totale puntensaldo
expiring_soonPunten 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

CodeStatusBeschrijving
CUSTOMER_NOT_FOUND404Geen klant gevonden met het opgegeven ID
WALLET_NOT_FOUND404De 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

ParameterTypeBeschrijving
customerstringHet klant-ID

Query parameters

ParameterTypeVerplichtStandaardBeschrijving
date_fromstringNee—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_tostringNee—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
typestringNee—Eén van earn, adjust, redeem of expire
pageintegerNee1Welke 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

TypeTeken van het bedragBeschrijving
earnPositiefPunten verdiend met een transactie
adjustPositief of negatiefHandmatige saldo-aanpassing
redeemNegatiefPunten besteed aan een beloning
expireNegatiefPunten die vervallen zijn

Fouten

CodeStatusBeschrijving
CUSTOMER_NOT_FOUND404Geen klant gevonden met het opgegeven ID
WALLET_NOT_FOUND404De 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

ParameterTypeBeschrijving
customerstringHet klant-ID

Request body

VeldTypeVerplichtBeschrijving
amountintegerJaPunten die je bijboekt (positief) of afboekt (negatief). Een geheel getal, niet 0, dat in een signed 32-bit integer past.
reasonstringJaToelichting bij de aanpassing (max. 500 tekens)
idempotency_keystringJaUnieke 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

CodeStatusBeschrijving
CUSTOMER_DEACTIVATED422De 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_FOUND404Geen klant gevonden met het opgegeven ID
IDEMPOTENCY_KEY_CONFLICT422Deze idempotency_key is al gebruikt, voor een ander bedrag
INSUFFICIENT_BALANCE422De aanpassing zou het saldo onder nul brengen
WALLET_NOT_FOUND404De 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

ParameterTypeBeschrijving
customerstringHet klant-ID

Query parameters

ParameterTypeVerplichtBeschrijving
platformstringJaVoor 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

CodeStatusBeschrijving
APPLE_WALLET_UNAVAILABLE503Apple 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_DEACTIVATED422De 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_FOUND404Geen 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

ParameterTypeBeschrijving
customerstringHet klant-ID

Request body

VeldTypeVerplichtBeschrijving
channelstringNeeWaar 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

CodeStatusBeschrijving
CARD_SEND_THROTTLED429Deze klant kreeg zijn kaart net al toegestuurd; wacht details.retry_after seconden
CUSTOMER_DEACTIVATED422De klant is gestopt met het programma, dus er wordt geen kaart verstuurd
CUSTOMER_EMAIL_SUPPRESSED422Mail 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_EMAIL422De klant heeft geen e-mailadres, dus er is geen adres om de kaart naartoe te sturen
CUSTOMER_NOT_FOUND404Geen 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

ParameterTypeBeschrijving
externalIdstringDe external_id die je deze klant in je eigen systeem hebt gegeven. Een id met / kun je hier niet gebruiken

Request body

VeldTypeVerplichtBeschrijving
channelstringNeeWaar 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

CodeStatusBeschrijving
CARD_SEND_THROTTLED429Deze klant kreeg zijn kaart net al toegestuurd; wacht details.retry_after seconden
CUSTOMER_DEACTIVATED422De klant is gestopt met het programma, dus er wordt geen kaart verstuurd
CUSTOMER_EMAIL_SUPPRESSED422Mail 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_EMAIL422De klant heeft geen e-mailadres, dus er is geen adres om de kaart naartoe te sturen
EXTERNAL_ID_NOT_FOUND404Geen klant van jou heeft deze external_id. Een gedeactiveerde klant wordt wél gevonden en geweigerd met CUSTOMER_DEACTIVATED