Tổng Kho eSIM
Outbound Webhooks · 8 events

Webhooks

The system delivers events to URLs registered via POST /v1/webhooks (updated via PUT /v1/webhooks/{id}; your endpoint must return 2xx for CHECK_HEALTH upon creation). Webhook dispatch is an outbound push channel signed with a dedicated secret, independent of your API key secret.

Webhooks are a convenience stream, not the source of truth. REST is the ultimate source of truth: even if your webhook receiver drops an event, GET /v1/orders?updated_since=… fully reconstructs accurate system state. Design your architecture with this guarantee in mind.

webhook-events.en.json

Event Catalog

event_typeWhen triggeredKey data fields
order.completedOrder committed; eSIMs are ready
order_idexternal_refitems[] (order_item_id, iccid)
order.failedOrder did not complete (provisioning failure, etc.)
order_idexternal_refreason
esim.deliveredAn eSIM was provisioned with an installation payload

Match by `order_id` and `external_ref`. `lpa` is decrypted just before POST, never stored as plaintext in deliveries. No QR image URL is sent: generate QR from `lpa` or fetch `qr_code_base64` through `GET /v1/esims/{id}`.

order_item_idorder_idexternal_reficcidlpa
esim.activatedThe eSIM was installed and activated (smdp enabled/in_use)

Use `order_id` and `external_ref` (your order reference sent at purchase) to match this eSIM to your order.

order_item_idorder_idexternal_reficcidactivated_at
esim.usage_thresholdUsage reached the 75% / 90% threshold

Match the eSIM to your order using `order_id` and `external_ref`. Only the highest newly crossed threshold is emitted (0→95% sends only 90). `threshold` means at least this level has been exceeded.

order_item_idorder_idexternal_refthreshold (75/90)used_bytesdata_bytes
esim.expiring3 days / 1 day before expiry

Use `order_id` and `external_ref` (your order reference sent at purchase) to match this eSIM to your order.

order_item_idorder_idexternal_refdays_remaining (3/1)expires_at
wallet.low_balanceWallet reached the 25% / 10% warning threshold

`threshold` is a percentage (25/10), included only when a real percentage is known; otherwise omitted (compare `balance` yourself). Never send raw VND under `threshold`.

threshold (25/10)balancecurrency
catalog.updatedCatalog changed: package added, edited or hidden, base price changed, or out of stock

No price is included because prices differ by organization. Call `GET /v1/catalog/packages` for your prices. Changes within a short window (default 10 minutes) are merged into one `changes[]` webhook; each package appears once. A sync of hundreds of packages does not send hundreds of webhooks. Applies to test and live modes.

changes[] (package_code, change: price|availability|package, occurred_at)change_count
json
{
  "event_id": "018f3c00-0000-7000-8000-0000000000c1",
  "event_type": "ping",
  "occurred_at": "2026-07-11T07:29:00Z",
  "is_testmode": true,
  "data": {
    "message": "TongKho eSIM webhook health check"
  }
}

Webhook Envelope & Signature Headers

All webhooks use one envelope. Sign `HMAC-SHA256(webhook_secret, timestamp\nraw_body)` in `X-TKE-Webhook-Signature` (lowercase hex). During the 24-hour rotation window also send `X-TKE-Webhook-Signature-Old`. At-least-once delivery; deduplicate by `event_id`.

json
{
  "event_id": "018f3c00-0000-7000-8000-0000000000b0",
  "event_type": "order.completed",
  "occurred_at": "2026-07-11T07:30:00Z",
  "is_testmode": false,
  "data": {
    "order_id": "018f2b00-0000-7000-8000-00000000009a",
    "external_ref": "partner-cart-9931",
    "items": [
      {
        "order_item_id": "018f2b00-0000-7000-8000-000000000001",
        "iccid": "8984012345678901234"
      }
    ]
  }
}
FieldDescription
dataPayload by event type (see below).
event_idStable UUID unique to each event (deduplication).
event_typeEvent type.
is_testmodetrue if generated by a test key.
occurred_atISO-8601 UTC.

Catalog Sync: catalog.updated

Emitted when packages change: new/edited/hidden packages, base price changes, or out-of-stock. Payload contains an aggregated changes[] array: hundreds of package updates result in one delivery per endpoint.

json
{
  "event_id": "018f3c00-0000-7000-8000-0000000000c1",
  "event_type": "catalog.updated",
  "occurred_at": "2026-07-11T08:10:00Z",
  "is_testmode": false,
  "data": {
    "changes": [
      {
        "package_code": "THAY-BANG-MA-GOI-CUA-BAN",
        "change": "price",
        "occurred_at": "2026-07-11T08:02:11Z"
      },
      {
        "package_code": "VN_10GB_30D",
        "change": "availability",
        "occurred_at": "2026-07-11T08:04:52Z"
      },
      {
        "package_code": "JP_3GB_7D",
        "change": "package",
        "occurred_at": "2026-07-11T08:09:30Z"
      }
    ],
    "change_count": 3
  }
}
No price in payload: intentional design

Selling prices are organization-specific . Emitting a single rate would be both inaccurate and a confidentiality leak. This event is a trigger : call GET /v1/catalog/packages?market=… to refresh your pricing.

change accepts three values: price (base cost change), availability (in/out of stock), package (new / updated / hidden). If an unexpected value appears, treat it as a trigger to sync.

Signing, retry/backoff, and dedupe by event_id match standard events. Applies across both test and live environments, separated by is_testmode.

How to Verify Webhook Signatures

Every outgoing webhook carries an X-TKE-Signature header computed using HMAC-SHA256 over the webhook_secret and the raw request body bytes.

text
signing_string = timestamp + "\n" + raw_body   # timestamp = header X-TKE-Webhook-Timestamp
signature      = HMAC_SHA256(webhook_secret, signing_string)   → hex lowercase

# Verify freshness (±300s) to prevent replay.
# Sign over exact received raw bytes. DO NOT re-serialize JSON.

Retry Backoff Policy

1 phút→5 phút→30 phút→2 giờ→6 giờ

If your receiver fails (non-2xx response) or times out after 5 seconds, the system retries exponentially over a 24 giờ window: 1 phút → 5 phút → 30 phút → 2 giờ → 6 giờ.

4 Critical Webhook Guidelines

1. Asynchronous processing & Instant 200 OK
Your webhook receiver should enqueue payloads immediately and return HTTP 200 OK (< 1s). Avoid heavy synchronous tasks within webhook requests, as requests timeout after 5 seconds.
2. Idempotency & Deduplication
Webhooks guarantee at-least-once delivery. Use event_id or data.order_id / data.order_item_id to deduplicate incoming payloads and ensure idempotency.
3. Separate Endpoints for Live and Test Mode
Each API key maintains separate webhook configurations. Never share a single webhook receiver URL between live and test keys to avoid test data polluting production databases.
4. Periodic Pull Reconciliation
Webhooks may be permanently dropped during prolonged network outages. Periodically poll GET /v1/orders?updated_since=… to catch any missing state transitions.

Field Notes & Behaviors

esim.delivered.lpa is decrypted right before POST dispatch (not stored plaintext). Payload does not include QR image URL: construct QR from LPA or fetch via GET /v1/esims/{id}.

esim.usage_threshold: fires highest exceeded threshold only (jump from 0→95% emits threshold=90 only).

wallet.low_balance.threshold is a percentage (25/10), not a raw VND amount.

catalog.updated contains no pricing or granular inventory: only package code + change type.