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.
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.
Queryparameters
| Parameter | Type | Verplicht | 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) |
Pagineren doe je met ?page=, de paginagrootte stel je in met ?per_page= (1–100,
standaard 15). De paginator leest page zelf uit de querystring, dus dat veld staat
niet in de parametertabel hierboven.
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 |
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 |
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 |
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.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
externalId | string | Je eigen identifier voor het product (SKU/PLU) |
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.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
externalId | string | Je eigen identifier voor het product (SKU/PLU) |
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.
Example request
curl -X POST https://puntjes.app/api/v1/products/batch \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response
{
"data": {
"results": [
{
"external_id": "SKU-1001",
"index": 0,
"product.external_id": "SKU-1001",
"product.id": 1,
"product.name": "Cappuccino",
"product.price_cents": 350,
"product.status": "active",
"status": "created"
}
],
"summary": {
"total": 2,
"created": 1,
"updated": 1,
"failed": 0
}
}
}
De status van elk resultaat is created, updated of error. Bij error krijg je er een errors-object bij dat beschrijft welke velden misliepen.
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.
Padparameters
| Parameter | Type | Beschrijving |
|---|---|---|
externalId | string | Je eigen identifier voor het product (SKU/PLU) |
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) |
code_valid_for_hours | integer | Nee | Geldigheid van de inwisselcode in uren |
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
}
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}'
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",
"code_valid_for_hours": null,
"created_at": "2026-07-18T10:00:00+00:00",
"updated_at": "2026-07-18T10:00:00+00:00"
}
}
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.
Fouten
| Code | Status | Beschrijving |
|---|---|---|
PRODUCT_NOT_FOUND | 404 | Je hebt geen product met deze external_id |