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

FieldTypeDescription
idintegerUnique customer ID
first_namestringCustomer's first name
last_namestringCustomer's last name
emailstringEmail address (unique per vendor)
phonestringPhone number
date_of_birthdateDate of birth
customer_sincedateOptional start date for this customer relationship. Left empty, Puntjes counts from the day the record was created.
localestringPreferred language (nl or en, default: nl)
statusstringCurrent 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:

TypeDescription
loyalty_cardAn 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.
emailThe 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
StatusDescription
ActiveCustomer can earn and spend points normally
DeactivatedCustomer is disabled; no new transactions or redemptions
AnonymizedPersonal 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.


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.

SourceMeaning
portalA vendor user ticked the box in the admin portal
apiYour integration sent marketing_consent
emailThe 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.