# Webhooks

> Have events about sessions, payments and devices sent to your system, with signature verification.

Webhooks report events to an HTTPS URL of your choice, for example when charging starts or a session is settled. You don't have to poll the API.

## Create an endpoint

```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"] }'
```

The response contains the `secret` for signature verification. It is shown only in this response. Without `events` you receive all events (`["*"]`).

## Events

<table>
  <thead>
    <tr><th>Type</th><th>When</th></tr>
  </thead>
  <tbody>
    <tr><td>`session.created`</td><td>Session created</td></tr>
    <tr><td>`session.started`</td><td>Charging started</td></tr>
    <tr><td>`session.stopped`</td><td>Charging ended</td></tr>
    <tr><td>`session.settled`</td><td>Session settled, receipt created</td></tr>
    <tr><td>`session.failed`</td><td>Payment failed, expired or cancelled</td></tr>
    <tr><td>`payment.authorized`</td><td>Amount reserved or package paid</td></tr>
    <tr><td>`payment.captured`</td><td>Final amount captured</td></tr>
    <tr><td>`payment.released`</td><td>Reservation released</td></tr>
    <tr><td>`payment.refunded`</td><td>Refund executed</td></tr>
    <tr><td>`device.online`, `device.offline`, `device.error`</td><td>Charge point state changed</td></tr>
    <tr><td>`terminal.online`, `terminal.offline`</td><td>Terminal state changed</td></tr>
    <tr><td>`integration.error`</td><td>Connection to manufacturer or payment partner disrupted</td></tr>
  </tbody>
</table>

`session.updated` is only distributed live to the dashboard and not sent as a webhook, because it occurs very often during charging.

## Structure

```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 }
}
```

## Verify the signature

The `Ladehase-Signature` header contains a timestamp `t` and the signature `v1`. `v1` is an HMAC-SHA256 over `t` + `.` + the unmodified raw request body, keyed with the endpoint secret, hex encoded.

1. Read the raw body before parsing it as JSON.
2. Compute `HMAC_SHA256(secret, t + "." + rawBody)`.
3. Compare the result with `v1` in constant time.
4. Reject requests whose `t` differs from the current time by more than 5 minutes.

With the SDK it is one line:

```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("Invalid signature", { status: 400 });
  const event = JSON.parse(body);
  // handle event …
  return new Response(null, { status: 204 });
}
```

Without the 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:** Don't trust webhook content blindly. For important decisions, read the session again with `GET /v1/sessions/{id}`.

## Response and retries

Respond within 10 seconds with a 2xx status. Otherwise Ladehase retries after 1 minute, 5 minutes, 30 minutes, 2, 6, 12, 24 and another 24 hours, about 3 days in total. Events may arrive twice, so check the `id` and process each event only once.

See the delivery log with `GET /v1/webhook_endpoints/{id}/deliveries`. Resend single events with `POST /v1/events/{id}/resend`.
