API reference
API Overview
The Puntjes API is a RESTful JSON API. All endpoints are scoped to the authenticated vendor and require OAuth2 bearer tokens.
Base URL
https://puntjes.app/api/v1
All endpoints documented in this reference are relative to this base URL.
Request format
- Use
Content-Type: application/jsonfor request bodies - Include
Authorization: Bearer {token}on every request (see Authentication) - All monetary amounts are in cents (e.g.,
2500= EUR 25.00) - All point values are integers
Response envelope
All successful responses are wrapped in a data envelope:
{
"data": {
"id": 1,
"first_name": "Jan",
"last_name": "De Vries"
}
}
Paginated endpoints put the whole page inside that envelope, so the items sit two levels
down, at data.data, next to data.links and data.meta. GET /rewards is the one list
that is not paginated: there data is the bare array.
{
"data": {
"data": [...],
"links": {
"first": "...",
"last": "...",
"prev": null,
"next": "..."
},
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 15,
"total": 72
}
}
}
Follow data.links.next as given. It carries the query parameters you sent (filters,
sort order and per_page), so page 2 of a filtered result set is
the same filter as page 1. Rebuilding the URL yourself from
data.meta.current_page drops them: a walk that starts at per_page=5 silently
returns 15 rows on the second request.
Error responses
Errors follow a consistent format:
{
"error": {
"code": "CUSTOMER_NOT_FOUND",
"message": "No customer found with the given identifier.",
"status": 404,
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
Validation errors (422) include a details field:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The given data was invalid.",
"status": 422,
"request_id": "...",
"details": {
"email": ["The email field must be a valid email address."],
"identifiers.0.type": ["The identifier type must be one of: email, loyalty_card."]
}
}
}
See Error handling for a full list of error codes.
Idempotency
Every endpoint that moves points requires an idempotency_key (max 255
characters). Repeating a request with a key you already used returns the
original resource instead of processing it again, so these calls are safe to
retry after a network failure or timeout:
| Endpoint | Key is unique per | A replay returns |
|---|---|---|
POST /transactions | vendor | The original transaction; no points re-awarded |
POST /redemptions | vendor | The original redemption; no points deducted again, stock untouched |
POST /customers/{customer}/wallet/adjust | customer's wallet | The original ledger entry; balance not moved again |
Use a value that identifies the operation in your own system (an order ID, or a UUID generated once per checkout and reused across retries). Random values that change on every attempt defeat the purpose.
This protects against network issues and duplicate submissions from POS systems.
Endpoint summary
| Method | Endpoint | Description |
|---|---|---|
GET | /me | Get vendor branding |
GET | /customers/lookup | Look up a customer |
POST | /customers | Register a customer |
PUT PATCH | /customers/by-external-id/{externalId} | Update a customer |
POST | /customers/link-external-id | Link an external ID |
POST | /customers/by-external-id/{externalId}/send-card | Send the loyalty card, keyed on your own id |
POST | /customers/{customer}/send-card | Send the customer their loyalty card |
GET | /customers/{customer} | Get customer details |
POST | /transactions | Submit a transaction |
GET | /customers/{customer}/wallet | Get wallet balance |
GET | /customers/{customer}/ledger | List ledger entries |
GET | /customers/{customer}/redemptions | List a customer's redemptions |
GET | /customers/{customer}/vouchers | List a customer's open vouchers |
GET | /customers/{customer}/wallet-pass | Download a wallet pass |
GET | /customers/{customer}/transactions | List customer transactions |
POST | /customers/{customer}/wallet/adjust | Adjust wallet balance |
GET | /campaigns | List campaigns |
GET | /branches | List active branches |
GET | /rewards | List rewards |
POST | /redemptions | Redeem a reward |
GET | /redemptions/{code} | Look up a redemption |
POST | /redemptions/{code}/verify | Verify a redemption |
GET | /vouchers/{code} | Look up a voucher |
POST | /vouchers/{code}/verify | Verify a voucher |
GET | /products | List products |
POST | /products | Create a product |
POST | /products/batch | Bulk upsert products |
POST | /products/{externalId}/reward | Create a reward from a product |
GET | /products/{externalId} | Get a product |
PUT | /products/{externalId} | Create or update a product (upsert) |
PATCH | /products/{externalId} | Update a product |
DELETE | /products/{externalId} | Delete a product |
Get vendor branding
GET /api/v1/me
Return the branding of the vendor your token belongs to. Use it to colour a POS screen or a webshop widget the way the vendor's own customers already recognise, without hardcoding anything per client.
default_branch is the branch your API credential defaults to, as { "external_id", "name", "type" }.
It is null when the credential has none. A branch that is deactivated is still named.
A write that falls back to it can still refuse it: a new transaction at a deactivated branch
answers BRANCH_INACTIVE.
Read it to learn your own branch key instead of keeping a copy in your configuration.
Example request
curl https://puntjes.app/api/v1/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response
{
"data": {
"display_name": "Koffiebar De Hoek",
"logo_url": "https://cdn.example.com/logo.png",
"brand_color_primary": "#4F46E5",
"brand_color_accent": "#06B6D4",
"default_branch": {
"external_id": "webshop",
"name": "Webshop",
"type": "online"
}
}
}
List active branches
GET /api/v1/branches
Return the active branches of your vendor, in the order you keep them. A deactivated branch is not
listed. The list is not paginated: data is the bare array. Check the branch key you have
configured against the external_id values here, or show the names to the cashier.
Example request
curl https://puntjes.app/api/v1/branches \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response
{
"data": [
{
"external_id": "webshop",
"name": "Webshop",
"type": "online"
}
]
}