Core concepts
Customers
A customer is an end user enrolled in a vendor's loyalty program. Customers earn points through transactions and spend them on rewards.
Customer data
| Field | Type | Description |
|---|---|---|
id | integer | Unique customer ID |
first_name | string | Customer's first name |
last_name | string | Customer's last name |
email | string | Email address (unique per vendor) |
phone | string | Phone number |
date_of_birth | date | Date of birth |
customer_since | date | Optional start date for this customer relationship. Left empty, Puntjes counts from the day the record was created. |
locale | string | Preferred language (nl or en, default: nl) |
status | string | Current status: active, deactivated, or anonymized |
Identifiers
Each customer has one or more identifiers: lookup values used to find the customer during transactions at the POS. There are two types:
| Type | Description |
|---|---|
loyalty_card | An 8-character code of uppercase letters and digits (A-Z, 0-9). Puntjes issues one automatically, or the vendor pre-generates a batch and has them printed on physical cards. |
email | The customer's email address. |
Identifier values are unique within a vendor, and lookup is case-insensitive.
Your own IDs
An identifier is how a customer is recognised. Your external_id is separate: it is your own
key for that customer (the record number in your POS or webshop) stored on the customer and
unique within your vendor. Puntjes never invents one, and it is not used for lookup by scanning.
Set it at registration, or, for customers who registered before you integrated, attach it later
with POST /customers/link-external-id. Once linked you can read and
update that customer by your own ID instead of storing ours.
Loyalty cards
Every customer gets a loyalty card. You do not have to invent a card number: if none is
supplied at registration, Puntjes issues one and returns it as loyalty_card_code.
Vendors who want physical cards generate a batch in the admin portal first, export the codes as CSV for a print shop, and hand the printed cards out at the counter. Scanning such a card during registration assigns that specific card to the customer. See Managing customers for the workflow.
A customer can end up holding more than one card (up to five), most often after losing one
and being handed a replacement. Exactly one of them is the card they scan: assigning a card
in the admin portal makes it that card, so in practice it is the most recently assigned one.
It is what loyalty_card_code reports and what their wallet pass shows. Their older cards
stay active and keep working when scanned, so nothing you handed out before stops being
usable.
The email identifier is managed for you
The email identifier is a projection of the customer's profile email address. Puntjes creates it, keeps it in step with every later change to the address, and removes it when the address is cleared. It cannot be edited or deactivated on its own. Change the customer's email instead, and the identifier follows.
Identifier status
Loyalty card identifiers can be independently activated or deactivated. Deactivated identifiers cannot be used for customer lookup or transaction submission.
Older identifier values still work
Puntjes previously accepted several additional identifier types for different scanning technologies. Those types
were consolidated into loyalty_card. Existing values were migrated and still resolve on lookup, so cards already
in customers' wallets keep working, but they can no longer be created, and an API client sending a retired type
receives a validation error. See Error handling.
Customer status lifecycle
Active → Deactivated
Active → Anonymized
| Status | Description |
|---|---|
| Active | Customer can earn and spend points normally |
| Deactivated | Customer is disabled; no new transactions or redemptions |
| Anonymized | Personal data removed for GDPR compliance; excluded from all queries by default |
Anonymization is irreversible
Anonymizing a customer permanently removes their personal data. Anonymized customers are excluded from all API responses and queries. This operation cannot be undone.
Registration
Register a new customer via the API by providing their personal details. No identifier is required. Puntjes issues a loyalty card and returns its code:
POST /api/v1/customers
{
"first_name": "Jan",
"last_name": "De Vries",
"email": "jan@example.com",
"phone": "+31612345678",
"date_of_birth": "1990-05-15",
"locale": "nl"
}
To hand out a pre-printed card instead, scan it and send its code:
{
"first_name": "Jan",
"last_name": "De Vries",
"identifiers": [{ "type": "loyalty_card", "value": "K7M2QX4P" }]
}
A wallet is automatically created for each new customer. If the vendor has configured a signup bonus (Settings → Signup Bonus in the admin portal), that number of points is credited to the wallet immediately at registration. Recorded on the ledger as a regular earn, so point expiration applies to bonus points like any other.
Marketing consent
Campaign email (a birthday message, an anniversary reward) goes only to customers who
explicitly asked for it. One flag decides that, marketing_consent, and it is off for every
customer until somebody sets it to true. Nothing is grandfathered in: customers who existed
before the flag did are unsubscribed, and so is every customer created by an import.
Puntjes stores the decision as provenance rather than as a bare boolean. An opt-in records the moment and the surface it came through, and a later withdrawal records its own moment and surface without erasing the first pair. Consent you cannot date and attribute is consent you cannot demonstrate, which is the situation a data-protection request puts you in.
| Source | Meaning |
|---|---|
portal | A vendor user ticked the box in the admin portal |
api | Your integration sent marketing_consent |
email | The customer used the unsubscribe link in an email |
Three surfaces can change it: your integration, through marketing_consent on
POST /customers or the update endpoint; a vendor user, on the
customer's page in the admin portal; and the customer themselves, through the unsubscribe link
carried by campaign email. That link is signed and never expires, so a year-old message still
works, and following it needs no account and no password.
Withdrawal takes effect immediately and asks for no confirmation. marketingConsent reads
false on the next request, and the grant timestamp stays where it was so you can still see
when the customer had opted in.
Omitting the flag on an update is not the same as sending false
On PUT|PATCH /customers/by-external-id/{external_id}, leaving marketing_consent out of the body leaves the
stored decision untouched. Only an explicit false withdraws it. A nightly POS sync that pushes names and phone
numbers therefore cannot unsubscribe the people it touches. See Customer endpoints for the
full rule, including what an explicit null means.
Lookup
Find a customer by any of their active identifiers:
GET /api/v1/customers/lookup?identifier=K7M2QX4P
The response includes the customer's profile data along with their current wallet balance. This is the primary way POS systems identify customers at checkout.
See Customer endpoints for the complete API reference.