{
  "envelope": {
    "description": "Tous les webhooks partagent une enveloppe. Signature `HMAC-SHA256(webhook_secret, timestamp\\nraw_body)` dans `X-TKE-Webhook-Signature` (hex minuscule). Pendant la rotation de 24 h, ajout de `X-TKE-Webhook-Signature-Old`. Livraison au moins une fois ; dédupliquez par `event_id`.",
    "fields": {
      "data": "Contenu selon le type d'événement (voir ci-dessous).",
      "event_id": "UUID stable, unique par événement (déduplication).",
      "event_type": "Type d'événement.",
      "is_testmode": "true si généré par une clé de test.",
      "occurred_at": "ISO-8601 UTC."
    }
  },
  "health_check": {
    "data_fields": [
      "message"
    ],
    "event_type": "ping",
    "note": "Non configurable via `events` (y mettre `ping` renvoie `internal.validation_failed`) : tous les endpoints le reçoivent. Même enveloppe et signature réelle (`X-TKE-Webhook-Signature`, sans `-Old`) pour tester la vérification. Retournez 2xx pour un `event_type` inconnu : 4xx fait échouer la création ou modification avec `webhook.health_check_failed` (422), sans sauvegarde. `ping` n'apparaît pas dans `GET /v1/webhooks/deliveries`.",
    "when": "À chaque `POST /v1/webhooks` et `PUT /v1/webhooks/{id}`, contrôle de disponibilité avant sauvegarde"
  },
  "items": [
    {
      "data_fields": [
        "order_id",
        "external_ref",
        "items[] (order_item_id, iccid)"
      ],
      "event_type": "order.completed",
      "note": "",
      "when": "Commande confirmée, eSIM prêtes"
    },
    {
      "data_fields": [
        "order_id",
        "external_ref",
        "reason"
      ],
      "event_type": "order.failed",
      "note": "",
      "when": "Commande non terminée (échec de fourniture, etc.)"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "iccid",
        "lpa"
      ],
      "event_type": "esim.delivered",
      "note": "Rattachez via `order_id` et `external_ref`. `lpa` est déchiffré juste avant POST, jamais stocké en clair dans les envois. Aucune URL d'image QR : générez depuis `lpa` ou lisez `qr_code_base64` avec `GET /v1/esims/{id}`.",
      "when": "Une eSIM a été fournie avec ses données d'installation"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "iccid",
        "activated_at"
      ],
      "event_type": "esim.activated",
      "note": "Utilisez `order_id` et `external_ref` (votre référence envoyée à l'achat) pour rattacher l'eSIM à votre commande.",
      "when": "L'eSIM a été installée et activée (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": "Rattachez l'eSIM via `order_id` et `external_ref`. Seul le plus haut seuil nouvellement franchi est émis (0→95 % émet seulement 90). `threshold` signifie au moins ce seuil dépassé.",
      "when": "Utilisation ayant atteint le seuil de 75 % / 90 %"
    },
    {
      "data_fields": [
        "order_item_id",
        "order_id",
        "external_ref",
        "days_remaining (3/1)",
        "expires_at"
      ],
      "event_type": "esim.expiring",
      "note": "Utilisez `order_id` et `external_ref` (votre référence envoyée à l'achat) pour rattacher l'eSIM à votre commande.",
      "when": "3 jours / 1 jour avant expiration"
    },
    {
      "data_fields": [
        "threshold (25/10)",
        "balance",
        "currency"
      ],
      "event_type": "wallet.low_balance",
      "note": "`threshold` est un pourcentage (25/10), inclus seulement si connu, sinon absent (comparez `balance`). Jamais de montant VND brut dans `threshold`.",
      "when": "Le portefeuille a atteint le seuil d'alerte de 25 % / 10 %"
    },
    {
      "data_fields": [
        "changes[] (package_code, change: price|availability|package, occurred_at)",
        "change_count"
      ],
      "event_type": "catalog.updated",
      "note": "Aucun prix inclus car les prix varient selon l'organisation. Consultez `GET /v1/catalog/packages`. Les changements d'une fenêtre courte (10 minutes par défaut) sont regroupés dans un seul webhook `changes[]`, une fois par forfait. Une synchronisation de centaines de forfaits n'envoie pas des centaines de webhooks. Modes test et réel.",
      "when": "Catalogue modifié : forfait ajouté, modifié, masqué, changement de prix de base ou rupture de stock"
    }
  ]
}
