Handleidingen

Foutafhandeling

De Puntjes API gebruikt de standaard HTTP-statuscodes, en geeft bij elke fout een response met dezelfde opbouw terug.


Opbouw van een foutresponse

Elke fout komt in deze vorm terug:

{
    "error": {
        "code": "ERROR_CODE",
        "message": "A human-readable description of what went wrong.",
        "status": 422,
        "request_id": "550e8400-e29b-41d4-a716-446655440000",
        "details": {}
    }
}
VeldBeschrijving
codeMachine-leesbare foutcode. Hierop laat je je integratie beslissen
messageFoutmelding voor mensen. Geschikt voor je logs, niet voor je eindgebruikers
statusHTTP-statuscode, dezelfde als die van de response zelf
request_idUniek id van dit request, voor debuggen en support
detailsExtra context, aanwezig bij validatiefouten

Dezelfde request_id komt terug in de responseheader X-Request-ID, ook bij een geslaagde response. Elke logregel die het request bij ons schrijft, bevat dat id, zodat support het request op dat id alleen terugvindt. Stuur je eigen X-Request-ID mee (letters, cijfers, ., _ en -, maximaal 128 tekens) en Puntjes gebruikt dat in plaats van er zelf een aan te maken.


HTTP-statuscodes

StatusBeschrijving
200Gelukt
201Resource aangemaakt
202Aanvaard. Het werk staat in een wachtrij en is nog niet uitgevoerd
204Gelukt, en de response heeft geen body
400Puntjes kon dit request aan geen enkele handelaar koppelen, of de JSON-body is niet leesbaar
401Unauthorized. Ongeldig of verlopen token
403Forbidden. Handelaar opgeschort of gedeactiveerd
404Resource niet gevonden
405Het pad bestaat wel, maar niet voor deze HTTP-methode
409Conflict. De resource is al geregistreerd
415De API kan dit soort body niet lezen. Stuur JSON, of een formulier
422Validatiefout of overtreding van een bedrijfsregel
429Rate limit of planlimiet overschreden
500Interne serverfout
503Een dienst die het request nodig heeft, is nog niet klaar. Probeer opnieuw na het aantal seconden in de Retry-After-header

Overzicht foutcodes

Authenticatiefouten

CodeStatusBeschrijving
INVALID_CLIENT401Het token is geldig, maar de OAuth-client hoort bij geen enkele handelaar of mag de client credentials grant niet gebruiken. Een nieuw token helpt niet; kijk de client na
UNAUTHENTICATED401Het access token ontbreekt, is onleesbaar, verlopen of ingetrokken, of de client is ingetrokken. Vraag een nieuw token aan

Autorisatiefouten

CodeStatusBeschrijving
FORBIDDEN403Het token is geldig, maar mag deze actie niet uitvoeren
SUBSCRIPTION_EXPIRED403Het abonnement van deze handelaar is afgelopen. Lezen antwoordt nog altijd 200; schrijven ligt stil tot de handelaar weer een plan heeft. Wachten lost dat niet op, en daarom is dit een 403 en geen 429.
VENDOR_CONTEXT_MISSING500Het token wees naar geen enkele handelaar. Geef het opnieuw uit vanaf de API-client van de handelaar zelf
VENDOR_CONTEXT_REQUIRED400Dit endpoint moet weten om welke handelaar het gaat, en het token zei dat niet
VENDOR_DEACTIVATED403Het account van de handelaar is definitief gedeactiveerd
VENDOR_INACTIVE403Het account van de handelaar staat niet in een status die API-verkeer aanvaardt
VENDOR_PENDING403Het account van de handelaar is nog niet geactiveerd
VENDOR_SUSPENDED403Het account van de handelaar is opgeschort en de respijtperiode is voorbij

Validatiefouten

CodeStatusBeschrijving
BATCH_TOO_LARGE422Een batch mag hoogstens 100 producten bevatten. Splits je catalogus op en stuur hem in stukken
VALIDATION_ERROR422De request body of een queryparameter raakte niet door de validatie. In details staat per veld wat er misging

Fouten bij klantregistratie

CodeStatusBeschrijving
CUSTOMER_ALREADY_LINKED409De klant heeft al een andere external_id. Koppelen overschrijft nooit wat er al staat
EXTERNAL_ID_DUPLICATE409Deze external_id staat al bij deze handelaar geregistreerd
IDENTIFIER_DUPLICATE409Deze identifier of het e-mailadres (email) van de klant staat al bij deze handelaar geregistreerd, of een andere klant claimde de gescande kaart net eerder
LOYALTY_CARD_NOT_FOUND422De loyalty_card die je meestuurt is bij deze handelaar geen vrije kaart: de code is onbekend, of de handelaar heeft de kaart geblokkeerd. Scan opnieuw, of laat identifiers weg en laat Puntjes zelf een kaart uitgeven.
MARKETING_CONSENT_UNDER_AGE422De klant is volgens date_of_birth jonger dan 13, dus marketing_consent kan voor deze klant niet worden vastgelegd

Resourcefouten

CodeStatusBeschrijving
BRANCH_INACTIVE422De vestiging is gedeactiveerd. Je krijgt deze code bij een nieuwe transactie, en bij een credential waarvan de standaardvestiging intussen sloot. Puntjes gaat nooit stilzwijgend verder zonder vestiging
BRANCH_NOT_FOUND422Geen enkele vestiging van jou heeft deze branch-sleutel. Puntjes kijkt ook bij je gedeactiveerde vestigingen, dus je ziet het verschil tussen een typfout en een winkel die dicht is
CUSTOMER_DEACTIVATED422De klant bestaat, maar is gedeactiveerd, dus hij kan geen punten sparen of uitgeven
CUSTOMER_EMAIL_SUPPRESSED422Mail naar het adres van deze klant kwam terug, bij jouw zaak of bij een andere, of jouw mail werd als spam gemarkeerd. Daarom gaat zijn klantenkaart er niet naartoe tot het adres verbeterd is of het versturen weer aanstaat in het beheerportaal
CUSTOMER_HAS_NO_EMAIL422Van deze klant is geen e-mailadres bekend, dus zijn klantenkaart kan nergens naartoe
CUSTOMER_NOT_FOUND404Geen klant gevonden met deze identifier of dit ID
EXTERNAL_ID_NOT_FOUND404Geen enkele klant van jou heeft deze external_id. Verwijderde en geanonimiseerde klanten bereik je niet meer. Een gedeactiveerde klant vind je wel, en die krijgt CUSTOMER_DEACTIVATED
NO_WALLET422De klant heeft bij deze handelaar nog geen wallet, dus er is geen saldo om van af te schrijven
VOUCHER_NOT_FOUND404Geen enkele bon van jou heeft deze BON--code. Een code van een andere handelaar geeft dezelfde 404.
PRODUCT_EXTERNAL_ID_DUPLICATE409Er bestaat al een product met deze external_id. Gebruik PUT: die maakt aan of werkt bij
PRODUCT_NOT_FOUND404Geen enkel product van jou heeft deze external_id
REDEMPTION_NOT_FOUND404Geen inwisseling gevonden met deze bevestigingscode
REWARD_NOT_FOUND404Geen enkele beloning van jou heeft dit id
REWARD_UNAVAILABLE422De beloning is inactief, valt buiten de periode waarin ze beschikbaar is, of is niet gevonden
WALLET_NOT_FOUND404Deze klant heeft geen wallet, dus er valt niets te lezen of aan te passen

Bedrijfsregelfouten

CodeStatusBeschrijving
BRANCH_REQUIRED422Deze beloning of campagnebon geldt maar in een deel van je vestigingen, en je request noemde er geen enkele van: ofwel helemaal geen branch, ofwel een die niet in die lijst staat. Er wordt niets verzilverd, dus dezelfde aanroep slaagt zodra je een toegelaten vestiging meestuurt
CODE_ALREADY_USED422Deze inwisseling is al geverifieerd
CODE_CANCELLED422De zaak heeft de verzilvering geannuleerd en de punten teruggegeven, dus de bevestigingscode werkt niet meer
CODE_EXPIRED422De bevestigingscode is verlopen
IDEMPOTENCY_KEY_CONFLICT422Deze idempotency_key is al gebruikt, voor een andere klant, een ander bedrag of een andere beloning
INSUFFICIENT_BALANCE422De klant heeft niet genoeg punten voor deze inwisseling of wallet-aanpassing
OUT_OF_STOCK422Deze beloning is niet meer op voorraad
REDEMPTION_LIMIT_REACHED422De klant wisselde deze beloning al zo vaak in als de winkel toestaat
VERIFICATION_FAILED422De code raakte niet geverifieerd, en niet omdat hij verlopen of al gebruikt is
VOUCHER_ALREADY_USED422De bon is al verbruikt. Verifiëren is precies wat hem verbruikt, dus een tweede aanroep komt hier uit. Vang dat op voordat je uit coulance nog een korting geeft.
VOUCHER_EXPIRED422De bon is voorbij zijn valid_until-datum, en die dag telt zelf nog mee. De laatste geldige dag is dus nog niet "voorbij"

Rate limiting

CodeStatusBeschrijving
CARD_SEND_THROTTLED429De klant kreeg zijn klantenkaart net al opgestuurd. Anders dan de twee andere 429-codes verdwijnt deze binnen enkele seconden. details.retry_after zegt na hoeveel.
PLAN_LIMIT_EXCEEDED429Het abonnement van de handelaar heeft zijn transactietegoed voor deze periode opgebruikt
RATE_LIMITED429Te veel requests in dit venster. Wacht even en probeer opnieuw

Requestfouten

CodeStatusBeschrijving
INVALID_JSON400De body is als JSON verstuurd, maar is geen geldige JSON: hij is afgebroken of geen geldige UTF-8. Er is niets aangemaakt, gewijzigd of verstuurd. Herstel de body voor je hem opnieuw stuurt. Ook POST /oauth/token geeft deze code bij zo'n body
METHOD_NOT_ALLOWED405Het pad bestaat wel, maar niet voor deze HTTP-methode
ROUTE_NOT_FOUND404Op dit pad staat geen endpoint, of het pad bevat een NUL-byte of een byte die geen geldige UTF-8 is. Controleer de versieprefix en de spelling
UNSUPPORTED_MEDIA_TYPE415Het request heeft een body, maar de Content-Type is geen JSON, application/x-www-form-urlencoded of multipart/form-data, dus de API kan hem niet lezen. Er is niets aangemaakt, gewijzigd of verstuurd. Stuur de body als JSON met Content-Type: application/json. Een request zonder body heeft hier geen last van. Dit antwoord komt nog voor de controle van het token

Serverfouten

CodeStatusBeschrijving
APPLE_WALLET_UNAVAILABLE503De zaak kan nog geen passen voor Apple Wallet ondertekenen: Apple is nog niet klaar met haar pass-identiteit. Probeer opnieuw na het aantal seconden in Retry-After; het platform google merkt hier niets van
INTERNAL_ERROR500Er liep iets mis aan onze kant. Probeer opnieuw, en vermeld request_id als het blijft gebeuren

Afkomstig van POST /api/v1/customers. Het volledige contract van dat request staat bij Klant-endpoints.

De identifiers-array accepteert twee types

identifiers[].type aanvaardt precies twee waarden: email en loyalty_card. Elke andere waarde krijgt een VALIDATION_ERROR (422) terug, en de melding noemt de types die wel mogen, zodat je het zelf kunt oplossen. Stuurt je client een type dat er niet meer bij hoort, dan pas je die aan. Puntjes vangt oude waarden nergens op.

Lezen verandert niet: GET /customers/lookup zoekt alleen op de waarde zelf, dus elke identifier die je klanten al dragen, blijft werken.

Aan vier codes zie je of opnieuw proberen zin heeft, en maar drie daarvan zijn een 429. Kijk dus naar de code en niet naar de status: sinds de vierde in een andere statusklasse binnenkwam, houdt de status deze gevallen niet meer uit elkaar.

RATE_LIMITED gaat vanzelf over: de Retry-After-header zegt na hoeveel seconden je opnieuw kunt proberen. CARD_SEND_THROTTLED gaat ook vanzelf over, binnen enkele seconden, en begrenst de mailbox van één klant in plaats van jouw doorvoer. details.retry_after zegt na hoeveel seconden, en de Retry-After-header draagt hetzelfde getal. PLAN_LIMIT_EXCEEDED gaat binnen de periode niet over: het transactietegoed van het abonnement is voor deze periode op, en opnieuw proberen verbruikt alleen wat er nog rest. De volgende periode vult het weer aan, en een groter abonnement legt de grens hoger.

SUBSCRIPTION_EXPIRED is een 403, geen vierde 429. Schrijven stopt zodra het abonnement van een handelaar afloopt, terwijl lezen gewoon 200 blijft antwoorden, dus een GET krijgt deze code nooit te zien. Wachten lost het niet op: de handelaar blijft alleen-lezen tot die weer een plan heeft, en iemand moet dat plan kiezen. Een beheerder van de handelaar doet dat zelf onder Facturatie; een platformbeheerder kan er ook een namens de handelaar toewijzen. De code hoort bij dezelfde 403-familie als VENDOR_SUSPENDED en VENDOR_DEACTIVATED, met één verschil waar je rekening mee moet houden: die twee weigeren ook leesacties.


Details bij een validatiefout

Bij de foutcode VALIDATION_ERROR staat in het veld details per veld wat er misging:

{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "The given data was invalid.",
        "status": 422,
        "request_id": "...",
        "details": {
            "email": ["validation.email"],
            "identifiers.0.type": ["The identifier type must be one of: email, loyalty_card."]
        }
    }
}

Elke sleutel in details is een veldnaam, en de waarde is een array met de foutmeldingen voor dat veld. Een ingebouwde regel stuurt een sleutel zoals validation.email of validation.required, die je client zelf kan vertalen. Een eigen regel van Puntjes stuurt een Engelse zin. Een e-mailadres dat een andere klant van je al gebruikt, is geen validatiefout: dat geeft 409 IDENTIFIER_DUPLICATE.

Een verplicht veld dat je leeg stuurt ("" of null), kan twee sleutels krijgen: eerst die van het type (validation.string voor tekst, validation.numeric voor een getal), daarna validation.required. Laat je het veld weg, dan krijg je alleen validation.required. Zoek validation.required dus in de hele array, niet alleen als eerste. In POST /products/batch staan dezelfde sleutels in de errors van het item.

Ook queryparameters en de velden van een form-encoded of multipart body gaan door de validatie. Tekst die na het decoderen van de percent-encoding geen geldige UTF-8 is, geeft een 422, en details noemt het veld. GET /products?search=%FF geeft dus een 422 met details.search, want na het decoderen is %FF één losse byte, en dat is geen UTF-8-teken. Bij een lijst zoals metadata noemt details de lijst. Een JSON-body met zo'n byte is helemaal geen geldige JSON, dus die geeft in plaats daarvan 400 INVALID_JSON, net als een JSON-body die afgebroken is. Een lege body, {}, [] en null zijn geldig en houden hun antwoord. Een body van een ander type, zoals text/plain, geeft 415 UNSUPPORTED_MEDIA_TYPE. Hoofdletters in het type maken niet uit: de API leest Application/JSON gewoon als JSON. Een form-encoded of multipart body zonder één veld dat het endpoint leest, geeft 422 VALIDATION_ERROR, en details noemt elke sleutel die erin stond. Meestal komt dat door curl -d '{"first_name":"Jan"}': curl stuurt dat als formulier met één sleutel, {"first_name":"Jan"}. Stuur in de plaats JSON met Content-Type: application/json. Beide antwoorden komen nog voor de controle van het token, dus je krijgt ze ook zonder token.

Drie waarden worden alleen gecontroleerd als het endpoint ze leest: identifier op GET /customers/lookup als je geen external_id meestuurt, identifier op GET /rewards als affordable aan staat, en marketing_consent_statement_version als marketing_consent true is. Een veld dat eerst op een andere regel faalt, houdt alleen de melding van die regel. In POST /products/batch faalt in plaats daarvan het item, met het veld in zijn errors, net als bij elk ander fout item. Dezelfde bytes krijgen altijd hetzelfde antwoord. Pas de codering in je client aan in plaats van opnieuw te proberen.

Een NUL-byte midden in een tekst, in de body of in de querystring, geeft ook een 422, met het veld in details: de database zou de tekst maar tot die byte bewaren. Een NUL-byte helemaal vooraan of achteraan valt eerst weg, net als een spatie. In POST /products/batch faalt een item met zo'n byte in zijn external_id op zichzelf.

Een datumfilter zoals date_from geeft een 422 bij een jaartal van vijf of meer cijfers, zoals 99999-01-01: PHP zou dat als een ander jaar lezen en de verkeerde rijen teruggeven.


Fouten afhandelen in je integratie

async function submitTransaction(data) {
    const response = await fetch('/api/v1/transactions', {
        method: 'POST',
        headers: {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
    });

    if (!response.ok) {
        const { error } = await response.json();

        switch (error.code) {
            case 'CUSTOMER_NOT_FOUND':
                // Vraag de gebruiker om de klant te registreren
                break;
            case 'PLAN_LIMIT_EXCEEDED':
                // Toon limietwaarschuwing, neem contact op met de leveranciersbeheerder
                break;
            case 'VALIDATION_ERROR':
                // Verwerk fouten per veld uit error.details
                break;
            default:
                // Log error.request_id voor ondersteuning
                console.error(`API error: ${error.code}`, error.request_id);
        }
    }

    return response.json();
}