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
| Parameter | Type | Verplicht | Beschrijving |
|---|---|---|---|
identifier | string | Nee | De waarde waarmee je zoekt: een klantenkaartcode of een e-mailadres |
external_id | string | Nee | Je 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
| Code | Status | Beschrijving |
|---|---|---|
CUSTOMER_NOT_FOUND | 404 | Geen 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
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
first_name | string | Nee | Voornaam van de klant |
last_name | string | Nee | Achternaam van de klant |
email | string | Nee | E-mailadres (uniek per handelaar) |
phone | string | Nee | Telefoonnummer |
external_id | string | Nee | Je eigen identifier voor deze klant (uniek per handelaar) |
date_of_birth | string | Nee | Geboortedatum (YYYY-MM-DD) |
customer_since | string | Nee | De datum waarop deze persoon klant werd (YYYY-MM-DD). Laat je hem weg, dan telt Puntjes vanaf de registratiedatum. |
locale | string | Nee | Voorkeurstaal: nl of en (standaard: nl) |
marketing_consent | boolean | Nee | Opt-in voor campagnemail. Staat standaard uit. |
identifiers | array | Nee | Array 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:
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
type | string | Ja | email of loyalty_card. Een andere waarde wordt niet geaccepteerd |
value | string | Ja | De 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
| Code | Status | Beschrijving |
|---|---|---|
EXTERNAL_ID_DUPLICATE | 409 | De external_id is al geregistreerd voor deze handelaar |
IDENTIFIER_DUPLICATE | 409 | De identifier is al geregistreerd, ook als het om een kaart gaat die nu bij een andere klant hoort |
LOYALTY_CARD_NOT_FOUND | 422 | De 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
| Parameter | Type | Beschrijving |
|---|---|---|
externalId | string | De external_id die je deze klant in je eigen systeem hebt gegeven |
Elk veld is optioneel: stuur alleen wat er veranderd is.
Request body
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
first_name | string | Nee | Max. 255 tekens |
last_name | string | Nee | Max. 255 tekens |
email | string | Nee | Geldig e-mailadres, max. 255 tekens |
phone | string | Nee | Max. 50 tekens |
date_of_birth | string | Nee | YYYY-MM-DD |
customer_since | string | Nee | YYYY-MM-DD. Stuur null om hem te wissen en terug te vallen op de registratiedatum. |
locale | string | Nee | nl of en |
marketing_consent | boolean | Nee | true 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.
marketing_consent weglaten is iets anders dan false sturen
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
| Code | Status | Beschrijving |
|---|---|---|
EXTERNAL_ID_NOT_FOUND | 404 | Geen 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
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
identifier | string | Ja | Een klantenkaartcode of e-mailadres dat de klant al heeft |
external_id | string | Ja | Je 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
| Code | Status | Beschrijving |
|---|---|---|
CUSTOMER_ALREADY_LINKED | 409 | De klant is al gekoppeld aan een andere external_id |
CUSTOMER_NOT_FOUND | 404 | Geen enkele klant van jou draagt die identifier |
EXTERNAL_ID_DUPLICATE | 409 | Een 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
| Parameter | Type | Beschrijving |
|---|---|---|
customer | string | Het 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
| Code | Status | Beschrijving |
|---|---|---|
CUSTOMER_NOT_FOUND | 404 | Klant niet gevonden of hoort bij een andere handelaar |