# Inbound Webhooks

Externe Systeme (Geräte, LIMS, ERP, AI-Agenten) können Events an LabNote pushen.
Endpoints werden pro Organisation in der Admin-UI unter **Einstellungen → Integrationen → Inbound Webhooks** angelegt.

Base-URL:

```
POST https://vilasdqkwlszlulteqrb.supabase.co/functions/v1/inbound-webhook/{endpoint_slug}
```

`{endpoint_slug}` wird beim Anlegen des Endpoints vergeben (URL-safe, eindeutig pro Org).

## Authentifizierung — HMAC-SHA256

Jede Anfrage muss zwei Header tragen:

| Header | Inhalt |
|---|---|
| `X-Labnote-Timestamp` | Unix-Sekunden, max. ±300 s Skew |
| `X-Labnote-Signature` | `t=<ts>,v1=<hex-hmac>` |

Signatur wird über `{timestamp}.{raw_body}` mit dem beim Anlegen erzeugten `signing_secret` gebildet.

### Beispiel (TypeScript / Deno / Node)

```ts
import { createHmac } from "node:crypto";

const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify(payload);
const sig = createHmac("sha256", signingSecret)
  .update(`${ts}.${body}`)
  .digest("hex");

await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Labnote-Timestamp": ts,
    "X-Labnote-Signature": `t=${ts},v1=${sig}`,
    "Idempotency-Key": crypto.randomUUID(),
  },
  body,
});
```

### Beispiel (Python)

```python
import hmac, hashlib, json, time, uuid, requests

ts   = str(int(time.time()))
body = json.dumps(payload, separators=(",", ":"))
sig  = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()

requests.post(url, data=body, headers={
    "Content-Type":         "application/json",
    "X-Labnote-Timestamp":  ts,
    "X-Labnote-Signature":  f"t={ts},v1={sig}",
    "Idempotency-Key":      str(uuid.uuid4()),
})
```

## Request-Schema

```json
{
  "event_type": "instrument.measurement_recorded",
  "occurred_at": "2026-06-19T20:15:00Z",
  "external_id": "device-42-run-2026-06-19T20:15:00Z",
  "payload": {
    "instrument_serial": "HPLC-A-001",
    "method": "USP-621",
    "results": [{ "peak": "API", "area": 12345, "rt_min": 4.21 }]
  }
}
```

| Feld | Pflicht | Beschreibung |
|---|---|---|
| `event_type` | ja | Frei wählbar, Punkt-Notation empfohlen (`<domain>.<action>`). Wird gegen `allowed_event_types` des Endpoints geprüft (falls gesetzt). |
| `occurred_at` | ja | ISO-8601-Zeitstempel des Ereignisses. |
| `external_id` | empfohlen | Idempotenz-Anker des Senders. Doppelte `(endpoint_id, external_id)` werden mit HTTP 200 + `duplicate: true` quittiert, ohne erneut verarbeitet zu werden. |
| `payload` | ja | Beliebiges JSON, max. 256 KB. |

## Responses

| HTTP | Bedeutung |
|---|---|
| `202 Accepted` | Akzeptiert, asynchron verarbeitet. Body: `{ "event_id": "...", "duplicate": false }`. |
| `200 OK`       | Duplikat. Body: `{ "event_id": "...", "duplicate": true }`. |
| `400`          | Schema- oder Header-Fehler. |
| `401`          | Signatur ungültig oder Timestamp außerhalb Skew. |
| `404`          | Endpoint-Slug unbekannt oder deaktiviert. |
| `413`          | Body > 256 KB. |
| `422`          | `event_type` nicht in `allowed_event_types`. |
| `429`          | Rate-Limit überschritten, mit `Retry-After`-Header. |
| `5xx`          | Server-seitiger Fehler — Retry empfohlen. |

## Retry-Semantik (Empfehlung für Sender)

- Bei `5xx`/`429`: Exponential Backoff (z. B. 1 s, 5 s, 30 s, 5 min, 30 min), max. 6 Versuche.
- Bei `4xx` außer `429`: nicht retryen — Payload/Signatur fixen.
- `external_id` bei allen Retries beibehalten — LabNote dedupliziert.

## Event-Routing

Events landen in `inbound_webhook_events`. Optional kann ein Endpoint einen
`auto_job_type` setzen — dann wird pro akzeptiertem Event automatisch ein
`background_jobs`-Eintrag erzeugt (Handler implementiert org-spezifische Logik:
HPLC-Result importieren, Sample-Status aktualisieren, etc.).

## Beispiel-Payloads pro Use-Case

### Gerätedaten (Synefex-Szenario)

```json
{
  "event_type": "instrument.measurement_recorded",
  "occurred_at": "2026-06-19T20:15:00Z",
  "external_id": "HPLC-A-001#run-87421",
  "payload": {
    "instrument_serial": "HPLC-A-001",
    "operator_id": "ext-user-42",
    "sample_label": "S-2026-0341",
    "method": "USP-621",
    "results": [
      { "peak": "API",      "area": 12345, "rt_min": 4.21, "unit": "AU·s" },
      { "peak": "Impurity1","area":    78, "rt_min": 5.84, "unit": "AU·s" }
    ],
    "raw_file_uri": "s3://customer-bucket/runs/87421.raw"
  }
}
```

### ERP-Bestand

```json
{
  "event_type": "stock.replenished",
  "occurred_at": "2026-06-19T20:00:00Z",
  "external_id": "po-2026-0099-line-3",
  "payload": {
    "catalog_no": "SIGMA-12345",
    "lot": "BCBN1234",
    "quantity": 500,
    "unit": "g",
    "expiry": "2028-01-31",
    "storage_location_code": "FRIDGE-B2"
  }
}
```

### Externer Agent

```json
{
  "event_type": "agent.observation",
  "occurred_at": "2026-06-19T20:30:00Z",
  "external_id": "agent-run-abc123",
  "payload": {
    "agent": "qm-watcher",
    "observation": "deviation candidate",
    "ref_experiment_id": "EXP-2026-0451",
    "confidence": 0.83
  }
}
```

## Checklist für den Receiver-Betreiber

1. Endpoint anlegen → `signing_secret` einmalig sicher speichern.
2. Skew der eigenen Systemuhr ≤ 60 s halten (NTP).
3. `external_id` ableiten aus stabiler Quelle (PO-Zeile, Geräte-Run-ID, …).
4. Bei Netzwerkfehler: Retry mit identischer `external_id`.
5. Logs/Antworten 30 Tage aufbewahren für Audit.
