# Webhook-Ereignisse

Was Samy an deine Webhook-URL schickt: Ereignisse, Header, Signatur und Wiederholungen.

Tust du etwas in der App, schickt Samy ein Ereignis an die Webhook-URL deines Packlets. Diese Seite ist zum Nachschlagen. Wie du einen Empfänger baust, steht unter [Webhook einrichten](https://packlets.app/docs/webhook.md).

## Übersicht

| `type` | `X-Samy-Event` | Wann |
|---|---|---|
| [`button`](#knopf-getippt-button) | `button.pressed` | Du tippst einen Aktions-[Knopf](https://packlets.app/docs/knoepfe.md) unter einer Nachricht. |
| [`ui`](#element-bedient-ui) | `ui.changed` | Du bedienst ein [Element](https://packlets.app/docs/elemente.md) in einer Nachricht: Button, Schalter oder Eingabefeld. |
| [`message`](#chat-nachricht-message) | `message.created` | Du schreibst in einem [Kanal mit Antworten](https://packlets.app/docs/chat.md). |

Jedes Ereignis ist ein `POST` mit JSON-Body. `type`, `packlet_id`, `event_id` und `timestamp` sind immer dabei. Alle Zeitpunkte sind ISO 8601 in UTC, nur `timestamp` ist eine Zahl.

| Feld | Bedeutung |
|---|---|
| `event_id` | Kennung des Ereignisses. Wiederholt Samy die Zustellung, bleibt sie gleich. |
| `timestamp` | Zeitpunkt der Zustellung in Sekunden seit 1970 (Unix-Zeit). |

Beide stehen im Body und sind damit von der [Signatur](#signatur) gedeckt. Damit erkennst du [wiederholte und alte Anfragen](#schutz-vor-wiederholung).

## Knopf getippt: button

```json
{
  "type": "button",
  "packlet_id": "9d2f7c10-3a5b-4c8e-9f21-6b7d8e9f0a12",
  "message_id": "5b0c1a52-7c1e-4a43-9d0b-2f6f0c7d1e11",
  "button_id": "done",
  "pressed_at": "2026-11-09T18:00:00.000Z"
}
```

| Feld | Bedeutung |
|---|---|
| `message_id` | Die Nachricht, zu der der Knopf gehört. Derselbe Wert wie in der Antwort von `POST /v1/push`. |
| `button_id` | Die `id`, die du dem Knopf gegeben hast. |
| `pressed_at` | Zeitpunkt des Tipps. |

Pro Nachricht kommt dieses Ereignis höchstens einmal: Nach dem ersten Tipp sind die Aktions-Knöpfe der Nachricht erledigt. Link-Knöpfe (mit `url`) lösen kein Ereignis aus.

## Element bedient: ui

```json
{
  "type": "ui",
  "packlet_id": "9d2f7c10-3a5b-4c8e-9f21-6b7d8e9f0a12",
  "message_id": "5b0c1a52-7c1e-4a43-9d0b-2f6f0c7d1e11",
  "element_id": "rollback",
  "value": null,
  "changed_at": "2026-11-10T18:00:00.000Z"
}
```

| Feld | Bedeutung |
|---|---|
| `message_id` | Die Nachricht, zu der das Element gehört. Derselbe Wert wie in der Antwort von `POST /v1/push`. |
| `element_id` | Die `id` des Elements. |
| `value` | Der neue Wert: `true` oder `false` beim Schalter, der Text beim Eingabefeld, `null` beim Button. |
| `changed_at` | Zeitpunkt der Bedienung. |

Den neuen Wert hat Samy schon gespeichert. Dein Tool muss ihn nicht zurückschicken.

## Chat-Nachricht: message

```json
{
  "type": "message",
  "packlet_id": "9d2f7c10-3a5b-4c8e-9f21-6b7d8e9f0a12",
  "message_id": "c41e8a77-0d1f-4e6a-b2c3-91a4d5e6f708",
  "text": "Läuft der Shop wieder?",
  "sent_at": "2026-11-10T08:15:00.000Z"
}
```

| Feld | Bedeutung |
|---|---|
| `message_id` | Deine Nachricht. |
| `text` | Was du geschrieben hast, 1 bis 4.000 Zeichen, ohne Formatierung. |
| `sent_at` | Zeitpunkt. |

## Header

| Header | Wert |
|---|---|
| `Content-Type` | `application/json` |
| `X-Samy-Event` | `button.pressed`, `ui.changed` oder `message.created` |
| `X-Samy-Signature` | `sha256=` und dahinter die Signatur als Hex-Text |
| `X-Samy-Event-Id` | Dasselbe wie `event_id` im Body |
| `X-Samy-Timestamp` | Dasselbe wie `timestamp` im Body |
| `User-Agent` | `Samy-Webhook/1.0` |

## Signatur

```text
X-Samy-Signature = "sha256=" + hex( HMAC-SHA256( Geheimnis, Body ) )
Geheimnis        = hex( SHA-256( SAMY_KEY ) )
```

- **Body** sind die rohen Bytes der Anfrage, so wie sie ankommen. Nicht das neu erzeugte JSON.
- **Geheimnis** ist der SHA-256-Hash deines Schlüssels als Hex-Text in Kleinbuchstaben, 64 Zeichen. Dieser Text ist der HMAC-Schlüssel, nicht die 32 Bytes dahinter.
- Erzeugst du in der App einen neuen Schlüssel, ändert sich auch das Geheimnis.

Fertiger Code dazu steht unter [Webhook einrichten](https://packlets.app/docs/webhook.md#4-signatur-pruefen).

## Schutz vor Wiederholung

Eine gültige Anfrage bleibt gültig, auch wenn sie jemand mitschneidet und später noch einmal schickt. Löst dein Tool etwas aus, das nicht doppelt passieren darf, prüf nach der Signatur zwei Dinge:

- **`timestamp`** liegt höchstens 5 Minuten zurück. Ältere Anfragen lehnst du ab.
- **`event_id`** hast du in diesen 5 Minuten noch nicht gesehen. Sonst antwortest du mit `200` und tust nichts.

Lies beide Werte aus dem Body, nicht aus den Headern: Nur der Body ist signiert.

## Zustellung

| Regel | Wert |
|---|---|
| Zeitlimit | 10 Sekunden bis zu deiner Antwort |
| Zeitbudget | 30 Sekunden Antwortzeit pro Minute und Konto. Ist es verbraucht, verschiebt Samy weitere Ereignisse um einige Sekunden. Antworte deshalb sofort und erledige die Arbeit danach. |
| Erfolg | jeder Status `2xx`. Der Inhalt deiner Antwort wird nicht gelesen. |
| Wird wiederholt | keine Verbindung, Zeitüberschreitung, `429`, `5xx` |
| Wird nicht wiederholt | alle anderen Antworten, auch `3xx`. Weiterleitungen folgt Samy nicht. |
| Wiederholungen | bis zu 5, also höchstens 6 Versuche |
| Abstand | erst etwa 10 bis 20 Sekunden, dann jedes Mal doppelt so lang. Nach rund zehn Minuten gibt Samy auf. |

- **Mindestens einmal:** Wegen der Wiederholungen kann dasselbe Ereignis mehrfach ankommen. Es trägt dann dieselbe `event_id`. Wie du Doppelte erkennst, steht unter [Webhook einrichten](https://packlets.app/docs/webhook.md#doppelte-ereignisse).
- **Kein Nachholen:** Hat das Packlet keine Webhook-URL, wird nichts gesendet und später auch nichts nachgeliefert.
- Jeden Versuch zeigt die App im Protokoll des Packlets, mit dem Status deiner Antwort.

## Die Webhook-URL

| Regel | |
|---|---|
| Protokoll | nur `https://` |
| Länge | bis 2.048 Zeichen |
| Zugangsdaten in der Adresse | nicht erlaubt (`https://name:passwort@…`) |
| Ziel | öffentlich erreichbar. Private Adressen wie `localhost`, `10.…`, `192.168.…` lehnt Samy ab. |
