# Webhooks

> Ereignisse zu Sitzungen, Zahlungen und Geräten an dein System senden lassen, mit Signaturprüfung.

Webhooks melden Ereignisse an eine HTTPS-Adresse deiner Wahl, zum Beispiel wenn eine Ladung beginnt oder abgerechnet ist. So musst du die API nicht ständig abfragen.

## Endpunkt anlegen

```bash
curl -X POST https://api.ladehase.com/v1/webhook_endpoints \
  -H "Authorization: Bearer lh_test_…" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.ch/webhooks/ladehase", "events": ["session.settled", "device.offline"] }'
```

Die Antwort enthält das Geheimnis `secret` für die Signaturprüfung. Es wird nur in dieser Antwort angezeigt. Ohne `events` erhältst du alle Ereignisse (`["*"]`).

## Ereignisse

<table>
  <thead>
    <tr><th>Typ</th><th>Wann</th></tr>
  </thead>
  <tbody>
    <tr><td>`session.created`</td><td>Sitzung angelegt</td></tr>
    <tr><td>`session.started`</td><td>Ladung hat begonnen</td></tr>
    <tr><td>`session.stopped`</td><td>Ladung beendet</td></tr>
    <tr><td>`session.settled`</td><td>Sitzung abgerechnet, Quittung erstellt</td></tr>
    <tr><td>`session.failed`</td><td>Zahlung fehlgeschlagen, verfallen oder abgebrochen</td></tr>
    <tr><td>`payment.authorized`</td><td>Betrag reserviert oder Paket bezahlt</td></tr>
    <tr><td>`payment.captured`</td><td>Endbetrag belastet</td></tr>
    <tr><td>`payment.released`</td><td>Reservation freigegeben</td></tr>
    <tr><td>`payment.refunded`</td><td>Rückerstattung ausgeführt</td></tr>
    <tr><td>`device.online`, `device.offline`, `device.error`</td><td>Zustand eines Ladepunkts hat sich geändert</td></tr>
    <tr><td>`terminal.online`, `terminal.offline`</td><td>Zustand eines Terminals hat sich geändert</td></tr>
    <tr><td>`integration.error`</td><td>Verbindung zu Hersteller oder Zahlungspartner gestört</td></tr>
  </tbody>
</table>

`session.updated` wird nur live an das Dashboard verteilt und nicht als Webhook versendet, weil es während einer Ladung sehr häufig vorkommt.

## Aufbau

```http
POST /webhooks/ladehase HTTP/1.1
Content-Type: application/json
User-Agent: Ladehase-Webhooks/1.0
Ladehase-Event: session.settled
Ladehase-Signature: t=1792958400,v1=5f2b9c…
```

```json
{
  "id": "evt_…",
  "object": "event",
  "type": "session.settled",
  "livemode": true,
  "created_at": "2026-10-25T08:30:00.000Z",
  "data": { "id": "ses_…", "object": "session", "state": "settled", "captured_amount": 950 }
}
```

## Signatur prüfen

Die Kopfzeile `Ladehase-Signature` enthält einen Zeitstempel `t` und die Signatur `v1`. `v1` ist ein HMAC-SHA256 über `t` + `.` + den unveränderten Rohtext der Anfrage, mit dem Geheimnis des Endpunkts als Schlüssel, hexadezimal codiert.

1. Lies den Rohtext der Anfrage, bevor du ihn als JSON verarbeitest.
2. Berechne `HMAC_SHA256(secret, t + "." + rohtext)`.
3. Vergleiche das Ergebnis zeitkonstant mit `v1`.
4. Lehne Anfragen ab, deren `t` mehr als 5 Minuten von der aktuellen Zeit abweicht.

Mit dem SDK geht das in einer Zeile:

```ts
import { verifyWebhook } from "@ladehase/sdk";

export async function POST(request: Request) {
  const body = await request.text();
  const ok = await verifyWebhook(body, request.headers.get("ladehase-signature") ?? "", process.env.LADEHASE_WEBHOOK_SECRET!);
  if (!ok) return new Response("Signatur ungültig", { status: 400 });
  const event = JSON.parse(body);
  // Ereignis verarbeiten …
  return new Response(null, { status: 204 });
}
```

Ohne SDK, in Node.js:

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

function verify(body: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}
```

> **Hinweis:** Vertrau dem Inhalt eines Webhooks nicht blind. Lies bei wichtigen Entscheiden die Sitzung mit `GET /v1/sessions/{id}` nach.

## Antwort und Wiederholungen

Antworte innert 10 Sekunden mit einem Status 2xx. Andernfalls versucht Ladehase es erneut: nach 1 Minute, 5 Minuten, 30 Minuten, 2, 6, 12, 24 und nochmals 24 Stunden, insgesamt rund 3 Tage. Ereignisse können doppelt ankommen. Prüfe darum die `id` und verarbeite jedes Ereignis nur einmal.

Das Zustellprotokoll siehst du mit `GET /v1/webhook_endpoints/{id}/deliveries`. Einzelne Ereignisse sendest du mit `POST /v1/events/{id}/resend` erneut.
