# Authenticatie

Elke request naar een `/v1/api`-route wordt geauthenticeerd met een tenant-API-key. De key is een bearer token dat bij één winkel (tenant) hoort: hij authenticeert de request én selecteert de winkel, dus een aparte tenant-identifier of header is nooit nodig. Deze pagina behandelt hoe keys werken, welke scopes er zijn, het identity-endpoint en wat er gebeurt wanneer een winkel wordt geschorst.

## API keys

Een API-key is een langlevend geheim token dat voor één winkel wordt uitgegeven. Keys verlopen niet; ze blijven geldig tot de merchant ze intrekt. De plaintext key wordt precies één keer getoond, bij het aanmaken. Sla hem direct op in een secret manager; hij kan niet opnieuw worden opgevraagd.

Merchants maken keys aan en trekken ze in via het client-dashboard (app.borkol.com) onder Settings > API keys. Bij het aanmaken kiest de merchant een naam en een set scopes voor de key. Het dashboard toont per key de naam, scopes, het laatste gebruiksmoment en het aanmaakmoment, en kan een key op elk moment intrekken. Een ingetrokken key werkt onmiddellijk niet meer: requests ermee geven `401 {"message": "Unauthenticated."}` terug.

Stuur de key bij elke request mee in de `Authorization`-header:

```
Authorization: Bearer {api_key}
```

Er is geen `X-Tenant-Id`-header op `/v1/api`-routes en er wordt er geen gelezen. De winkelcontext wordt volledig afgeleid uit de key.

## Scopes

Elke key draagt een set scopes die bij het aanmaken zijn toegekend. Scopes bewaken endpoints: een route die een scope vereist die de key niet draagt, geeft `403 {"message": "Invalid ability provided."}` terug.

Scopenamen volgen het patroon `{module}:{read|write}`. De volledige set:

| Scope | Geeft toegang tot |
| --- | --- |
| `tenant:read` | Basale toegang tot de winkelidentiteit (`GET /v1/api/business`, de redirect- en sitemap-endpoints). Altijd toekenbaar. |
| `catalog:read` | Producten, categorieën en de configurator-endpoints. `commerce:read` wordt op dezelfde routes geaccepteerd. |
| `catalog:write` | Toekenbaar; nog geen `/v1/api`-route gebruikt hem. |
| `commerce:read` | Winkelwagens (lezen), bestellingen, en overal waar `catalog:read` of `sales:read` wordt geaccepteerd. |
| `commerce:write` | Winkelwagens (schrijven), checkout, offerteaanvragen, en overal waar `sales:write` wordt geaccepteerd. |
| `configurator:read` / `configurator:write` | Toekenbaar; de configurator-endpoints controleren `catalog:read` of `commerce:read`, niet deze. |
| `contacts:read` | GET-endpoints van klantaccounts (`/v1/api/contacts/*`). |
| `contacts:write` | Registratie, inloggen, uitloggen, wachtwoordherstel, profiel- en adreswijzigingen van klanten. |
| `customers:read` / `customers:write` | De vroegere namen van `contacts:read` / `contacts:write`. Nog steeds toekenbaar en geaccepteerd op dezelfde routes, zodat keys van vóór de hernoeming blijven werken. |
| `sales:read` | Een offerte lezen via token (`GET /v1/api/sales/quotes/by-token/{token}`). |
| `sales:write` | Een offerte accepteren of afwijzen via token. |
| `stock:read` | Voorraadniveaus (`GET /v1/api/stock/*`). |
| `stock:write` | Toekenbaar; nog geen `/v1/api`-route gebruikt hem. |
| `objects:read` | Objecttypes en -records (`GET /v1/api/objects/*`), en de sitemap. |
| `objects:write` | Toekenbaar; nog geen `/v1/api`-route gebruikt hem. |
| `forms:read` | Gepubliceerde formulieren laden (`GET /v1/api/forms/{key}`) en de adreslookup. |
| `forms:write` | Bestanden uploaden voor bestandsvelden van formulieren (`POST /v1/api/forms/{key}/uploads`) en formulierinzendingen versturen (`POST /v1/api/forms/{key}/submissions`). Alleen op de server; zie hieronder. |

Een module zonder publieke surface (payroll) biedt haar permissienamen ook als scopes aan; geen enkele `/v1/api`-route gebruikt ze. Waar een route twee scopes noemt, volstaat een key met één van beide: de oudere `commerce:*`-familie dateert van vóór de splitsing van de commerce-module in catalog, commerce en sales, en wordt op alle drie geaccepteerd.

Een merchant kan alleen scopes toekennen voor modules die op dat moment voor zijn winkel zijn ingeschakeld: de toekenbare set is altijd `tenant:read` plus het `{module}:read`- en `{module}:write`-paar van elke ingeschakelde module. Het aanvragen van een niet-beschikbare scope bij het aanmaken van een key faalt met een 422 in de dashboardflow.

**Houd `forms:write`-sleutels op je server.** Met een sleutel met `forms:write` verstuur je inzendingen, en elke inzending verstuurt mail namens de merchant. Lever hem nooit uit aan een browser of een mobiele app: laad en verstuur formulieren vanaf je eigen server, en gebruik aparte sleutels voor het laden (`forms:read`) en het versturen (`forms:write`). Zie [Formulieren](/nl/api/forms/index#houd-formswrite-sleutels-op-je-server).

Scopes en module-inschakeling worden onafhankelijk van elkaar afgedwongen. Zelfs wanneer een key `commerce:read` draagt, geeft het aanroepen van een commerce-route terwijl de commerce-module voor de winkel is uitgeschakeld `403 {"error": "Module not enabled: commerce"}` terug. Lees-endpoints vereisen de `read`-scope van de module; create-, update- en delete-endpoints vereisen de `write`-scope van de module. De referentiepagina van elk endpoint vermeldt de vereiste scope.

## Verwerkingsvolgorde van requests

Middleware draait in een vaste volgorde op elke `/v1/api`-request. De volgorde kennen helpt bij het interpreteren van error-responses:

1. **Authenticatie**: het bearer token wordt herleid tot een winkel. Ontbrekend, misvormd of ingetrokken token: `401 {"message": "Unauthenticated."}`.
2. **Tenancy en suspensie**: de winkelcontext wordt geïnitialiseerd vanuit de key. Geschorste winkel: `403 {"message": "Tenant suspended.", "code": "tenant_suspended"}`.
3. **Rate limiting**: 120 requests per minuut per winkel. Overschreden: `429 {"message": "Too Many Attempts."}`.
4. **Module-gate en scope-check** (alleen module-routes): beide draaien na de bovenstaande stappen, maar hun onderlinge volgorde verschilt per route. Uitgeschakelde module: `403 {"error": "Module not enabled: {key}"}`. Ontbrekende ability: `403 {"message": "Invalid ability provided."}`. De checks zijn onafhankelijk; wanneer beide zouden falen, kan elk van de twee errors als eerste optreden.

## Tenant-suspensie

Borkol kan een winkel schorsen. Zolang de winkel geschorst is, geeft elke `/v1/api`-request met de keys van die winkel, ongeacht scope of endpoint, dit terug:

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

met HTTP-status 403. Het `code`-veld is stabiel en machine-leesbaar; controleer op `tenant_suspended` om deze 403 te onderscheiden van scope- en module-errors. Suspensie maakt keys niet ongeldig: wanneer de schorsing wordt opgeheven, werken bestaande keys ongewijzigd weer.

## GET /v1/api/me

Geeft de identiteit terug van de winkel waar de aangeboden API-key bij hoort, plus de scopes die aan die key zijn toegekend. Dit is de canonieke eerste aanroep om te verifiëren dat een key werkt en om zijn abilities te inspecteren. Het endpoint vereist geen scope; elke geldige key kan het aanroepen.

### Parameters

Geen. Alleen de header `Authorization: Bearer {api_key}` is vereist.

### Responsevelden

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `tenant` | object | altijd | De winkel waar de key bij hoort. |
| `tenant.id` | string (UUID) | altijd | Winkel-ID. |
| `tenant.name` | string | altijd | Weergavenaam van de winkel. |
| `tenant.slug` | string | altijd | URL-veilige winkelidentifier. |
| `scopes` | array van strings | altijd | De scopes die aan deze key zijn toegekend, bijvoorbeeld `["tenant:read", "commerce:read"]`. |

```bash title="Request"
curl https://api.borkol.com/v1/api/me \
  -H "Authorization: Bearer {api_key}"
```

```javascript title="Request (JS)"
const res = await fetch("https://api.borkol.com/v1/api/me", {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const me = await res.json();
```

```json title="Response"
{
  "tenant": {
    "id": "9c4e7f0a-2b31-4d6e-8f0a-1d2c3b4a5e6f",
    "name": "Example Store",
    "slug": "example-store"
  },
  "scopes": ["tenant:read", "commerce:read", "commerce:write", "stock:read"]
}
```

### Errors

| Status | Body | Wanneer |
| --- | --- | --- |
| 401 | `{"message": "Unauthenticated."}` | Bearer token ontbreekt, is misvormd of ingetrokken. |
| 403 | `{"message": "Tenant suspended.", "code": "tenant_suspended"}` | De winkel is geschorst. |
| 429 | `{"message": "Too Many Attempts."}` | Meer dan 120 requests in de huidige minuut. |

## GET /v1/ping

Ongeauthenticeerde health check voor API v1. Er gelden geen credentials, geen winkelcontext, geen scopes en geen tenant-rate-limit. Gebruik het voor uptime-monitoring en om connectiviteit en de base URL te verifiëren voordat je credentials aansluit.

### Parameters

Geen.

### Responsevelden

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `status` | string | altijd | Altijd `"ok"`. |
| `version` | string | altijd | Altijd `"v1"`. |

```bash title="Request"
curl https://api.borkol.com/v1/ping
```

```json title="Response"
{
  "status": "ok",
  "version": "v1"
}
```

### Errors

Geen die specifiek zijn voor deze route.
