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

ParameterTypeVerplichtStandaardBeschrijving
statusstringNee—Filter op status: active, inactive of archived
categorystringNee—Filter op exacte categorie
searchstringNee—Zoekt in de productnaam, niet hoofdlettergevoelig
per_pageintegerNee—Paginagrootte, 1–100 (standaard: 15)
pageintegerNee1Welke 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

VeldTypeVerplichtBeschrijving
external_idstringJaJe eigen productidentifier (SKU/PLU), uniek per handelaar. Een id met / kun je niet in een /products/{externalId}-pad gebruiken
namestringJaProductnaam
descriptionstringNeeProductbeschrijving
price_centsintegerNeePrijs in eurocenten (bv. 350 = EUR 3,50)
image_urlstringNeeAbsolute URL naar de productafbeelding, alleen http of https
categorystringNeeVrij in te vullen categorielabel
stockintegerNeeBeschikbare voorraad (laat weg als je die niet bijhoudt)
statusstringNeeactive, inactive of archived (standaard: active)
metadataobjectNeeEigen 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

CodeStatusBeschrijving
PRODUCT_EXTERNAL_ID_DUPLICATE409Je 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

ParameterTypeBeschrijving
externalIdstringJe 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

ParameterTypeBeschrijving
externalIdstringJe 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

CodeStatusBeschrijving
PRODUCT_NOT_FOUND404Je 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

CodeStatusBeschrijving
PRODUCT_NOT_FOUND404Je 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

CodeStatusBeschrijving
PRODUCT_NOT_FOUND404Je 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

VeldTypeVerplichtBeschrijving
productsarrayJa1–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

CodeStatusBeschrijving
BATCH_TOO_LARGE422De 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

ParameterTypeBeschrijving
externalIdstringJe eigen identifier voor het product (SKU/PLU). Een id met / kun je niet in een /products/{externalId}-pad gebruiken

Request body

VeldTypeVerplichtBeschrijving
point_costintegerJaPunten die het inwisselen kost (minimaal 1)
namestringNeeOverschrijf de beloningsnaam (standaard: productnaam)
descriptionstringNeeOverschrijf de beloningsbeschrijving (standaard: productbeschrijving)
total_stockintegerNeeBeloningsvoorraad (laat weg voor onbeperkt)
statusstringNeeactive of inactive (standaard: active)
available_fromstringNeeStartdatum (YYYY-MM-DD)
available_untilstringNeeEinddatum (YYYY-MM-DD), op of na available_from
code_valid_for_hoursintegerNeeGeldigheid van de inwisselcode in uren, van 1 tot 87600 (tien jaar). Een grotere waarde geeft 422 VALIDATION_ERROR
payment_amountintegerNeeBedrag dat de kassa bovenop de punten aanrekent, in eurocenten (0 = alleen punten)
idempotency_keystringNeeOptioneel. 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_customerintegerNeeHoe 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

CodeStatusBeschrijving
IDEMPOTENCY_KEY_CONFLICT422Deze idempotency_key is al gebruikt voor een ander product of een ander bedrag, of de beloning ervan is verwijderd
PRODUCT_NOT_FOUND404Je hebt geen product met deze external_id