{
  "envelope": {
    "description": "Alle Webhooks nutzen dieselbe Hülle. Signatur `HMAC-SHA256(webhook_secret, timestamp\\nraw_body)` in `X-TKE-Webhook-Signature` (Hex-Kleinbuchstaben). Im 24-Stunden-Rotationsfenster zusätzlich `X-TKE-Webhook-Signature-Old`. Mindestens einmal zugestellt; anhand `event_id` deduplizieren.",
    "fields": {
      "data": "Nutzlast je Ereignistyp (siehe unten).",
      "event_id": "Stabile, je Ereignis eindeutige UUID (Deduplizierung).",
      "event_type": "Ereignistyp.",
      "is_testmode": "true bei Erzeugung durch einen Testschlüssel.",
      "occurred_at": "ISO-8601 UTC."
    }
  },
  "health_check": {
    "data_fields": [
      "message"
    ],
    "event_type": "ping",
    "note": "Nicht über `events` an- oder abwählbar (`ping` ergibt dort `internal.validation_failed`); jeder Endpunkt empfängt es. Reale Hülle und Signatur (`X-TKE-Webhook-Signature`, ohne `-Old`) zum Prüfen der Verifikation. Bei unbekanntem `event_type` muss Ihr Handler 2xx liefern: 4xx verhindert das Speichern mit `webhook.health_check_failed` (422). `ping` erscheint nicht in `GET /v1/webhooks/deliveries`.",
    "when": "Jeder `POST /v1/webhooks` und `PUT /v1/webhooks/{id}` prüft vor dem Speichern die Erreichbarkeit"
  },
  "items": [
    {
      "data_fields": [
        "order_id",
        "external_ref",
        "items[] (order_item_id, iccid)"
      ],
      "event_type": "order.completed",
      "note": "",
      "when": "Bestellung bestätigt, eSIMs bereit"
    },
    {
      "data_fields": [
        "order_id",
        "external_ref",
        "reason"
      ],
      "event_type": "order.failed",
      "note": "",
      "when": "Bestellung nicht abgeschlossen (Bereitstellung fehlgeschlagen usw.)"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "iccid",
        "lpa"
      ],
      "event_type": "esim.delivered",
      "note": "Zuordnung über `order_id` und `external_ref`. `lpa` wird erst unmittelbar vor POST entschlüsselt und nie im Zustellprotokoll unverschlüsselt gespeichert. Keine QR-Bild-URL: QR aus `lpa` erzeugen oder `qr_code_base64` über `GET /v1/esims/{id}` abrufen.",
      "when": "Eine eSIM wurde mit Installationsdaten bereitgestellt"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "iccid",
        "activated_at"
      ],
      "event_type": "esim.activated",
      "note": "Mit `order_id` und `external_ref` (Ihrer beim Kauf gesendeten Referenz) ordnen Sie die eSIM Ihrer Bestellung zu.",
      "when": "Die eSIM wurde installiert und aktiviert (smdp enabled/in_use)"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "threshold (75/90)",
        "used_bytes",
        "data_bytes"
      ],
      "event_type": "esim.usage_threshold",
      "note": "Ordnen Sie die eSIM mit `order_id` und `external_ref` zu. Nur die höchste neu überschrittene Schwelle wird gemeldet (0→95 % sendet nur 90). `threshold` bedeutet mindestens diese Schwelle überschritten.",
      "when": "Verbrauch hat 75 % / 90 % erreicht"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "days_remaining (3/1)",
        "expires_at"
      ],
      "event_type": "esim.expiring",
      "note": "Mit `order_id` und `external_ref` (Ihrer beim Kauf gesendeten Referenz) ordnen Sie die eSIM Ihrer Bestellung zu.",
      "when": "3 Tage / 1 Tag vor Ablauf"
    },
    {
      "data_fields": [
        "threshold (25/10)",
        "balance",
        "currency"
      ],
      "event_type": "wallet.low_balance",
      "note": "`threshold` ist ein Prozentwert (25/10), nur bei bekannter Prozentzahl enthalten, sonst weggelassen (`balance` selbst vergleichen). Kein VND-Betrag unter `threshold`.",
      "when": "Wallet hat die Warnschwelle von 25 % / 10 % erreicht"
    },
    {
      "data_fields": [
        "changes[] (package_code, change: price|availability|package, occurred_at)",
        "change_count"
      ],
      "event_type": "catalog.updated",
      "note": "Keine Preise enthalten, da sie je Organisation variieren. Rufen Sie `GET /v1/catalog/packages` ab. Änderungen im kurzen Zeitfenster (Standard 10 Minuten) werden in einem `changes[]`-Webhook zusammengefasst, je Paket einmal. Hunderte synchronisierte Pakete erzeugen nicht Hunderte Webhooks. Gilt im Test- und Livebetrieb.",
      "when": "Katalog geändert: Paket hinzugefügt, geändert, ausgeblendet, Basispreis geändert oder ausverkauft"
    }
  ]
}
