# Fehlermodell

Alle `/v1/*`-Endpunkte und `inbound-webhook` nutzen das einheitliche Envelope:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "details": { "retry_after_seconds": 30 }
  }
}
```

| Feld | Beschreibung |
|---|---|
| `code` | Maschinenlesbarer Kurzname (snake_case). Stabil über Versionen. |
| `message` | Menschlich lesbar, Englisch. |
| `details` | Optional, Endpunkt-spezifische Zusatzinfos (z. B. Validation-Fehlerpfade). |

## Standard-Codes

| HTTP | `code` | Bedeutung | Header |
|---|---|---|---|
| 400 | `invalid_request` | Schema-Validierung oder Pflichtfeld fehlt. `details.fields` listet Pfade. | — |
| 400 | `invalid_json` | Body ist kein gültiges JSON. | — |
| 401 | `unauthorized` | API-Key/Token fehlt oder ungültig. | `WWW-Authenticate: Bearer` |
| 401 | `signature_invalid` | HMAC-Signatur stimmt nicht (inbound). | — |
| 401 | `timestamp_skew` | Timestamp > 300 s vom Server entfernt. | — |
| 403 | `forbidden` | Token gültig, aber Scope/Org-Mitgliedschaft fehlt. | — |
| 403 | `scope_required` | Token-Scope reicht nicht (`details.required_scope`). | — |
| 404 | `not_found` | Ressource oder Endpoint-Slug existiert nicht oder gehört nicht zur Org. | — |
| 409 | `conflict` | Eindeutigkeitsverletzung, z. B. doppelter Slug. | — |
| 409 | `idempotency_mismatch` | Gleicher `Idempotency-Key`, anderer Request-Body. | — |
| 410 | `gone` | Endpoint/Resource ist deprecated und entfernt. | `Sunset: <date>` |
| 413 | `payload_too_large` | Body überschreitet Limit (256 KB inbound, 4 MB API). | — |
| 422 | `unprocessable` | Business-Regel verletzt, z. B. `event_type` nicht in Allowlist. | — |
| 429 | `rate_limited` | Rate-Limit überschritten. `details.retry_after_seconds`. | `Retry-After: <s>` |
| 500 | `internal_error` | Unerwarteter Fehler. Request-ID in `details.request_id` für Support. | — |
| 502 | `upstream_error` | Externe Abhängigkeit (Storage, AI-Gateway) hat gefehlt. | — |
| 503 | `service_unavailable` | Wartung oder Überlast. | `Retry-After: <s>` |

## Idempotency

Writes (`POST`, `PATCH`, `DELETE`) akzeptieren `Idempotency-Key: <uuid>`.
Erneuter Request mit gleichem Key + Body → 200 mit dem ursprünglichen Resultat.
Gleicher Key + anderer Body → `409 idempotency_mismatch`.
Keys werden 24 h aufbewahrt.

## Rate-Limits

Standard pro Token: **120 req/min**, **5 000 req/h**. Org-Overrides möglich.
Bei `429`: `Retry-After` (Sekunden) beachten und Backoff nutzen.

## Request-ID

Jede Response enthält `X-Request-Id: <uuid>`. Bei Support-Anfragen mit angeben.

## Test-Mode (Dry-Run)

Schreibende Anfragen (`POST`, `PATCH`, `PUT`, `DELETE`) akzeptieren den Header
`X-Test-Mode: true`. Effekte:

- **Keine** Datenbank-Mutation.
- **Keine** Audit-Trail-Einträge.
- **Keine** Outbound-Webhooks oder E-Mails.
- Authentifizierung, Scope-Check, Org-Mitgliedschaft und Idempotency-Wiring
  laufen identisch zum echten Request.
- Antwort enthält `data.dry_run: true` und echo't das `would_apply`-Payload.
- Response-Header `X-Test-Mode: true` bestätigt den Modus.

Beispiel:

```bash
curl -X POST "$LABNOTE_BASE/v1/samples" \
  -H "Authorization: Bearer $LABNOTE_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Test-Mode: true" \
  -d '{"name":"probe-1","type":"liquid","status":"active"}'
```

```json
{
  "data": {
    "dry_run": true,
    "resource": "samples",
    "id": null,
    "method": "POST",
    "would_apply": { "name": "probe-1", "type": "liquid", "status": "active" }
  }
}
```

Reads ignorieren den Header.
