API-referentie
Product-endpoints
Endpoints om je productcatalogus te synchroniseren en producten om te zetten in inwisselbare beloningen.
Puntjes herkent een product aan je eigen external id: de SKU/PLU uit je kassa of webshop, die bij jou uniek is. Prijzen staan in eurocenten. Push je catalogus met deze endpoints en bied daarna elk product aan als "gratis product"-beloning.
Een external_id met / kun je niet in een pad gebruiken
Puntjes bewaart een external_id met een / erin, maar geen enkele route /products/{externalId} bereikt dat
product. Het pad geeft 404 ROUTE_NOT_FOUND, of 405 METHOD_NOT_ALLOWED als het toevallig op een andere route
past, zoals PUT /products/A%2Freward. De / als %2F sturen helpt niet. Lees zo'n product met GET /products en
wijzig het met POST /products/batch.
Idempotente synchronisatie
Synchroniseer je catalogus bij voorkeur met PUT /products/{external_id} (één product) of POST /products/batch
(meerdere): allebei maken ze het product aan als het nog niet bestaat, en werken ze het bij als het er al staat. Je
mag dezelfde catalogus dus zo vaak pushen als je wil, en je komt altijd op dezelfde rijen uit.
Producten weergeven
GET /api/v1/products
Geeft je producten terug, gepagineerd en op naam gesorteerd zoals in een telefoonboek: hoofdletters
en accenten verschuiven een naam niet, dus de Smet staat naast Dhondt. Producten met dezelfde
naam houden op elke pagina dezelfde volgorde.
Query parameters
| Parameter | Type | Verplicht | Standaard | Beschrijving |
|---|---|---|---|---|
status | string | Nee | — | Filter op status: active, inactive of archived |
category | string | Nee | — | Filter op exacte categorie |
search | string | Nee | — | Zoekt in de productnaam, niet hoofdlettergevoelig |
per_page | integer | Nee | — | Paginagrootte, 1–100 (standaard: 15) |
page | integer | Nee | 1 | Welke pagina je terugkrijgt. Een waarde onder 1, of een waarde die geen geheel getal is, geeft pagina 1 |
Response
{
"data": {
"data": [
{
"id": 1,
"external_id": "SKU-1001",
"name": "Cappuccino",
"description": "A warm cup",
"price_cents": 350,
"image_url": "https://cdn.example.com/cappuccino.png",
"category": "drinks",
"stock": 20,
"status": "active",
"metadata": {
"barcode": "54000012"
},
"created_at": "2026-07-18T10:00:00+00:00",
"updated_at": "2026-07-18T10:00:00+00:00"
}
],
"links": {
"first": "https://puntjes.app/api/v1/products?page=1",
"last": "https://puntjes.app/api/v1/products?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"last_page": 1,
"per_page": 15,
"total": 1
}
}
}
Een product aanmaken
POST /api/v1/products
Maak één product aan. De external_id moet uniek zijn binnen je eigen zaak; gebruik PUT voor idempotent aanmaken-of-bijwerken.
Request body
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
external_id | string | Ja | Je eigen productidentifier (SKU/PLU), uniek per handelaar. Een id met / kun je niet in een /products/{externalId}-pad gebruiken |
name | string | Ja | Productnaam |
description | string | Nee | Productbeschrijving |
price_cents | integer | Nee | Prijs in eurocenten (bv. 350 = EUR 3,50) |
image_url | string | Nee | Absolute URL naar de productafbeelding, alleen http of https |
category | string | Nee | Vrij in te vullen categorielabel |
stock | integer | Nee | Beschikbare voorraad (laat weg als je die niet bijhoudt) |
status | string | Nee | active, inactive of archived (standaard: active) |
metadata | object | Nee | Eigen sleutel/waarde-attributen die je ongewijzigd terugkrijgt, maximaal 16384 bytes UTF-8 in hun compacte JSON-vorm |
Example request
{
"external_id": "SKU-1001",
"name": "Cappuccino",
"description": "A warm cup",
"price_cents": 350,
"image_url": "https://cdn.example.com/cappuccino.png",
"category": "drinks",
"stock": 20,
"status": "active",
"metadata": {
"barcode": "54000012"
}
}
curl -X POST https://puntjes.app/api/v1/products \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"external_id":"SKU-1001","name":"Cappuccino","description":"A warm cup","price_cents":350,"image_url":"https://cdn.example.com/cappuccino.png","category":"drinks","stock":20,"status":"active","metadata":{"barcode":"54000012"}}'
Response (201 Created)
{
"data": {
"id": 1,
"external_id": "SKU-1001",
"name": "Cappuccino",
"description": "A warm cup",
"price_cents": 350,
"image_url": "https://cdn.example.com/cappuccino.png",
"category": "drinks",
"stock": 20,
"status": "active",
"metadata": {
"barcode": "54000012"
},
"created_at": "2026-07-18T10:00:00+00:00",
"updated_at": "2026-07-18T10:00:00+00:00"
}
}
Fouten
| Code | Status | Beschrijving |
|---|---|---|
PRODUCT_EXTERNAL_ID_DUPLICATE | 409 | Je hebt al een product met deze external_id |
Een product aanmaken of bijwerken (upsert)
PUT /api/v1/products/{externalId}
Idempotent: bestaat het product nog niet, dan maakt deze call het aan, en anders vervangt hij het. De external_id komt uit de URL en zit dus niet in de body. Laat je een optioneel veld weg, dan wist Puntjes het, dus stuur telkens het volledige product mee. Dit is de bouwsteen waarmee je je catalogus synchroniseert.
Path parameters
| Parameter | Type | Beschrijving |
|---|---|---|
externalId | string | Je eigen identifier voor het product (SKU/PLU). Een id met / kun je niet in een /products/{externalId}-pad gebruiken |
Request body
Dezelfde velden als bij Een product aanmaken, zonder external_id. name is verplicht.
Example request
{
"name": "Cappuccino",
"description": "A warm cup",
"price_cents": 350,
"image_url": "https://cdn.example.com/cappuccino.png",
"category": "drinks",
"stock": 20,
"status": "active",
"metadata": {
"barcode": "54000012"
}
}
curl -X PUT https://puntjes.app/api/v1/products/SKU-1001 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Cappuccino","description":"A warm cup","price_cents":350,"image_url":"https://cdn.example.com/cappuccino.png","category":"drinks","stock":20,"status":"active","metadata":{"barcode":"54000012"}}'
Response
Je krijgt 201 Created als er een nieuw product is aangemaakt, en 200 OK als een bestaand product is bijgewerkt. De body is het productobject, met dezelfde vorm als bij Aanmaken.
Een product bijwerken
PATCH /api/v1/products/{externalId}
Werk een deel van een product bij. Alleen de velden die in de body staan, veranderen.
Path parameters
| Parameter | Type | Beschrijving |
|---|---|---|
externalId | string | Je eigen identifier voor het product (SKU/PLU). Een id met / kun je niet in een /products/{externalId}-pad gebruiken |
Request body
De velden bij Een product aanmaken die je wil wijzigen, zonder external_id (die ligt vast).
Example request
{
"name": "Cappuccino",
"description": "A warm cup",
"price_cents": 350,
"image_url": "https://cdn.example.com/cappuccino.png",
"category": "drinks",
"stock": 20,
"status": "active",
"metadata": {
"barcode": "54000012"
}
}
curl -X PATCH https://puntjes.app/api/v1/products/SKU-1001 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Cappuccino","description":"A warm cup","price_cents":350,"image_url":"https://cdn.example.com/cappuccino.png","category":"drinks","stock":20,"status":"active","metadata":{"barcode":"54000012"}}'
Fouten
| Code | Status | Beschrijving |
|---|---|---|
PRODUCT_NOT_FOUND | 404 | Je hebt geen product met deze external_id |
Een product ophalen
GET /api/v1/products/{externalId}
Haal één product op met zijn external id.
Response
{
"data": {
"id": 1,
"external_id": "SKU-1001",
"name": "Cappuccino",
"description": "A warm cup",
"price_cents": 350,
"image_url": "https://cdn.example.com/cappuccino.png",
"category": "drinks",
"stock": 20,
"status": "active",
"metadata": {
"barcode": "54000012"
},
"created_at": "2026-07-18T10:00:00+00:00",
"updated_at": "2026-07-18T10:00:00+00:00"
}
}
Fouten
| Code | Status | Beschrijving |
|---|---|---|
PRODUCT_NOT_FOUND | 404 | Je hebt geen product met deze external_id |
Een product verwijderen
DELETE /api/v1/products/{externalId}
Zet een product op verwijderd (soft delete). Het staat niet meer in de catalogus, maar beloningen die je er al van maakte, blijven werken. Push je dezelfde external_id opnieuw met PUT, dan staat het product er weer.
Response
204 No Content bij succes.
Fouten
| Code | Status | Beschrijving |
|---|---|---|
PRODUCT_NOT_FOUND | 404 | Je hebt geen product met deze external_id |
Producten in bulk upserten
POST /api/v1/products/batch
Upsert tot 100 producten in één call: hiermee synchroniseer je een hele catalogus in één keer. Elk item wordt apart verwerkt. Gaat er één mis, dan krijgt dat item status error in zijn eigen resultaat en loopt de rest van de batch gewoon door. De call geeft altijd 200 OK terug, met een array met één resultaat per item en een samenvatting.
Request body
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
products | array | Ja | 1–100 productobjecten, elk met een external_id en de velden bij Een product aanmaken |
Example request
{
"products": [
{
"external_id": "SKU-1001",
"name": "Cappuccino",
"description": "A warm cup",
"price_cents": 350,
"image_url": "https://cdn.example.com/cappuccino.png",
"category": "drinks",
"stock": 20,
"status": "active",
"metadata": {
"barcode": "54000012"
}
}
]
}
curl -X POST https://puntjes.app/api/v1/products/batch \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"products":[{"external_id":"SKU-1001","name":"Cappuccino","description":"A warm cup","price_cents":350,"image_url":"https://cdn.example.com/cappuccino.png","category":"drinks","stock":20,"status":"active","metadata":{"barcode":"54000012"}}]}'
Response
{
"data": {
"results": [
{
"index": 0,
"external_id": "SKU-1001",
"status": "created",
"product": {
"id": 1,
"external_id": "SKU-1001",
"name": "Cappuccino",
"description": "A warm cup",
"price_cents": 350,
"image_url": "https://cdn.example.com/cappuccino.png",
"category": "drinks",
"stock": 20,
"status": "active",
"metadata": {
"barcode": "54000012"
},
"created_at": "2026-07-18T10:00:00+00:00",
"updated_at": "2026-07-18T10:00:00+00:00"
},
"errors": {}
}
],
"summary": {
"total": 2,
"created": 1,
"updated": 1,
"failed": 0
}
}
}
De status van elk resultaat is created, updated of error. Het voorbeeld hierboven toont elke sleutel die een resultaat kan dragen; errors staat er alleen bij een error-item en beschrijft welke velden misliepen. Staat dezelfde external_id meer dan één keer in één batch, dan wordt alleen het eerste item ermee verwerkt. Elk later item krijgt status error, met in errors.external_id de index van dat eerste item.
Fouten
| Code | Status | Beschrijving |
|---|---|---|
BATCH_TOO_LARGE | 422 | De batch bevat meer dan 100 producten |
Een beloning maken van een product
POST /api/v1/products/{externalId}/reward
Maak van een product een gratis product-beloning die klanten kunnen inwisselen. De beloning neemt de naam en de beschrijving van het product over, allebei te overschrijven, en blijft aan het product gekoppeld. Het inwisselen zelf loopt daarna via de gewone beloning- en inwisseling-endpoints.
Path parameters
| Parameter | Type | Beschrijving |
|---|---|---|
externalId | string | Je eigen identifier voor het product (SKU/PLU). Een id met / kun je niet in een /products/{externalId}-pad gebruiken |
Request body
| Veld | Type | Verplicht | Beschrijving |
|---|---|---|---|
point_cost | integer | Ja | Punten die het inwisselen kost (minimaal 1) |
name | string | Nee | Overschrijf de beloningsnaam (standaard: productnaam) |
description | string | Nee | Overschrijf de beloningsbeschrijving (standaard: productbeschrijving) |
total_stock | integer | Nee | Beloningsvoorraad (laat weg voor onbeperkt) |
status | string | Nee | active of inactive (standaard: active) |
available_from | string | Nee | Startdatum (YYYY-MM-DD) |
available_until | string | Nee | Einddatum (YYYY-MM-DD), op of na available_from |
code_valid_for_hours | integer | Nee | Geldigheid van de inwisselcode in uren, van 1 tot 87600 (tien jaar). Een grotere waarde geeft 422 VALIDATION_ERROR |
payment_amount | integer | Nee | Bedrag dat de kassa bovenop de punten aanrekent, in eurocenten (0 = alleen punten) |
idempotency_key | string | Nee | Optioneel. Je eigen sleutel voor deze beloning, maximaal 255 tekens en uniek binnen je zaak. Een nieuwe poging is dezelfde sleutel met hetzelfde product, dezelfde point_cost en hetzelfde payment_amount. Naam, tekst, voorraad en datums mogen verschillen: je krijgt de eerste beloning terug zoals ze nu is |
max_redemptions_per_customer | integer | Nee | Hoe vaak één klant deze beloning kan inwisselen, minstens 1 (laat weg voor geen grens) |
Example request
{
"point_cost": 150,
"name": "Cappuccino",
"description": "A warm cup",
"total_stock": 50,
"status": "active",
"available_from": null,
"available_until": null,
"code_valid_for_hours": null,
"payment_amount": 200,
"idempotency_key": "reward-sku-1001",
"max_redemptions_per_customer": 2
}
curl -X POST https://puntjes.app/api/v1/products/SKU-1001/reward \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"point_cost":150,"name":"Cappuccino","description":"A warm cup","total_stock":50,"status":"active","available_from":null,"available_until":null,"code_valid_for_hours":null,"payment_amount":200,"idempotency_key":"reward-sku-1001","max_redemptions_per_customer":2}'
Response (201 Created)
{
"data": {
"id": 1,
"product_id": 1,
"name": "Cappuccino",
"description": "A warm cup",
"type": "free_product",
"point_cost": 150,
"image_url": "https://cdn.example.com/cappuccino.png",
"total_stock": 50,
"remaining_stock": 50,
"status": {
"value": "active",
"label": "Active"
},
"available_from": null,
"available_until": null,
"discount_value": null,
"discount_type": null,
"product_reference": "SKU-1001",
"payment_amount": 200,
"code_valid_for_hours": null,
"created_at": "2026-07-18T10:00:00+00:00",
"updated_at": "2026-07-18T10:00:00+00:00",
"is_unlimited": false,
"max_redemptions_per_customer": 2
}
}
is_unlimited is true als de beloning geen voorraadlimiet heeft; remaining_stock is dan 0.
status.label is altijd Engels
De beloning komt terug met status als { "value": "active", "label": "Active" }. Deze API kent geen taal
per request en vertaalt niets aan de serverkant, dus staat er in label het Engelse woord, welke taal je
integratie ook spreekt. Vertaal value zelf, dat is active of inactive.
Stuur een idempotency_key mee, dan kun je veilig opnieuw proberen. Een tweede call met dezelfde
sleutel, hetzelfde product, dezelfde point_cost en hetzelfde payment_amount geeft 201 met de
beloning van de eerste call, en maakt niets bij. Naam, beschrijving, voorraad en datums mogen
verschillen. Dezelfde sleutel met een ander product of een ander bedrag geeft 422
IDEMPOTENCY_KEY_CONFLICT, net als een sleutel waarvan je de beloning in het adminportaal hebt
verwijderd. Probeer je opnieuw nadat het product verwijderd is, dan krijg je 404
PRODUCT_NOT_FOUND. Zonder sleutel, of met een lege, maakt elke call een nieuwe beloning.
Fouten
| Code | Status | Beschrijving |
|---|---|---|
IDEMPOTENCY_KEY_CONFLICT | 422 | Deze idempotency_key is al gebruikt voor een ander product of een ander bedrag, of de beloning ervan is verwijderd |
PRODUCT_NOT_FOUND | 404 | Je hebt geen product met deze external_id |