Introductie

SDK's & integraties

Officiële clientbibliotheken voor de Puntjes API. Ze halen je OAuth2-tokens op en vernieuwen ze, proberen een call veilig opnieuw met een idempotency key, geven je getypeerde fouten terug en lopen je pagina's af. In je integratie schrijf je dus je eigen logica en geen HTTP-afhandeling. De experimentele Delphi-units doen dat niet: dat zijn alleen gegenereerde calls.


Officiële pakketten

PakketVoorVereist
puntjes/php-sdkElke PHP-applicatie: WooCommerce/WordPress-plugins, kassakoppelingen, cronjobsPHP 8.1+
puntjes/laravelLaravel-applicatiesPHP 8.2+, Laravel 12–13
Puntjes.SdkElke .NET-applicatie: kassasoftware, webshops, achtergronddiensten.NET 8 of .NET 10
com.github.PuntjesApp:puntjes-javaElke Java-applicatie: kassasoftware, webshopbackends, achtergronddienstenJava 17+
@puntjes/sdkNode.js-servers: webshopbackends, serverless functies, kassakoppelingenNode 20+
puntjes-delphiDelphi- en Free Pascal-programma's, experimenteelmORMot 2.4

puntjes/laravel is een dunne laag bovenop de core SDK: configuratie, een facade en een token store in je cache, zodat je applicatie één token grant per uur doet in plaats van één per request.

De PHP-pakketten staan op Packagist en volgen SemVer. Beide staan op 2.0: pin ze met ^2.0. Na een minor- of patchrelease van puntjes/php-sdk volgt vanzelf een release van puntjes/laravel met dezelfde calls. Bij een nieuwe major van de core wacht die release tot wij het Laravel-pakket ertegen hebben nagekeken.

Een klant registreren

De identifiers-array accepteert email en loyalty_card, en je mag ze volledig weglaten zodat Puntjes zelf de kaart uitgeeft. Dat is de kortste juiste oproep en wat de meeste integraties willen.

Stuur je wel een loyalty_card mee, dan moet die waarde een kaart zijn die de handelaar heeft laten drukken en nog aan niemand gaf. Zie Klant-endpoints.

Op puntjes/php-sdk onder 1.0.0 werkt geen van beide: de oproep zonder kaart wordt geweigerd voor er een request vertrekt, en de SDK kent geen loyalty_card als identifiertype. Werk bij in plaats van eromheen.


Gewoon PHP (elk framework)

composer require puntjes/php-sdk:^2.0

De SDK praat PSR-18 en gebruikt de HTTP-client die je project al heeft. Ze installeert er zelf geen. Heb je er geen:

composer require guzzlehttp/guzzle

Maak je credentials aan in het beheerportaal (zie API-clients beheren). Het secret zie je één keer.

use Puntjes\Generated\Customers\Lookup\LookupRequestBuilderGetRequestConfiguration as Lookup;
use Puntjes\Generated\Models\CreateTransactionData;
use Puntjes\Puntjes;

$puntjes = Puntjes::make(getenv('PUNTJES_CLIENT_ID'), getenv('PUNTJES_CLIENT_SECRET'));

// Een klant scant zijn klantenkaart aan de kassa.
$customer = $puntjes->customers()->lookup()->get(new Lookup(
    queryParameters: Lookup::createQueryParameters(identifier: $scannedCard),
))->wait()->getData();

echo $customer->getWalletBalance();

// Registreer de aankoop. Bedragen zijn in centen.
$body = new CreateTransactionData();
$body->setIdentifier($scannedCard);
$body->setTotalAmount(4200);
$body->setIdempotencyKey('order-'.$orderId);

$transaction = $puntjes->transactions()->post($body)->wait()->getData();

echo $transaction->getPointsEarned();

Puntjes::make() geeft een client terug die uit het OpenAPI-document van Puntjes is gegenereerd. Hij heeft dus elke call die de API heeft. Een call volgt de URL: ->customers()->byCustomer('42')->wallet()->get() is GET /customers/42/wallet.

Elke call geeft een promise terug. Sluit hem af met ->wait() en lees dan het data-deel van het antwoord met ->getData(). Een geslaagd antwoord zonder data-deel geeft null.

De SDK regelt de authenticatie: ze haalt bij de eerste call een client_credentials-token op, houdt het in cache tot het vervalt, en haalt één keer een nieuw token op als de API het oude afwijst.

Fouten

Elke exception erft van Puntjes\Exception\PuntjesException. Een foutantwoord van de API wordt een exception met een eigen naam, en errorCode() geeft de foutcode als enum:

use Puntjes\Exception\{ApiException, NotFoundException, ValidationException};
use Puntjes\Generated\Errors\ErrorCode;

try {
    $puntjes->redemptions()->post($body)->wait();
} catch (NotFoundException $e) {
    // 404: die klant of beloning bestaat niet
} catch (ValidationException $e) {
    $e->errorsFor('reward_id');
} catch (ApiException $e) {
    if ($e->errorCode() === ErrorCode::InsufficientBalance) {
        // ...
    }

    $e->requestId(); // geef dit door aan de support
}

Is een code nieuwer dan je SDK, dan is errorCode() null en geeft code() nog altijd de ruwe tekst. Een 422 voor een fout veld is een ValidationException. Een 422 voor een bedrijfsregel, zoals INSUFFICIENT_BALANCE, is een ApiException.

Apple wallet-pass

WalletPass::apple($puntjes, $customerId) geeft de Apple-pass (een .pkpass-bestand) terug als stream:

use Puntjes\WalletPass;

$pass = WalletPass::apple($puntjes, $customerId);

Overstappen van 1.x

2.0 heeft een nieuwe kern, en de 1.x-calls ($puntjes->customers->lookup(), Puntjes\Request\*) bestaan niet meer. De upgradegids zet elke 1.x-call naast zijn 2.0-vorm.

1.x krijgt beveiligingsfixes tot 6 maanden nadat 2.0.0 op Packagist staat. Ze komen op de branch 1.x. Wil je op 1.x blijven, laat dan "puntjes/php-sdk": "^1.7" in je composer.json staan.


Laravel

composer require puntjes/laravel
PUNTJES_CLIENT_ID=your-client-id
PUNTJES_CLIENT_SECRET=your-client-secret
PUNTJES_BASE_URL=https://puntjes.app/api/v1

Meer is er niet aan. Laravel vindt de service provider en de facade vanzelf, en de tokens staan in de cache store van je applicatie.

use Puntjes\Generated\Customers\Lookup\LookupRequestBuilderGetRequestConfiguration as Lookup;
use Puntjes\Generated\Models\CreateTransactionData;
use Puntjes\Laravel\Facades\Puntjes;

$customer = Puntjes::customers()->lookup()->get(new Lookup(
    queryParameters: Lookup::createQueryParameters(identifier: $card),
))->wait()->getData();

$body = new CreateTransactionData();
$body->setIdentifier($card);
$body->setTotalAmount($order->total_cents);
$body->setIdempotencyKey('order-'.$order->id);

Puntjes::transactions()->post($body)->wait();

De facade geeft elke call door aan de gegenereerde client van de core, dus elke call hierboven werkt ook via de facade. Je kunt Puntjes\Generated\PuntjesClient ook uit de container injecteren. Kom je van 1.x? De UPGRADING.md van het pakket zegt wat je moet aanpassen. Zie de README van het pakket voor configuratieopties, eigen HTTP-clients en testadvies.


WordPress / WooCommerce

Bouw rechtstreeks op puntjes/php-sdk: het heeft bewust geen harde dependency op Guzzle of op een framework, dus je kunt het veilig in een plugin meeleveren. Twee dingen tellen zodra je die plugin verdeelt:

  1. Scope je dependencies met PHP-Scoper of Strauss, zodat je gebundelde bibliotheken niet kunnen botsen met andere plugins in het gedeelde WordPress-proces.
  2. Bewaar tokens in een transient, zodat niet elke paginalading een token grant doet. De SDK-README bevat een kant-en-klare TransientTokenStore-implementatie en de volledige richtlijnen.

.NET

dotnet add package Puntjes.Sdk
using Puntjes.Sdk;
using Puntjes.Sdk.Generated.Models;

var puntjes = PuntjesClientFactory.Create(
    Environment.GetEnvironmentVariable("PUNTJES_CLIENT_ID")!,
    Environment.GetEnvironmentVariable("PUNTJES_CLIENT_SECRET")!);

// Een klant scant zijn klantenkaart aan de kassa.
var customer = (await puntjes.Customers.Lookup.GetAsync(c => c.QueryParameters.Identifier = scannedCard))?.Data;

// Registreer de aankoop. Bedragen zijn in centen.
var transaction = (await puntjes.Transactions.PostAsync(new CreateTransactionData
{
    Identifier = scannedCard,
    TotalAmount = 4200,
    IdempotencyKey = $"order-{orderId}",
}))?.Data;

Net als bij de PHP SDK is de client gegenereerd uit het OpenAPI-document en volgt een call de URL: puntjes.Customers["42"].Wallet.GetAsync() is GET /customers/42/wallet. Tokens, retries en fouten werken zoals bij de PHP SDK hieronder. Een foutantwoord wordt een subklasse van PuntjesException, zoals NotFoundException of ValidationException, met ErrorCode, RawCode en RequestId. Paginator.AllAsync() loopt de pagina's van een lijst af, en WalletPass.AppleAsync() geeft de Apple-pass terug als bytes. In de README vind je alle opties.

Puntjes.Sdk staat op nuget.org en volgt SemVer. Krijgt de API een nieuwe minor- of patchversie, dan volgt vanzelf een nieuwe pakketversie.


Java

Voeg de JitPack-repository en de dependency toe. De SDK vraagt Java 17 of nieuwer.

<repositories>
  <repository>
    <id>jitpack.io</id>
    <url>https://jitpack.io</url>
  </repository>
</repositories>

<dependency>
  <groupId>com.github.PuntjesApp</groupId>
  <artifactId>puntjes-java</artifactId>
  <version>1.0.0</version>
</dependency>

Met Gradle zet je maven { url 'https://jitpack.io' } bij repositories en implementation 'com.github.PuntjesApp:puntjes-java:1.0.0' bij dependencies.

import app.puntjes.sdk.IdempotencyKey;
import app.puntjes.sdk.PuntjesClientFactory;
import app.puntjes.sdk.generated.models.CreateTransactionData;

var puntjes = PuntjesClientFactory.create(
        System.getenv("PUNTJES_CLIENT_ID"), System.getenv("PUNTJES_CLIENT_SECRET"));

// Een klant scant zijn klantenkaart aan de kassa.
var customer = puntjes.customers().lookup().get(c -> c.queryParameters.identifier = scannedCard).getData();

// Registreer de aankoop. Bedragen zijn in centen.
var purchase = new CreateTransactionData();
purchase.setIdentifier(scannedCard);
purchase.setTotalAmount(4200);
purchase.setIdempotencyKey(IdempotencyKey.newKey());
var transaction = puntjes.transactions().post(purchase).getData();

De client is gegenereerd uit het OpenAPI-document, dus een call volgt de URL: puntjes.customers().byCustomer("42").wallet().get() is GET /customers/42/wallet. Tokens en retries werken zoals bij de PHP SDK hierboven. IdempotencyKey.newKey() maakt een key voor een POST.

Elke fout is een unchecked PuntjesException. Een foutantwoord wordt een subklasse, zoals NotFoundException, ValidationException of RateLimitException. errorCode() geeft de foutcode als Optional, en requestId() geeft het request-id. ValidationException heeft daarnaast details(), de meldingen per veld, en errorsFor("reward_id"), de meldingen van één veld. Een 422 voor een bedrijfsregel, zoals INSUFFICIENT_BALANCE, is een gewone PuntjesException.

Paginator.all() loopt de pagina's van een lijst af als een lazy Stream, en WalletPass.apple() geeft de Apple-pass terug als bytes. In de README vind je alle opties.

JitPack levert com.github.PuntjesApp:puntjes-java, en het volgt SemVer. Het staat niet op Maven Central.


TypeScript en Node.js

npm install @puntjes/sdk
import { createPuntjes } from '@puntjes/sdk';

const puntjes = createPuntjes({
    clientId: process.env.PUNTJES_CLIENT_ID!,
    clientSecret: process.env.PUNTJES_CLIENT_SECRET!,
});

// Een klant scant zijn klantenkaart aan de kassa.
const { data: customer } = await puntjes.lookupCustomer({ query: { identifier: scannedCard } });

// Registreer de aankoop. Bedragen zijn in centen.
const { data: transaction } = await puntjes.submitTransaction({
    body: { identifier: scannedCard, total_amount: 4200, idempotency_key: `order-${orderId}` },
});

De client is gegenereerd uit het OpenAPI-document, dus elk endpoint is een methode met getypeerde path, query en body. Een call geeft { data, request, response } terug, en het eigen data-veld van de API zit daarbinnen: het record is dus customer.data. Tokens, retries en fouten werken zoals bij de PHP SDK hierboven. Bij een foutantwoord gooit de client een subklasse van PuntjesError, zoals NotFoundError of ValidationError, met code, rawCode en requestId; RateLimitError heeft retryAfter, in milliseconden. paginate() loopt de pagina's van een lijst af, en walletPass.apple() geeft de Apple-pass terug als bytes. In de README vind je alle opties.

Het pakket is een ES-module voor Node 20 of nieuwer. Gebruik het alleen op een server: met het client secret heb je volledige toegang tot de API van je zaak, dus het hoort nooit in een browser. @puntjes/sdk staat op npm en volgt SemVer. Elke versie na 1.0.0 publiceert onze CI met provenance. Krijgt de API een nieuwe minor- of patchversie, dan volgt vanzelf een nieuwe pakketversie.


Delphi (experimenteel)

Download de twee units uit de laatste release, of installeer ze met Boss:

boss install github.com/PuntjesApp/puntjes-delphi
http := TJsonClient.Create('https://puntjes.app', 'api/v1');
puntjes := TPuntjesClient.Create(http);
http.SetBearer(accessToken);

writeln(puntjes.LookupCustomer(card).Data.WalletBalance);

mORMot 2 mopenapi genereert de units uit het OpenAPI-document. Je hebt mORMot 2 op tag 2.4-stable nodig in je zoekpad. De units zijn experimenteel: onze CI compileert ze met Free Pascal, maar met de eigen compiler van Delphi is dat nog niet gebeurd. Een veld dat leeg kan zijn of meer dan één vorm heeft, komt binnen als variant. Een update stuurt elk veld mee: een veld dat je niet invult, gaat als null naar de API en wordt dan leeggemaakt. Neem dus elk veld dat je wilt houden over in de payload. Een antwoordtype zoals TDtoPuntjes53 kan in elke release een ander nummer krijgen. Lees dus .Data in de expressie, en declareer dat type niet zelf. Anders dan de SDK's hierboven halen de units geen token op en vernieuwen ze het niet, proberen ze niets opnieuw en lopen ze geen pagina's af: dat doet je eigen code. In de README staat een volledig programma, met de tokenrequest.


Wat de SDK voor je afhandelt

  • Tokens: ophalen, cachen en opnieuw ophalen van client_credentials-tokens, met een token store die je per platform zelf invult.
  • Veilige retries: een GET, PUT, PATCH of DELETE gaat met exponential backoff opnieuw de deur uit na een verbroken verbinding, een 5xx of een 429 RATE_LIMITED. Een POST gaat alleen opnieuw als de body een idempotency_key heeft, want de API geeft bij een herhaalde key het eerste antwoord terug. De SDK maakt geen key voor je aan: zet er een op elke POST die punten verplaatst, met IdempotencyKey::new() of met een waarde uit je eigen gegevens, bv. 'order-'.$orderId. Een POST zonder key probeert ze nooit opnieuw.
  • Getypeerde fouten: elke foutcode komt terug als een exception met een eigen naam, met de machineleesbare code, de HTTP-status en de request_id erbij. Bij een rate limit staat retryAfter() klaar.
  • Paginering: Paginator::of() loopt de pagina's van een lijst af en vraagt de volgende pagina pas op wanneer je lus ze nodig heeft:
use Puntjes\Generated\Products\ProductsRequestBuilderGetRequestConfiguration as Products;
use Puntjes\Pagination\Paginator;

$all = Paginator::of(fn (int $page) => $puntjes->products()->get(new Products(
    queryParameters: Products::createQueryParameters(page: $page),
))->wait());

foreach ($all as $product) {
    echo $product->getExternalId();
}

Het OpenAPI-document

In PuntjesApp/puntjes-openapi staat het OpenAPI-document van elke versie van de API die Puntjes uitbracht. Een versie komt daar terecht op het moment dat Puntjes ze uitbrengt. Elke versie krijgt een tag, zoals v1.2.0, en een stukje in de CHANGELOG.md van de repository.

Wat er verandertDe versie
Een bestaande client werkt niet meer: een endpoint valt weg, een veld dat in elk antwoord zat valt weg, een input wordt verplichtMajor (2.0.0)
Elke andere wijziging aan de API: een nieuw endpoint, een veld, iets dat uitgefaseerd wordtMinor (1.3.0)
Alleen een beschrijving of een voorbeeld verandertPatch (1.2.1)

Genereer je zelf een client, lees api.json dan op een tag en niet op main. Zo verandert je client pas wanneer jij naar een nieuwere tag overstapt.


Andere talen

PHP, .NET, Java en TypeScript hebben vandaag een officiële SDK, en voor Delphi zijn er experimentele units. Vanuit elke andere taal werk je rechtstreeks tegen de REST API, of je genereert een client uit het OpenAPI-document. Elk endpoint is gewone JSON over OAuth2, en de referentiepagina's in deze documentatie beschrijven de vorm van elke request en elke response.

Volgende
Klanten