Guide to Celitech Webhooks: Features and Best Practices
Celitech sells eSIM data packages through an API, so travel apps, telecom resellers and other businesses can offer mobile data to their own customers. Its webhooks report two kinds of change: a customer has used 50% or 80% of a package's data, or the eSIM itself has changed state on the customer's phone, from download and install through to enable, disable and delete. Teams use them to send top-up offers before data runs out and to track whether a customer actually installed what they bought.
What are Celitech webhooks?
Webhooks aren't on by default. Celitech enables them per account when you ask, and the Developers > Webhooks page then lets you add endpoints and pick event types. Each delivery is a JSON POST.
Celitech's docs don't name a delivery platform, but the headers are svix-id, svix-timestamp and svix-signature, and the verification samples install the svix package. The signature is the Standard Webhooks HMAC-SHA256 construction that Svix uses.
Celitech webhook features
| Feature | Details |
|---|---|
| Availability | Enabled per account on request |
| Configuration | Dashboard, Developers > Webhooks |
| Signature | Standard Webhooks HMAC-SHA256 |
| Signature header | svix-signature, with svix-id and svix-timestamp |
| Secret | One per endpoint, rotatable in the dashboard |
| Body | JSON |
| Event routing | Body field type |
| Response window | 2xx within about 15 seconds |
| Retries | Increasing delays over a period of hours, then the endpoint may be disabled |
| Test events | Sent from the dashboard, never retried |
| Endpoints | Public HTTPS only |
| Source IPs | Not published |
Events and payloads
| Documented event | Meaning |
|---|---|
consumption.threshold.reached | Sent when an eSIM's data usage crosses 50% or 80% of its package's data limit. |
esim.downloaded | Sent when an eSIM profile is downloaded to the user's device. |
esim.installed | Sent when an eSIM is installed on the user's device. |
esim.enabled | Sent when an eSIM is enabled on the user's device (turned on). |
esim.disabled | Sent when an eSIM is disabled on the user's device (turned off). |
esim.deleted | Sent when an eSIM is permanently deleted from the user's device. |
Bodies are flat, with no data wrapper. consumption.threshold.reached carries purchaseId, dataLimitInGB, dataUsageRemainingInGB, actualPercentage and notificationPercentage, the threshold that fired. The five esim.* events carry only type and the eSIM's iccid, so you look the eSIM up by ICCID to find the customer.
Celitech's API reference documents each body as an OpenAPI schema with per-field examples rather than a complete sample, and Hookdeck's samples library has none yet. Send an example event from the dashboard to see a real body before you write parsing code.
Setting up Celitech webhooks
- Ask Celitech to enable webhooks on your account if Developers > Webhooks isn't available.
- In Developers > Webhooks, add an endpoint with an HTTPS URL and a description.
- Select the event types you need and save. There is no verification challenge.
- Copy the endpoint's signing secret and store it as
CELITECH_WEBHOOK_SECRET.
Celitech's docs don't show the secret's format. Svix endpoint secrets start with whsec_, and the svix library, Hookdeck's source and the function below all base64-decode the part after that prefix. Check that your secret has it before you deploy.
To use Hookdeck, create a source with the Celitech type, paste the secret, and use the source URL as the endpoint URL. The Hookdeck CLI forwards the source to your machine for testing:
npx hookdeck-cli listen 3000 celitech --path /webhooks/celitech
Dashboard example events are a single attempt. If one fails, send it again by hand.
Securing Celitech webhooks
The signed content is <svix-id>.<svix-timestamp>.<raw body>, the key is the base64-decoded secret after whsec_, and the header can list several v1, signatures separated by spaces. The function below checks each one and uses webhook-* headers when svix-id isn't there:
const crypto = require('node:crypto');
function decodeBase64(value) {
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
const bytes = Buffer.from(value, 'base64');
return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
? bytes : null;
}
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const prefix = headers['svix-id'] ? 'svix' : 'webhook';
const id = headers[`${prefix}-id`];
const timestamp = headers[`${prefix}-timestamp`];
const signature = headers[`${prefix}-signature`];
if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
!/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
const key = decodeBase64(secret.replace(/^whsec_/, ''));
if (!key) return false;
const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
const received = decodeBase64(candidate);
if (!received) return false;
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
Celitech's own example uses new Webhook(secret).verify(...) from the svix package, which also rejects a timestamp it considers "too far in the past or future". Celitech doesn't give a number. Svix's libraries use five minutes, and an event Hookdeck retries or replays keeps the original svix-timestamp, with no timestamp tolerance applied by Hookdeck. The function above skips the age check so those deliveries still verify.
For threshold events the useful work is a customer notification, which can wait for a worker:
const express = require('express');
// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
try {
const value = JSON.parse(rawBody.toString('utf8'));
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
} catch {
return null;
}
}
// jobs.enqueue(name, payload) resolves once the job is stored.
function createCelitechReceiver(jobs) {
const app = express();
app.post('/webhooks/celitech', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.CELITECH_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
const event = parseJsonObject(req.body);
if (!event) return res.sendStatus(400); // signed, but not a JSON object
const deliveryId = req.headers['svix-id'] || req.headers['webhook-id'];
const job = event.type === 'consumption.threshold.reached' ? 'offer-top-up' : 'sync-esim-state';
try {
await jobs.enqueue(job, { deliveryId, event });
} catch {
return res.sendStatus(503);
}
return res.sendStatus(200); // well inside the 15-second window
});
return app;
}
When the request comes through a Hookdeck Celitech source, a bad signature doesn't produce a 401. Hookdeck answers 200 and marks the request as failed verification, so it never reaches the receiver.
Make Celitech webhooks production-ready. Hookdeck Event Gateway verifies the Svix signature on every eSIM and data-usage event, records each delivery, and holds events for your notification service while it is unavailable.
Celitech webhook limitations and pain points
Access depends on Celitech
The Problem: The webhook settings aren't there when you go to build the integration.
Why It Happens: Webhook access is enabled per account by Celitech, on request.
Workarounds:
- Ask for webhooks early in the project, and poll the usage API until they are on.
How Hookdeck Can Help: Once you have access, a Hookdeck source gives you a stable URL and a request log from the first delivery, which helps when you are seeing Celitech's bodies for the first time.
The endpoint can be disabled
The Problem: After a long outage, Celitech stops sending events to your endpoint.
Why It Happens: Failed deliveries are retried with increasing delays "over a period of hours", and after that the endpoint may be disabled. Celitech doesn't publish the attempt count or intervals.
Workarounds:
- Check the endpoint's status in Developers > Webhooks after any outage.
- Reconcile usage through the API for purchases that should have crossed a threshold.
How Hookdeck Can Help: Hookdeck acknowledges Celitech straight away, so your downtime doesn't count against the endpoint, and holds the events until your service can take them.
Thin eSIM events
The Problem: An esim.deleted event tells you which eSIM, but not which customer or purchase.
Why It Happens: The esim.* bodies carry only type and iccid.
Workarounds:
- Store the ICCID against the customer when you create the purchase, and look it up on each event.
How Hookdeck Can Help: A transformation can't add your customer data, but it can rename or reshape fields so every Celitech event reaches your service in the format it already expects.
The secret format is undocumented
The Problem: You can't confirm from the docs how to use the secret.
Why It Happens: Celitech never prints a secret value. It is most likely a Svix whsec_ secret, but the docs don't say.
Workarounds:
- Look at the secret in the dashboard. If it doesn't start with
whsec_, contact Celitech before relying on any verifier.
How Hookdeck Can Help: Send a dashboard example event to a Hookdeck source and check the verification result in the request log before you go live.
Best practices
Deduplicate on svix-id
Every retry of a delivery carries the same svix-id. Store it before acting, so a retried threshold event doesn't send two top-up emails. There is more on this in implementing webhook idempotency.
Use both thresholds
The 50% and 80% notifications arrive separately for the same purchaseId. Check notificationPercentage and send a gentle reminder at 50% and a stronger one at 80%, or act only on the one that fits your product.
Track installation, not only purchase
esim.installed and esim.enabled tell you the customer actually set the eSIM up. Purchases with no install event after a day or two are a good trigger for a support message.
Acknowledge quickly
Return 2xx well inside the 15-second window and send notifications from a worker. See why to process webhooks asynchronously.
Conclusion
Celitech's six events track data running low and eSIM state changes on a device. Request access early, and keep the endpoint healthy so it isn't disabled after hours of failures.
Hookdeck Event Gateway gives Celitech a target that is always up and gives you a record of every eSIM event. Try it free.