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": {}
}
}
| Veld | Beschrijving |
|---|---|
code | Machine-leesbare foutcode. Hierop laat je je integratie beslissen |
message | Foutmelding voor mensen. Geschikt voor je logs, niet voor je eindgebruikers |
status | HTTP-statuscode, dezelfde als die van de response zelf |
request_id | Uniek id van dit request, voor debuggen en support |
details | Extra 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
| Status | Beschrijving |
|---|---|
200 | Gelukt |
201 | Resource aangemaakt |
202 | Aanvaard. Het werk staat in een wachtrij en is nog niet uitgevoerd |
204 | Gelukt, en de response heeft geen body |
400 | Puntjes kon dit request aan geen enkele handelaar koppelen, of de JSON-body is niet leesbaar |
401 | Unauthorized. Ongeldig of verlopen token |
403 | Forbidden. Handelaar opgeschort of gedeactiveerd |
404 | Resource niet gevonden |
405 | Het pad bestaat wel, maar niet voor deze HTTP-methode |
409 | Conflict. De resource is al geregistreerd |
415 | De API kan dit soort body niet lezen. Stuur JSON, of een formulier |
422 | Validatiefout of overtreding van een bedrijfsregel |
429 | Rate limit of planlimiet overschreden |
500 | Interne serverfout |
503 | Een dienst die het request nodig heeft, is nog niet klaar. Probeer opnieuw na het aantal seconden in de Retry-After-header |
Overzicht foutcodes
Authenticatiefouten
| Code | Status | Beschrijving |
|---|---|---|
INVALID_CLIENT | 401 | Het 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 |
UNAUTHENTICATED | 401 | Het access token ontbreekt, is onleesbaar, verlopen of ingetrokken, of de client is ingetrokken. Vraag een nieuw token aan |
Autorisatiefouten
| Code | Status | Beschrijving |
|---|---|---|
FORBIDDEN | 403 | Het token is geldig, maar mag deze actie niet uitvoeren |
SUBSCRIPTION_EXPIRED | 403 | Het 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_MISSING | 500 | Het token wees naar geen enkele handelaar. Geef het opnieuw uit vanaf de API-client van de handelaar zelf |
VENDOR_CONTEXT_REQUIRED | 400 | Dit endpoint moet weten om welke handelaar het gaat, en het token zei dat niet |
VENDOR_DEACTIVATED | 403 | Het account van de handelaar is definitief gedeactiveerd |
VENDOR_INACTIVE | 403 | Het account van de handelaar staat niet in een status die API-verkeer aanvaardt |
VENDOR_PENDING | 403 | Het account van de handelaar is nog niet geactiveerd |
VENDOR_SUSPENDED | 403 | Het account van de handelaar is opgeschort en de respijtperiode is voorbij |
Validatiefouten
| Code | Status | Beschrijving |
|---|---|---|
BATCH_TOO_LARGE | 422 | Een batch mag hoogstens 100 producten bevatten. Splits je catalogus op en stuur hem in stukken |
VALIDATION_ERROR | 422 | De request body of een queryparameter raakte niet door de validatie. In details staat per veld wat er misging |
Fouten bij klantregistratie
| Code | Status | Beschrijving |
|---|---|---|
CUSTOMER_ALREADY_LINKED | 409 | De klant heeft al een andere external_id. Koppelen overschrijft nooit wat er al staat |
EXTERNAL_ID_DUPLICATE | 409 | Deze external_id staat al bij deze handelaar geregistreerd |
IDENTIFIER_DUPLICATE | 409 | Deze 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_FOUND | 422 | De 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_AGE | 422 | De klant is volgens date_of_birth jonger dan 13, dus marketing_consent kan voor deze klant niet worden vastgelegd |
Resourcefouten
| Code | Status | Beschrijving |
|---|---|---|
BRANCH_INACTIVE | 422 | De 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_FOUND | 422 | Geen 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_DEACTIVATED | 422 | De klant bestaat, maar is gedeactiveerd, dus hij kan geen punten sparen of uitgeven |
CUSTOMER_EMAIL_SUPPRESSED | 422 | Mail 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_EMAIL | 422 | Van deze klant is geen e-mailadres bekend, dus zijn klantenkaart kan nergens naartoe |
CUSTOMER_NOT_FOUND | 404 | Geen klant gevonden met deze identifier of dit ID |
EXTERNAL_ID_NOT_FOUND | 404 | Geen 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_WALLET | 422 | De klant heeft bij deze handelaar nog geen wallet, dus er is geen saldo om van af te schrijven |
VOUCHER_NOT_FOUND | 404 | Geen enkele bon van jou heeft deze BON--code. Een code van een andere handelaar geeft dezelfde 404. |
PRODUCT_EXTERNAL_ID_DUPLICATE | 409 | Er bestaat al een product met deze external_id. Gebruik PUT: die maakt aan of werkt bij |
PRODUCT_NOT_FOUND | 404 | Geen enkel product van jou heeft deze external_id |
REDEMPTION_NOT_FOUND | 404 | Geen inwisseling gevonden met deze bevestigingscode |
REWARD_NOT_FOUND | 404 | Geen enkele beloning van jou heeft dit id |
REWARD_UNAVAILABLE | 422 | De beloning is inactief, valt buiten de periode waarin ze beschikbaar is, of is niet gevonden |
WALLET_NOT_FOUND | 404 | Deze klant heeft geen wallet, dus er valt niets te lezen of aan te passen |
Bedrijfsregelfouten
| Code | Status | Beschrijving |
|---|---|---|
BRANCH_REQUIRED | 422 | Deze 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_USED | 422 | Deze inwisseling is al geverifieerd |
CODE_CANCELLED | 422 | De zaak heeft de verzilvering geannuleerd en de punten teruggegeven, dus de bevestigingscode werkt niet meer |
CODE_EXPIRED | 422 | De bevestigingscode is verlopen |
IDEMPOTENCY_KEY_CONFLICT | 422 | Deze idempotency_key is al gebruikt, voor een andere klant, een ander bedrag of een andere beloning |
INSUFFICIENT_BALANCE | 422 | De klant heeft niet genoeg punten voor deze inwisseling of wallet-aanpassing |
OUT_OF_STOCK | 422 | Deze beloning is niet meer op voorraad |
REDEMPTION_LIMIT_REACHED | 422 | De klant wisselde deze beloning al zo vaak in als de winkel toestaat |
VERIFICATION_FAILED | 422 | De code raakte niet geverifieerd, en niet omdat hij verlopen of al gebruikt is |
VOUCHER_ALREADY_USED | 422 | De 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_EXPIRED | 422 | De 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
| Code | Status | Beschrijving |
|---|---|---|
CARD_SEND_THROTTLED | 429 | De 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_EXCEEDED | 429 | Het abonnement van de handelaar heeft zijn transactietegoed voor deze periode opgebruikt |
RATE_LIMITED | 429 | Te veel requests in dit venster. Wacht even en probeer opnieuw |
Requestfouten
| Code | Status | Beschrijving |
|---|---|---|
INVALID_JSON | 400 | De 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_ALLOWED | 405 | Het pad bestaat wel, maar niet voor deze HTTP-methode |
ROUTE_NOT_FOUND | 404 | Op 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_TYPE | 415 | Het 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
| Code | Status | Beschrijving |
|---|---|---|
APPLE_WALLET_UNAVAILABLE | 503 | De 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_ERROR | 500 | Er 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();
}