Agent skill

Celitech Webhooks Skill

Receive and verify Celitech webhooks (delivered via Svix). Use when setting up Celitech eSIM webhook handlers, debugging svix-signature verification, or handling events like consumption.threshold.reached, esim.downloaded, esim.installed, esim.enabled, esim.disabled, and esim.deleted.

Install this skill

npx skills add hookdeck/webhook-skills --skill celitech-webhooks


Celitech (B2B eSIM connectivity API) sends webhooks through Svix for data-usage thresholds and eSIM lifecycle changes. Every delivery is an HTTP POST with a JSON body and three signature headers: svix-id, svix-timestamp, and svix-signature. Webhooks are enabled per account by Celitech on request.

When to Use This Skill

  • How do I receive Celitech webhooks?
  • How do I verify Celitech webhook signatures (svix-signature)?
  • How do I handle consumption.threshold.reached or esim.installed events?
  • Why is my Celitech webhook signature verification failing?
  • Why are Hookdeck retries of Celitech webhooks rejected with a timestamp error?

Verification (core)

Signed content is {svix-id}.{svix-timestamp}.{raw body}. Celitech signs it with HMAC-SHA256 using the base64-decoded secret (the part after whsec_), base64-encodes the result, and sends one or more space-delimited v1,<signature> entries. Use the official svix library; it compares every v1 entry in constant time.

import { Webhook } from 'svix'; // svix 2.x is ESM-only

// Prefer svix-*; fall back to webhook-* when svix-id is absent (as Hookdeck does)
const p = req.headers['svix-id'] ? 'svix' : 'webhook';
const headers = {
  'svix-id': req.headers[`${p}-id`],
  'svix-timestamp': req.headers[`${p}-timestamp`],
  'svix-signature': req.headers[`${p}-signature`],
};
// rawBody: the raw Buffer/string (express.raw({ type: '*/*' })), never re-serialized JSON
new Webhook(process.env.CELITECH_WEBHOOK_SECRET).verify(rawBody, headers); // throws on failure
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["CELITECH_WEBHOOK_SECRET"])
wh.verify(raw_body, {"svix-id": msg_id, "svix-timestamp": ts, "svix-signature": sig})

Missing headers or a bad signature: return 400. If CELITECH_WEBHOOK_SECRET is unset, fail closed (the examples return 500).

Timestamp window and retries. The svix library always enforces a fixed 5-minute timestamp window. Hookdeck retries and replays resend the original timestamp, so the library rejects any retry or replay that arrives after 5 minutes. If you receive through Hookdeck, use the manual verifier in references/verification.md, where the window can be dropped or widened.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Common Event Types

The body is flat (no data wrapper): a top-level type plus the event's fields.

EventTriggered WhenFields
consumption.threshold.reachedA purchase's data usage hits a notification thresholdpurchaseId, dataLimitInGB, dataUsageRemainingInGB, actualPercentage, notificationPercentage
esim.downloadedThe eSIM profile is downloaded to the deviceiccid
esim.installedThe eSIM profile is installed on the deviceiccid
esim.enabledThe eSIM is enabled on the deviceiccid
esim.disabledThe eSIM is disabled on the deviceiccid
esim.deletedThe eSIM is deleted from the deviceiccid

These six are the documented events; the list may not be exhaustive. See references/overview.md.

Environment Variables

# Developers → Webhooks → your endpoint (Svix secret, whsec_ + base64)
CELITECH_WEBHOOK_SECRET=whsec_xxxxx

The secret page does not show the literal value in Celitech's docs; Svix endpoint secrets are whsec_<base64>, which is unconfirmed for Celitech specifically. Store the full value, including whsec_.

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 celitech --path /webhooks/celitech

Register the tunnel URL under Developers → Webhooks in the Celitech dashboard.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →