API-referentie

Klant-endpoints

Endpoints voor het opzoeken, registreren, bijwerken, koppelen en bekijken van klanten.

status.label is altijd Engels

De status van een klant komt binnen als { "value": "active", "label": "Active" }. Deze API kent geen taal per request en vertaalt niets aan de serverkant: in label staat het Engelse woord, welke taal je integratie ook spreekt. Vertaal value zelf. Dat is active, deactivated of anonymized, en precies die drie vertaalt het Puntjes-portaal.


Een klant opzoeken

GET /api/v1/customers/lookup

Zoek een klant op via een van zijn actieve identifiers (de klantenkaartcode of het e-mailadres), of via de external_id die je die klant in je eigen systeem hebt gegeven.

Het zoeken is hoofdletterongevoelig: een scanner met Caps Lock aan vindt dezelfde klant als een scanner zonder. Het is ook bewust typeonafhankelijk. Puntjes vergelijkt de waarde met elke actieve identifier die bij jou hoort, wat het type ook is. Identifiers die je uitgaf voordat Puntjes de toegestane types beperkte, werken daardoor nog altijd. Wat je ooit aan een klant meegaf, blijft werken.

Queryparameters

ParameterTypeVerplichtBeschrijving
identifierstringNeeDe waarde waarmee je zoekt: een klantenkaartcode of een e-mailadres
external_idstringNeeJe eigen identifier voor deze klant, zoals meegegeven bij registratie

Geef er precies één mee. Beide weglaten levert een VALIDATION_ERROR op; stuur je ze allebei, dan heeft external_id voorrang.

curl https://puntjes.app/api/v1/customers/lookup?identifier=K7M2QX4P \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response

{
    "data": {
        "id": 1,
        "first_name": "Jan",
        "last_name": "De Vries",
        "email": "jan@example.com",
        "phone": "+31612345678",
        "external_id": "POS-4471",
        "date_of_birth": "1990-05-15",
        "customer_since": "2019-04-02",
        "locale": "nl",
        "status": {
            "value": "string",
            "label": "string"
        },
        "identifiers": [
            {
                "id": 1,
                "type": "loyalty_card",
                "value": "K7M2QX4P",
                "is_active": true,
                "is_primary": true,
                "created_at": "2024-03-15T12:30:00Z"
            }
        ],
        "marketingConsent": false,
        "marketingConsentGrantedAt": "string",
        "marketingConsentGrantedSource": "string",
        "marketingConsentWithdrawnAt": "string",
        "marketingConsentWithdrawnSource": "string",
        "loyalty_card_code": "K7M2QX4P",
        "deactivated_at": null,
        "anonymized_at": null,
        "created_at": "2024-03-15T12:30:00Z",
        "updated_at": "string"
    }
}

Fouten

CodeStatusBeschrijving
CUSTOMER_NOT_FOUND404Geen klant gevonden met de opgegeven identifier

Een klant registreren

POST /api/v1/customers

Maak een nieuwe klant aan. Puntjes maakt meteen een wallet en een klantenkaart aan.

Elke klant verlaat de registratie met precies één klantenkaart. Ofwel scan je een voorgedrukte kaart en koppelt Puntjes die, ofwel stuur je geen kaart mee en geeft Puntjes er zelf een uit. In beide gevallen krijg je de code terug als loyalty_card_code in de response.

Request body

VeldTypeVerplichtBeschrijving
first_namestringNeeVoornaam van de klant
last_namestringNeeAchternaam van de klant
emailstringNeeE-mailadres (uniek per handelaar)
phonestringNeeTelefoonnummer
external_idstringNeeJe eigen identifier voor deze klant (uniek per handelaar)
date_of_birthstringNeeGeboortedatum (YYYY-MM-DD)
customer_sincestringNeeDe datum waarop deze persoon klant werd (YYYY-MM-DD). Laat je hem weg, dan telt Puntjes vanaf de registratiedatum.
localestringNeeVoorkeurstaal: nl of en (standaard: nl)
marketing_consentbooleanNeeOpt-in voor campagnemail. Staat standaard uit.
identifiersarrayNeeArray van identifier-objecten. Mag weggelaten of leeg zijn.

Marketingtoestemming bij registratie

Stuur marketing_consent: true alleen als de klant voor je echt ja heeft gezegd. Puntjes legt het moment vast en noteert api als het kanaal waarlangs de toestemming binnenkwam. Die twee samen zijn het bewijs achter elke campagnemail die je die klant later stuurt.

Het veld weglaten en false sturen komen hier op hetzelfde neer: een klant die nog niet bestaat, heeft geen toestemming om in te trekken. Een integratie die dit veld nooit heeft gezien, registreert dus iedereen zonder opt-in, en dat is precies de bedoelde standaard. Een waarde die geen boolean is, weigert Puntjes met VALIDATION_ERROR.

De identifiers-array

Elk item is een object, geen losse string:

VeldTypeVerplichtBeschrijving
typestringJaemail of loyalty_card. Een andere waarde wordt niet geaccepteerd
valuestringJaDe identifierwaarde (maximaal 255 tekens)

Stuur hoogstens één loyalty_card-item per registratie mee: een klant draagt één fysieke kaart. Die loyalty_card-waarde moet overeenkomen met een kaart die jij al hebt aangemaakt en die nog aan niemand is toegewezen. Zie Registreren met een gescande kaart.

Je hoeft geen email-item mee te sturen om het adres doorzoekbaar te maken. Geef je email op de klant zelf mee, dan onderhoudt Puntjes de bijbehorende identifier voor je en houdt die gelijk met elke latere wijziging van het adres.

Registreren zonder kaart (aanbevolen)

Laat identifiers volledig weg, dan geeft Puntjes de kaart uit. Dit is de eenvoudigste integratie: je hoeft geen code te verzinnen en geen voorraad bij te houden.

{
    "first_name": "Jan",
    "last_name": "De Vries",
    "email": "jan@example.com",
    "phone": "+31612345678",
    "date_of_birth": "1990-05-15",
    "locale": "nl"
}
curl -X POST https://puntjes.app/api/v1/customers \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Jan","last_name":"De Vries","email":"jan@example.com"}'

Registreren met een gescande kaart

De klant kreeg een fysieke kaart: stuur de code mee die daarop gedrukt staat. Die kaart moet al als niet-toegewezen voorraad bij jou bestaan. Kaarten maak je aan in het adminportaal onder Klantenkaarten, en van daaruit stuur je ze naar een drukkerij.

{
    "first_name": "Jan",
    "last_name": "De Vries",
    "email": "jan@example.com",
    "identifiers": [{ "type": "loyalty_card", "value": "K7M2QX4P" }]
}
curl -X POST https://puntjes.app/api/v1/customers \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Jan","last_name":"De Vries","identifiers":[{"type":"loyalty_card","value":"K7M2QX4P"}]}'

Codes worden hoofdletterongevoelig vergeleken, dus een scanner die hoofdletters omdraait vindt nog altijd de juiste kaart. De opgeslagen identifier draagt altijd de schrijfwijze die op de kaart gedrukt staat, niet die van de scan.

Response (201 Created)

{
    "data": {
        "id": 1,
        "first_name": "Jan",
        "last_name": "De Vries",
        "email": "jan@example.com",
        "phone": "+31612345678",
        "external_id": "POS-4471",
        "date_of_birth": "1990-05-15",
        "customer_since": "2019-04-02",
        "locale": "nl",
        "status": {
            "value": "string",
            "label": "string"
        },
        "identifiers": [
            {
                "id": 1,
                "type": "loyalty_card",
                "value": "K7M2QX4P",
                "is_active": true,
                "is_primary": true,
                "created_at": "2024-03-15T12:30:00Z"
            }
        ],
        "marketingConsent": false,
        "marketingConsentGrantedAt": "string",
        "marketingConsentGrantedSource": "string",
        "marketingConsentWithdrawnAt": "string",
        "marketingConsentWithdrawnSource": "string",
        "loyalty_card_code": "K7M2QX4P",
        "deactivated_at": null,
        "anonymized_at": null,
        "created_at": "2024-03-15T12:30:00Z",
        "updated_at": "string"
    }
}

loyalty_card_code is de kaartcode van de klant, naar het hoogste niveau gehaald zodat je de identifiers-array niet hoeft te doorzoeken. Bewaar hem als de scanbare waarde van de klant zodra de registratie terugkomt. Daarop matcht een latere lookup.

Het is een nullable string. In de praktijk staat er alleen null bij klanten die al bestonden voordat er klantenkaarten waren, en bij klanten van wie de identifiers door anonimisering zijn gewist. Een geslaagde registratie geeft altijd een code terug.

Een klant kan meerdere kaarten hebben (maximaal vijf), bijvoorbeeld nadat hij er een verloor en aan de toonbank een vervangende kaart kreeg. loyalty_card_code geeft dan de ene kaart terug die als zijn scankaart is aangeduid, dezelfde die op zijn wallet-pas staat. Wie in het adminportaal een kaart toewijst, maakt daarmee die kaart de scankaart, dus in de praktijk is het de laatst toegewezen kaart. Een klant die uit een ander loyaltysysteem is gemigreerd, houdt de kaart die dat systeem als standaard had aangeduid.

Elke andere kaart blijft actief en blijft matchen op GET /customers/lookup, dus een code die je eerder bewaarde stopt nooit met werken. Lees loyalty_card_code opnieuw als je wilt tonen welke kaart de klant nu bij zich draagt.

Fouten

CodeStatusBeschrijving
EXTERNAL_ID_DUPLICATE409De external_id is al geregistreerd voor deze handelaar
IDENTIFIER_DUPLICATE409De identifier is al geregistreerd, ook als het om een kaart gaat die nu bij een andere klant hoort
LOYALTY_CARD_NOT_FOUND422De meegestuurde loyalty_card-waarde hoort niet bij een niet-toegewezen kaart van deze handelaar

LOYALTY_CARD_NOT_FOUND afhandelen aan de kassa

Een meegestuurde loyalty_card-waarde moet overeenkomen met een kaart die jij al hebt aangemaakt en die nog niemand in handen heeft. Een code die nooit is aangemaakt, een code van een andere handelaar en een verkeerd ingetypte code geven alle drie dezelfde 422. De API onthult bewust niet welke van de drie het is.

De oplossing is opnieuw scannen, of de request herhalen zonder de identifiers-array en Puntjes een kaart laten uitgeven. Bij deze fout maakt Puntjes geen klant, wallet of kaart aan, dus de tweede poging begint schoon.

Uniciteit

E-mailadressen en external_id-waarden moeten uniek zijn binnen één handelaar, net als elke identifierwaarde. Een duplicaat registreren levert een 409 op in plaats van een validatiefout, zodat je client "deze klant bestaat al" kan onderscheiden van "deze request klopt niet".


Een klant bijwerken

PUT /api/v1/customers/by-external-id/{externalId}

Werk een bestaande klant bij, opgezocht via de external_id die je zelf hebt toegekend. Dit is de schrijfkant van de POS-sync: een wijziging aan de kassa (een gecorrigeerd e-mailadres, een nieuw telefoonnummer) komt in Puntjes terecht zonder dat je ons klant-ID hoeft bij te houden.

PUT en PATCH gedragen zich identiek, en allebei doen ze een gedeeltelijke update: alleen de velden die in de body staan, veranderen. Een veld weglaten laat het ongemoeid. Expliciet null sturen maakt het leeg.

De external_id is de onveranderlijke sleutel en staat in de URL, dus die kun je hier niet wijzigen. Klantenkaarten raakt dit endpoint evenmin aan: die beheer je via hun eigen routes. De klant moet actief zijn. Verwijderde, gedeactiveerde en geanonimiseerde klanten vindt dit endpoint niet, en die leveren EXTERNAL_ID_NOT_FOUND op in plaats van stilzwijgend opnieuw te worden aangemaakt.

Padparameters

ParameterTypeBeschrijving
externalIdstringDe external_id die je deze klant in je eigen systeem hebt gegeven

Elk veld is optioneel: stuur alleen wat er veranderd is.

Request body

VeldTypeVerplichtBeschrijving
first_namestringNeeMax. 255 tekens
last_namestringNeeMax. 255 tekens
emailstringNeeGeldig e-mailadres, max. 255 tekens
phonestringNeeMax. 50 tekens
date_of_birthstringNeeYYYY-MM-DD
customer_sincestringNeeYYYY-MM-DD. Stuur null om hem te wissen en terug te vallen op de registratiedatum.
localestringNeenl of en
marketing_consentbooleanNeetrue geeft toestemming voor campagnemail, false trekt die in. Weglaten laat de opgeslagen keuze ongemoeid.

Wijzig je email, dan werkt Puntjes ook de e-mailidentifier van de klant bij. Die identifier beheert Puntjes voor je: zelf toevoegen of verwijderen hoef je nooit.

Het veld weglaten laat de opgeslagen keuze staan zoals hij is. false sturen trekt de toestemming in. Dat verschil is de hele reden dat het veld zich zo gedraagt: een kassa die elke avond een gecorrigeerd telefoonnummer doorstuurt, mag niet iedereen uitschrijven die in de sync zit. Niemand zou het merken, jij niet en de klant niet.

{
    "email": "jan.devries@example.com",
    "phone": "+31698765432"
}
curl -X PATCH https://puntjes.app/api/v1/customers/by-external-id/POS-4471 \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"jan.devries@example.com","phone":"+31698765432"}'

Geeft de volledige bijgewerkte klant terug, in dezelfde vorm als GET /customers/{customer}.

Response

{
    "data": {
        "id": 1,
        "first_name": "Jan",
        "last_name": "De Vries",
        "email": "jan@example.com",
        "phone": "+31612345678",
        "external_id": "POS-4471",
        "date_of_birth": "1990-05-15",
        "customer_since": "2019-04-02",
        "locale": "nl",
        "status": {
            "value": "string",
            "label": "string"
        },
        "identifiers": [
            {
                "id": 1,
                "type": "loyalty_card",
                "value": "K7M2QX4P",
                "is_active": true,
                "is_primary": true,
                "created_at": "2024-03-15T12:30:00Z"
            }
        ],
        "marketingConsent": false,
        "marketingConsentGrantedAt": "string",
        "marketingConsentGrantedSource": "string",
        "marketingConsentWithdrawnAt": "string",
        "marketingConsentWithdrawnSource": "string",
        "loyalty_card_code": "K7M2QX4P",
        "deactivated_at": null,
        "anonymized_at": null,
        "created_at": "2024-03-15T12:30:00Z",
        "updated_at": "string"
    }
}

Fouten

CodeStatusBeschrijving
EXTERNAL_ID_NOT_FOUND404Geen enkele actieve klant van jou heeft deze external_id

Een external ID koppelen

POST /api/v1/customers/link-external-id

Koppel je eigen external_id aan een klant die er nog geen heeft, gevonden via een identifier die de klant al draagt: de klantenkaartcode of het e-mailadres.

Dit is de backfill-route. Klanten die al bestonden voordat je je kassa koppelde, hebben geen external_id. PUT|PATCH /customers/by-external-id/{external_id} bereikt hen dus niet, en opnieuw registreren botst op hun identifiers. Koppel ze één keer, dan loopt elke latere update op je eigen ID.

Request body

VeldTypeVerplichtBeschrijving
identifierstringJaEen klantenkaartcode of e-mailadres dat de klant al heeft
external_idstringJaJe eigen sleutel voor deze klant, uniek per handelaar
curl -X POST https://puntjes.app/api/v1/customers/link-external-id \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"identifier":"K7M2QX4P","external_id":"POS-4471"}'

Koppelen is eenrichtingsverkeer

Dezelfde external_id nogmaals sturen slaagt gewoon, dus opnieuw proberen en herhaalde backfill-runs zijn veilig. Een andere sturen naar een klant die al gekoppeld is, geeft CUSTOMER_ALREADY_LINKED in plaats van te overschrijven. Stilzwijgend herkoppelen zou het systeem breken dat de bestaande ID beheert: de updates daarvan zouden zonder enig signaal beginnen te falen. Een verkeerde koppeling pas je aan in het adminportaal.

Geeft de volledige klant terug, in dezelfde vorm als GET /customers/{customer}.

Response

{
    "data": {
        "id": 1,
        "first_name": "Jan",
        "last_name": "De Vries",
        "email": "jan@example.com",
        "phone": "+31612345678",
        "external_id": "POS-4471",
        "date_of_birth": "1990-05-15",
        "customer_since": "2019-04-02",
        "locale": "nl",
        "status": {
            "value": "string",
            "label": "string"
        },
        "identifiers": [
            {
                "id": 1,
                "type": "loyalty_card",
                "value": "K7M2QX4P",
                "is_active": true,
                "is_primary": true,
                "created_at": "2024-03-15T12:30:00Z"
            }
        ],
        "marketingConsent": false,
        "marketingConsentGrantedAt": "string",
        "marketingConsentGrantedSource": "string",
        "marketingConsentWithdrawnAt": "string",
        "marketingConsentWithdrawnSource": "string",
        "loyalty_card_code": "K7M2QX4P",
        "deactivated_at": null,
        "anonymized_at": null,
        "created_at": "2024-03-15T12:30:00Z",
        "updated_at": "string"
    }
}

Fouten

CodeStatusBeschrijving
CUSTOMER_ALREADY_LINKED409De klant is al gekoppeld aan een andere external_id
CUSTOMER_NOT_FOUND404Geen enkele klant van jou draagt die identifier
EXTERNAL_ID_DUPLICATE409Een andere klant van jou gebruikt deze external_id al

Klantgegevens ophalen

GET /api/v1/customers/{customer}

Haal één klant op via zijn numerieke ID.

Padparameters

ParameterTypeBeschrijving
customerstringHet klant-ID

Response

{
    "data": {
        "id": 1,
        "first_name": "Jan",
        "last_name": "De Vries",
        "email": "jan@example.com",
        "phone": "+31612345678",
        "external_id": "POS-4471",
        "date_of_birth": "1990-05-15",
        "customer_since": "2019-04-02",
        "locale": "nl",
        "status": {
            "value": "string",
            "label": "string"
        },
        "identifiers": [
            {
                "id": 1,
                "type": "loyalty_card",
                "value": "K7M2QX4P",
                "is_active": true,
                "is_primary": true,
                "created_at": "2024-03-15T12:30:00Z"
            }
        ],
        "marketingConsent": false,
        "marketingConsentGrantedAt": "string",
        "marketingConsentGrantedSource": "string",
        "marketingConsentWithdrawnAt": "string",
        "marketingConsentWithdrawnSource": "string",
        "loyalty_card_code": "K7M2QX4P",
        "deactivated_at": null,
        "anonymized_at": null,
        "created_at": "2024-03-15T12:30:00Z",
        "updated_at": "string"
    }
}

Fouten

CodeStatusBeschrijving
CUSTOMER_NOT_FOUND404Klant niet gevonden of hoort bij een andere handelaar
Vorige
Overzicht