# Fouten en limieten

Elke fout heeft dezelfde vorm, een HTTP-status, een vast type en een stabiele code. De melding is Nederlands en bedoeld voor mensen; je programma kijkt naar `type` en `code`.

## De foutvorm

```json 422 Unprocessable Entity
{
  "error": {
    "type": "invalid_request",
    "code": "template_invalid",
    "message": "Onbekende template_id; zie GET /v1/organization voor de beschikbare templates",
    "request_id": "req_…"
  }
}
```

`request_id` staat ook in de kop `X-Request-Id`. Noem het als je contact opneemt over een verzoek.

## Fouttypes

| Type | HTTP | Betekenis |
| --- | --- | --- |
| `invalid_request` | 400, 413, 415, 422 | Ongeldige JSON, verkeerde Content-Type, te groot verzoek of een veld dat niet klopt. `code` zegt welk. |
| `authentication_error` | 401 | Geen, ongeldige of verlopen API-sleutel. |
| `permission_error` | 403 | De sleutel mist het recht, of de werkgever heeft de partner niet (meer) toegelaten. |
| `not_found` | 404 | Bestaat niet, of is niet zichtbaar voor deze sleutel. |
| `conflict` | 409 | Kan niet in deze toestand, bijvoorbeeld een onboarding die al is afgerond. |
| `rate_limited` | 429 | Te veel verzoeken. Wacht het aantal seconden uit `Retry-After`. |
| `api_error` | 500 | Fout aan onze kant. Opnieuw proberen met dezelfde `Idempotency-Key` is veilig. |

## Veelvoorkomende codes

| Code | HTTP | Wanneer |
| --- | --- | --- |
| `unauthorized` | 401 | Geen, ongeldige of verlopen sleutel, of de eigenaar van de sleutel heeft geen toegang meer. |
| `forbidden` | 403 | De sleutel mist het recht, de werkgever laat de partner niet toe, of de sleutel hoort bij een andere organisatie. |
| `organization_required` | 422 | Een partnersleutel zonder de kop `Agreli-Organization`. |
| `organization_invalid` | 422 | De kop `Agreli-Organization` is geen geldig `org_…`-id. |
| `idempotency_conflict` | 422 | Dezelfde `Idempotency-Key` met een andere inhoud. |
| `idempotency_in_progress` | 409 | Een verzoek met dezelfde `Idempotency-Key` wordt nog verwerkt. Probeer het zo opnieuw. |
| `api_version_invalid` | 422 | Een onbekende versie in de kop `Agreli-Version`. |
| `subscription_required` | 403 | De proef van de werkgever is afgelopen zonder abonnement, of het abonnement is opgezegd. Lopende onboardings gaan door; nieuwe niet. |
| `api_approvals_disabled` | 403 | Goedkeuren of de ID-controle via de API staat uit bij deze werkgever. |
| `template_invalid` | 422 | Onbekende of niet goedgekeurde `template_id`. |
| `location_invalid` | 422 | Onbekende `location_id`. |
| `terms_invalid` | 422 | `hours_per_week` of `hourly_wage_cents` ontbreekt of klopt niet. |
| `return_url_invalid` | 422 | De `return_url` is geen https-adres. |
| `return_url_not_allowed` | 422 | Het domein van de `return_url` staat niet bij de [toegestane domeinen](/docs/inbedden#toegestane-domeinen). |
| `metadata_invalid` | 422 | Metadata is geen plat object, of een sleutel of waarde voldoet niet. |
| `metadata_personal_data` | 422 | Een waarde in metadata lijkt op een persoonsgegeven. |
| `field_not_updatable` | 422 | `PATCH` met een ander veld dan `reference` of `metadata`. |
| `data_version_required` | 422 | Goedkeuren zonder de gecontroleerde `data_version`. |
| `limit_invalid`, `cursor_invalid` | 422 | Ongeldige `limit` of `starting_after` bij het bladeren. |
| `events_invalid`, `url_insecure`, `url_not_public` | 422 | Een webhookadres met een onbekende gebeurtenis, zonder https of naar een intern adres. |
| `overlap_invalid` | 422 | `overlap_hours` bij het vernieuwen van een webhookgeheim is geen getal van 0 tot 168. |
| `webhook_disabled`, `delivery_pending` | 422 | Opnieuw versturen naar een uitgeschakeld adres, of een aflevering die al klaarstaat. |
| `since_invalid`, `type_invalid` | 422 | `GET /v1/events` met een ongeldig tijdstip of een onbekende gebeurtenis. |
| `nothing_to_advance`, `payroll_busy` | 409 | Sandbox: er valt niets na te spelen, of de salarisverwerking loopt al. |
| `invalid_json`, `content_type`, `too_large` | 400, 415, 413 | Geen geldige JSON, geen `Content-Type: application/json`, of meer dan 100 kB. |
| `rate_limited` | 429 | Te veel verzoeken. |
| `internal` | 500 | Fout aan onze kant. |

Deze lijst is niet volledig: elke controle in de backend heeft een eigen code. Behandel een onbekende code op basis van het `type`.

## Snelheidslimiet

Per sleutel gelden 120 verzoeken per minuut. Elk antwoord vertelt hoe je ervoor staat:

| Kop | Betekenis |
| --- | --- |
| `RateLimit-Limit` | Het aantal verzoeken per minuut (120). |
| `RateLimit-Remaining` | Wat er deze minuut nog over is. |
| `RateLimit-Reset` | Seconden tot de teller opnieuw begint. |
| `Retry-After` | Alleen bij `429`: wacht zoveel seconden. |

## Opnieuw proberen

- Bij `429`: wacht het aantal seconden uit `Retry-After`.
- Bij `500`, een time-out of een verbroken verbinding: probeer opnieuw met dezelfde `Idempotency-Key`. Dan wordt niets dubbel aangemaakt.
- Bij andere `4xx`: niet blind herhalen. Pas het verzoek aan op basis van `code`.

## Bladeren

Lijsten (`GET /v1/onboardings`, `GET /v1/employees`, `GET /v1/webhooks/{id}/deliveries`, `GET /v1/events`) geven `{ "object": "list", "data": [...], "has_more": true, "next_cursor": "onb_…" }`. Vraag de volgende pagina op met `starting_after=<next_cursor>`. `limit` is 1 tot 100, standaard 25 (50 bij events). De nieuwste staan bovenaan, behalve bij `GET /v1/events`: daar de oudste.

```bash
curl "https://app.agreli.nl/api/public/v1/onboardings?status=in_review&limit=50&starting_after=onb_…" \
  -H "Authorization: Bearer $AGRELI_API_KEY"
```
