# Errors en rate limits

De storefront-API signaleert fouten met conventionele HTTP-statuscodes en kleine JSON-bodies. Deze pagina catalogiseert elke error-envelope die de API produceert, legt de vorm van validatiefouten uit, documenteert de rate limit en definieert de paginatie-envelope die alle lijst-endpoints gebruiken. Endpoint-referentiepagina's linken hierheen in plaats van deze vormen te herhalen.

Alle error-bodies zijn JSON-objecten. De meeste dragen een `message`-veld; meerdere bijzondere gevallen wijken af en worden hieronder benoemd: de module-disabled-error gebruikt een `error`-key, en de suspensie-error, geweigerde cartregels, schendingen van het cart- en checkoutbeleid en sommige weigeringen op offertetokens voegen een machine-leesbare `code` toe.

## 401 Unauthenticated

Wordt teruggegeven wanneer de header `Authorization: Bearer {api_key}` ontbreekt, misvormd is of een ingetrokken key draagt. Wordt ook teruggegeven wanneer de header een ander schema dan `Bearer` gebruikt.

```json title="Response"
{
  "message": "Unauthenticated."
}
```

Oplossing: stuur een geldige, niet-ingetrokken API-key als `Authorization: Bearer {api_key}`. Verifieer de key met `GET /v1/api/me`.

## 403 Forbidden

Drie verschillende condities geven 403 terug. Onderscheid ze aan de vorm van de body.

### Tenant geschorst

De winkel waar de key bij hoort is door Borkol geschorst. Elke `/v1/api`-request faalt met deze body tot de schorsing wordt opgeheven. Het `code`-veld is stabiel; vertak op `code === "tenant_suspended"`.

```json title="Response"
{
  "message": "Tenant suspended.",
  "code": "tenant_suspended"
}
```

### Ontbrekende scope

De key is geldig maar draagt niet de scope die het endpoint vereist (bijvoorbeeld het aanroepen van een `commerce:write`-endpoint met een key die alleen `commerce:read` heeft).

```json title="Response"
{
  "message": "Invalid ability provided."
}
```

Oplossing: maak in het client-dashboard (Settings > API keys) een key aan met de vereiste scope. Scopes kunnen niet aan een bestaande key worden toegevoegd.

### Module niet ingeschakeld

De route hoort bij een module die niet is ingeschakeld voor de winkel. Deze error gebruikt een `error`-key, geen `message`, en wint van de scope-check: hij wordt zelfs teruggegeven wanneer de key de scope van de module draagt. `{key}` is de module-key: `catalog`, `commerce`, `configurator`, `sales`, `stock`, `objects` of `forms`.

De body bevat ook `code: "module_disabled"` en `module` (alleen de module-key), zodat een client op `code` kan vertakken in plaats van `error` te parsen. `error` blijft ongewijzigd.

Eén route antwoordt deze body met status 404 in plaats van 403: `POST /v1/api/commerce/carts/{id}/quote-request` zonder de sales-module geeft `404 {"error":"Module not enabled: sales"}`, dezelfde status als offerteaanvragen die in de winkelinstellingen zijn uitgeschakeld, zodat een storefront één "niet beschikbaar"-geval hoeft af te handelen.

```json title="Response"
{
  "error": "Module not enabled: commerce",
  "code": "module_disabled",
  "module": "commerce"
}
```

Oplossing: module-inschakeling wordt per winkel door Borkol beheerd; neem contact op met Borkol om de module in te schakelen.

## 404 Not Found

Wordt teruggegeven voor onbekende routes en voor padparameters die niet naar een bestaand record binnen de winkel verwijzen. De body is `{"message": "..."}`; bij een mislukte model-lookup is het bericht de standaard van Laravel, bijvoorbeeld:

```json title="Response"
{
  "message": "No query results for model [Borkol\\Catalog\\Models\\Product] 9c4e7f0a-2b31-4d6e-8f0a-1d2c3b4a5e6f"
}
```

Records die bij een andere winkel horen zijn niet te onderscheiden van niet-bestaande records: beide geven 404 terug, nooit 403.

## 422 Validatiefout

Wordt teruggegeven wanneer een request body of queryparameter de validatie niet doorstaat. De vorm is altijd:

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `message` | string | altijd | Het eerste validatiefoutbericht (een leesbare samenvatting). |
| `errors` | object | altijd | Map van veldnaam naar een array van één of meer foutberichten voor dat veld. Geneste velden gebruiken puntnotatie (bijvoorbeeld `lines.0.quantity`). |

```json title="Response"
{
  "message": "The email field is required.",
  "errors": {
    "email": ["The email field is required."],
    "lines.0.quantity": ["The lines.0.quantity must be greater than zero."]
  }
}
```

De referentiepagina van elk endpoint vermeldt zijn velden met types, verplicht-vlaggen en validatieregels; de 422-body volgt altijd deze envelope.

### Weigeringscode voor cartregels

Wanneer `POST /v1/api/commerce/carts/{id}/lines` een product weigert, voegt de 422-body een `code` op het hoogste niveau toe naast `message` en `errors`:

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `code` | string | conditioneel | `CAPABILITY_INACTIVE` (de module die de capability van het product verkoopt is uitgeschakeld), `INVALID_CONFIGURATION` (variant of configuratie past niet bij het product) of `NOT_PURCHASABLE` (het product kan niet verkocht worden). Afwezig bij gewone invoervalidatiefouten. |

```json title="Response"
{
  "message": "Product is not available.",
  "errors": {
    "product_id": ["Product is not available."]
  },
  "code": "CAPABILITY_INACTIVE"
}
```

Behandel de `code`-enum als open. Het quote-endpoint van de configurator meldt configuratiefouten in de gewone envelope zonder `code`. Zie [Cartregels](/nl/api/carts-and-checkout/cart-lines).

## 429 Too Many Attempts (rate limiting)

Alle `/v1/api`-routes delen één rate limit van **120 requests per minuut per winkel**. De limiet wordt bepaald per winkel (tenant), niet per API-key of IP: alle keys van één winkel putten uit hetzelfde budget. Ongeauthenticeerde requests naar rate-gelimiteerde routes worden per client-IP geteld. `GET /v1/ping` valt niet onder deze limiet.

Elke response op een rate-gelimiteerde route bevat:

| Header | Beschrijving |
| --- | --- |
| `X-RateLimit-Limit` | Het budget per minuut (120). |
| `X-RateLimit-Remaining` | Resterende requests in het huidige venster. |

Wanneer het budget op is, geeft de API HTTP 429 terug met een `Retry-After`-header (seconden tot het venster reset):

```json title="Response"
{
  "message": "Too Many Attempts."
}
```

Clients horen `X-RateLimit-Remaining` in de gaten te houden, terug te schakelen voordat de teller nul raakt en `Retry-After` te respecteren bij een 429. Cache voor catalogus-zware storefronts de reads aan jouw kant in plaats van elke paginaweergave naar de API te proxyen.

Formulierinzendingen hebben daarnaast een eigen limiet: elk formulier accepteert een door de merchant ingesteld aantal inzendingen per bezoeker per uur. Daarboven antwoordt `POST /v1/api/forms/{key}/submissions` met `429 {"message": "Too many submissions. Try again later."}`, zonder `Retry-After`-header. Zie [Inzendingen](/nl/api/forms/submissions). Bestandsuploads voor formulieren zijn beperkt tot 30 per formulier per bezoeker per uur: daarboven antwoordt `POST /v1/api/forms/{key}/uploads` met `429 {"message": "Too many uploads. Try again later."}`, ook zonder `Retry-After`. Zie [Uploads](/nl/api/forms/uploads).

## Paginatie-envelope

Elk lijst-endpoint in de storefront-API geeft dezelfde envelope terug: een `data`-array plus `links`- en `meta`-objecten. Dit is hier eenmalig gedocumenteerd; endpoint-pagina's beschrijven alleen de vorm van de objecten binnen `data`.

Lijst-endpoints accepteren deze queryparameters:

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `page` | integer | Nee | Paginanummer, beginnend bij 1. Standaard 1. |
| `per_page` | integer | Nee | Items per pagina. Elk endpoint documenteert zijn standaard en maximum (doorgaans standaard 25, maximum 100); waarden buiten het toegestane bereik worden begrensd. |

De envelope:

| Naam | Type | Beschrijving |
| --- | --- | --- |
| `data` | array | De pagina met resources. |
| `links.first` | string | URL van de eerste pagina. |
| `links.last` | string | URL van de laatste pagina. |
| `links.prev` | string of null | URL van de vorige pagina, null op de eerste pagina. |
| `links.next` | string of null | URL van de volgende pagina, null op de laatste pagina. |
| `meta.current_page` | integer | Huidig paginanummer. |
| `meta.from` | integer of null | 1-gebaseerde index van het eerste item op deze pagina, null wanneer de pagina leeg is. |
| `meta.last_page` | integer | Totaal aantal pagina's. |
| `meta.links` | array | Paginator-linkobjecten (`url`, `label`, `active`) voor het bouwen van paginanavigatie. |
| `meta.path` | string | Basis-URL van het endpoint zonder paginatieparameters. |
| `meta.per_page` | integer | Effectieve paginagrootte. |
| `meta.to` | integer of null | 1-gebaseerde index van het laatste item op deze pagina, null wanneer de pagina leeg is. |
| `meta.total` | integer | Totaal aantal overeenkomende items. |

```json title="Response"
{
  "data": [
    { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" }
  ],
  "links": {
    "first": "https://api.borkol.com/v1/api/commerce/products?page=1",
    "last": "https://api.borkol.com/v1/api/commerce/products?page=4",
    "prev": null,
    "next": "https://api.borkol.com/v1/api/commerce/products?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 4,
    "links": [
      { "url": null, "label": "&laquo; Previous", "active": false },
      { "url": "https://api.borkol.com/v1/api/commerce/products?page=1", "label": "1", "active": true }
    ],
    "path": "https://api.borkol.com/v1/api/commerce/products",
    "per_page": 25,
    "to": 25,
    "total": 87
  }
}
```

Endpoints die één object teruggeven (zoals `GET /v1/api/me`) geven het kale JSON-object terug zonder `data`-wrapper, tenzij hun referentiepagina anders vermeldt.

## Samenvatting foutafhandeling voor clients

| Status | Herken aan | Actie |
| --- | --- | --- |
| 401 | `message == "Unauthenticated."` | Key ontbreekt of is ingetrokken; regel een geldige key. Niet opnieuw proberen. |
| 403 | `code == "tenant_suspended"` | Winkel geschorst; stop met aanroepen tot dit is opgelost. |
| 403 | `message == "Invalid ability provided."` | Key mist de scope; geef een key uit met de juiste scopes. Niet opnieuw proberen. |
| 403 | `code == "module_disabled"` (of `error` beginnend met `"Module not enabled:"`) | Module uitgeschakeld voor de winkel. Niet opnieuw proberen. |
| 404 | `message` aanwezig, of `error` beginnend met `"Module not enabled:"` | Route of record bestaat niet in deze winkel, of (alleen offerteaanvraag) de sales-module is uitgeschakeld. |
| 422 | `code` is `CAPABILITY_INACTIVE`, `INVALID_CONFIGURATION` of `NOT_PURCHASABLE`, met `errors` | Cartregel geweigerd; branch op `code`, toon `errors`. |
| 422 | `code` aanwezig, geen `errors` | Beleids- of bedrijfsweigering; toon `message`. Codes: `cart_constraint` (cartregels), `login_required`, `guest_only`, `b2b_approval_required`, `b2c_only`, `min_order_value` (checkout), `SIGNATURE_REQUIRED`, `SIGNATURE_INVALID` (offerte accepteren). |
| 422 | `errors`-object aanwezig | Corrigeer de genoemde velden en stuur opnieuw. |
| 429 | `message == "Too Many Attempts."` | Wacht `Retry-After` seconden en probeer opnieuw. |
| 429 | `message == "Too many submissions. Try again later."` | Limiet voor formulierinzendingen van deze bezoeker; vraag de bezoeker het later opnieuw te proberen. |
| 429 | `message == "Too many uploads. Try again later."` | Limiet voor formulieruploads van deze bezoeker; vraag de bezoeker het later opnieuw te proberen. |
| 5xx | elke | Serverfout; probeer opnieuw met exponentiële backoff. |
