# 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.

```javascript
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

```

```python
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/celitech-webhooks/references/verification.md), where the window can be
> dropped or widened.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/celitech-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/celitech-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/celitech-webhooks/examples/fastapi/).

## Common Event Types

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

| Event | Triggered When | Fields |
| --- | --- | --- |
| `consumption.threshold.reached` | A purchase's data usage hits a notification threshold | `purchaseId`, `dataLimitInGB`, `dataUsageRemainingInGB`, `actualPercentage`, `notificationPercentage` |
| `esim.downloaded` | The eSIM profile is downloaded to the device | `iccid` |
| `esim.installed` | The eSIM profile is installed on the device | `iccid` |
| `esim.enabled` | The eSIM is enabled on the device | `iccid` |
| `esim.disabled` | The eSIM is disabled on the device | `iccid` |
| `esim.deleted` | The eSIM is deleted from the device | `iccid` |

These six are the documented events; the list may not be exhaustive. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/celitech-webhooks/references/overview.md).

## Environment Variables

```bash
# 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

```bash
# 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

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/celitech-webhooks/references/overview.md) - Celitech webhook concepts, events, payloads, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/celitech-webhooks/references/setup.md) - Register an endpoint and get the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/celitech-webhooks/references/verification.md) - Signature verification, header fallback, manual verifier